package luckygift

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"hyapp-admin-server/internal/integration/luckygiftadmin"
	"hyapp-admin-server/internal/middleware"
	luckygiftv1 "hyapp.local/api/proto/luckygift/v1"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type poolAdjustmentClientStub struct {
	luckygiftadmin.Client
	request  *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest
	response *luckygiftv1.AdjustLuckyGiftPoolBalanceResponse
	err      error
}

func (s *poolAdjustmentClientStub) AdjustLuckyGiftPoolBalance(_ context.Context, request *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
	s.request = request
	return s.response, s.err
}

func TestConfigToProtoConvertsBusinessUnitsToPPM(t *testing.T) {
	config := configToProto(configRequest{
		PoolID:                   " lucky_100 ",
		Enabled:                  true,
		TargetRTPPercent:         95,
		PoolRatePercent:          96,
		SettlementWindowWager:    1_000_000,
		ControlBandPercent:       1,
		GiftPriceReference:       100,
		NoviceMaxEquivalentDraws: 2_000,
		NormalMaxEquivalentDraws: 20_000,
		Stages: []stageDTO{
			{
				Stage: " novice ",
				Tiers: []tierDTO{
					{
						TierID:             " novice_0_3x ",
						Multiplier:         0.3,
						ProbabilityPercent: 22,
						Enabled:            true,
					},
				},
			},
		},
	})

	if config.GetPoolId() != "lucky_100" {
		t.Fatalf("pool id = %q, want lucky_100", config.GetPoolId())
	}
	if config.GetTargetRtpPpm() != 950_000 {
		t.Fatalf("target rtp ppm = %d, want 950000", config.GetTargetRtpPpm())
	}
	if config.GetPoolRatePpm() != 960_000 {
		t.Fatalf("pool rate ppm = %d, want 960000", config.GetPoolRatePpm())
	}
	if config.GetControlBandPpm() != 10_000 {
		t.Fatalf("control band ppm = %d, want 10000", config.GetControlBandPpm())
	}
	if config.GetNoviceMaxEquivalentDraws() != 2_000 || config.GetNormalMaxEquivalentDraws() != 20_000 {
		t.Fatalf("equivalent draw thresholds mismatch: novice=%d normal=%d", config.GetNoviceMaxEquivalentDraws(), config.GetNormalMaxEquivalentDraws())
	}
	tier := config.GetStages()[0].GetTiers()[0]
	if tier.GetStage() != "novice" {
		t.Fatalf("tier stage = %q, want novice", tier.GetStage())
	}
	if tier.GetTierId() != "novice_0_3x" {
		t.Fatalf("tier id = %q, want novice_0_3x", tier.GetTierId())
	}
	if tier.GetMultiplierPpm() != 300_000 {
		t.Fatalf("multiplier ppm = %d, want 300000", tier.GetMultiplierPpm())
	}
	if tier.GetBaseWeightPpm() != 220_000 {
		t.Fatalf("base weight ppm = %d, want 220000", tier.GetBaseWeightPpm())
	}
}

func TestCreditLuckyGiftPoolBalanceForwardsAuditedOwnerCommand(t *testing.T) {
	gin.SetMode(gin.TestMode)
	client := &poolAdjustmentClientStub{response: &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{
		Adjustment: &luckygiftv1.LuckyGiftPoolAdjustment{
			AdjustmentId: "adjustment-1", AppCode: "aslan", PoolId: "lucky", StrategyVersion: "dynamic_v3",
			Direction: "in", AmountCoins: 100, Reason: "launch budget", OperatorAdminId: 7, BalanceBefore: 0, BalanceAfter: 100,
		},
		Pool: &luckygiftv1.LuckyGiftPoolBalance{
			AppCode: "aslan", PoolId: "lucky", StrategyVersion: "dynamic_v3", Balance: 100,
			ManualCreditTotal: 100, Materialized: true,
		},
	}}
	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodPost, "/admin/ops-center/lucky-gifts/pools/credit?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3", bytes.NewBufferString(`{"adjustment_id":"adjustment-1","amount_coins":100,"reason":"launch budget"}`))
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set(middleware.ContextUserID, uint(7))
	c.Set(middleware.ContextRequestID, "request-1")
	New(client, time.Second, nil).CreditLuckyGiftPoolBalance(c)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.request == nil || client.request.GetDirection() != "in" || client.request.GetOperatorAdminId() != 7 ||
		client.request.GetMeta().GetAppCode() != "aslan" || client.request.GetMeta().GetRequestId() != "request-1" {
		t.Fatalf("owner request=%+v", client.request)
	}
	var envelope struct {
		Code int `json:"code"`
		Data struct {
			Pool struct {
				StrategyVersion   string `json:"strategy_version"`
				ManualCreditTotal int64  `json:"manual_credit_total"`
			} `json:"pool"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != 0 || envelope.Data.Pool.StrategyVersion != "dynamic_v3" || envelope.Data.Pool.ManualCreditTotal != 100 {
		t.Fatalf("response=%s", recorder.Body.String())
	}
}

func TestDebitLuckyGiftPoolBalanceMapsOwnerConflictToHTTP409(t *testing.T) {
	gin.SetMode(gin.TestMode)
	client := &poolAdjustmentClientStub{err: status.Error(codes.FailedPrecondition, "debit would cross reserve floor")}
	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(http.MethodPost, "/admin/ops-center/lucky-gifts/pools/debit?app_code=aslan&pool_id=lucky&strategy_version=dynamic_v3", bytes.NewBufferString(`{"adjustment_id":"adjustment-2","amount_coins":101,"reason":"return budget"}`))
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set(middleware.ContextUserID, uint(7))
	New(client, time.Second, nil).DebitLuckyGiftPoolBalance(c)
	if recorder.Code != http.StatusConflict {
		t.Fatalf("status=%d want=409 body=%s", recorder.Code, recorder.Body.String())
	}
}

func TestConfigToProtoGeneratesUniqueTierIDs(t *testing.T) {
	config := configToProto(configRequest{
		PoolID: "lucky",
		Stages: []stageDTO{
			{
				Stage: "advanced",
				Tiers: []tierDTO{
					{TierID: "manual_one", Multiplier: 0, ProbabilityPercent: 50, Enabled: true},
					{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, Enabled: true},
					{TierID: "manual_two", Multiplier: 5, ProbabilityPercent: 25, Enabled: true},
					{TierID: "manual_decimal", Multiplier: 0.5, ProbabilityPercent: 0},
				},
			},
		},
	})

	tiers := config.GetStages()[0].GetTiers()
	got := []string{tiers[0].GetTierId(), tiers[1].GetTierId(), tiers[2].GetTierId(), tiers[3].GetTierId()}
	want := []string{"advanced_none", "advanced_5x", "advanced_5x_2", "advanced_0_5x"}
	for index := range want {
		if got[index] != want[index] {
			t.Fatalf("tier id[%d] = %q, want %q; all=%v", index, got[index], want[index], got)
		}
	}
}

func TestConfigFromProtoConvertsPPMToBusinessUnits(t *testing.T) {
	config := configFromProto(&luckygiftv1.LuckyGiftRuleConfig{
		AppCode:                  "hyapp",
		PoolId:                   "lucky_100",
		RuleVersion:              7,
		Enabled:                  true,
		TargetRtpPpm:             950_000,
		PoolRatePpm:              960_000,
		SettlementWindowWager:    1_000_000,
		ControlBandPpm:           10_000,
		GiftPriceReference:       100,
		NoviceMaxEquivalentDraws: 2_000,
		NormalMaxEquivalentDraws: 20_000,
		CreatedByAdminId:         12,
		CreatedAtMs:              1_700_000_000_000,
		Stages: []*luckygiftv1.LuckyGiftRuleStage{
			{
				Stage: "novice",
				Tiers: []*luckygiftv1.LuckyGiftRuleTier{
					{
						TierId:        "novice_0_3x",
						MultiplierPpm: 300_000,
						BaseWeightPpm: 220_000,
						Enabled:       true,
					},
				},
			},
		},
	})

	if config.TargetRTPPercent != 95 {
		t.Fatalf("target rtp percent = %v, want 95", config.TargetRTPPercent)
	}
	if config.PoolRatePercent != 96 {
		t.Fatalf("pool rate percent = %v, want 96", config.PoolRatePercent)
	}
	if config.ControlBandPercent != 1 {
		t.Fatalf("control band percent = %v, want 1", config.ControlBandPercent)
	}
	if config.NoviceMaxEquivalentDraws != 2_000 || config.NormalMaxEquivalentDraws != 20_000 {
		t.Fatalf("equivalent draw thresholds were not copied from proto: novice=%d normal=%d", config.NoviceMaxEquivalentDraws, config.NormalMaxEquivalentDraws)
	}
	if config.Stages[0].Tiers[0].Multiplier != 0.3 {
		t.Fatalf("multiplier = %v, want 0.3", config.Stages[0].Tiers[0].Multiplier)
	}
	if config.Stages[0].Tiers[0].ProbabilityPercent != 22 {
		t.Fatalf("probability percent = %v, want 22", config.Stages[0].Tiers[0].ProbabilityPercent)
	}
}

func TestDynamicConfigAdminMappingRoundTrip(t *testing.T) {
	protoConfig := configToProto(configRequest{
		AppCode:                       " LALU ",
		PoolID:                        " lucky ",
		StrategyVersion:               " DYNAMIC_V3 ",
		Enabled:                       true,
		TargetRTPPercent:              98,
		PoolRatePercent:               98,
		ProfitRatePercent:             1,
		AnchorRatePercent:             1,
		SettlementWindowWager:         1_000_000,
		ControlBandPercent:            3,
		GiftPriceReference:            100,
		InitialPoolCoins:              1_000_000,
		LossStreakGuarantee:           5,
		LowWatermarkCoins:             10_000_000,
		LowWaterNonzeroFactorPercent:  70,
		HighWatermarkCoins:            20_000_000,
		HighWaterNonzeroFactorPercent: 130,
		RechargeBoostWindowMS:         300_000,
		RechargeBoostFactorPercent:    110,
		JackpotMultipliers:            []float64{200, 500, 1000},
		JackpotGlobalRTPMaxPercent:    98,
		JackpotUserDayRTPMaxPercent:   96,
		JackpotUser72hRTPMaxPercent:   96,
		JackpotSpendThresholdCoins:    5_000,
		MaxJackpotHitsPerUserDay:      5,
		MaxSinglePayout:               10_000,
		UserHourlyPayoutCap:           20_000,
		UserDailyPayoutCap:            30_000,
		DeviceDailyPayoutCap:          40_000,
		RoomHourlyPayoutCap:           50_000,
		AnchorDailyPayoutCap:          60_000,
		Stages: []stageDTO{{
			Stage:               " normal ",
			MinRecharge7DCoins:  7_000,
			MinRecharge30DCoins: 30_000,
			Tiers:               []tierDTO{{Multiplier: 1, ProbabilityPercent: 100, Enabled: true}},
		}},
	})

	if protoConfig.GetStrategyVersion() != "dynamic_v3" || protoConfig.GetProfitRatePpm() != 10_000 || protoConfig.GetAnchorRatePpm() != 10_000 {
		t.Fatalf("strategy/fund split proto mismatch: %+v", protoConfig)
	}
	if protoConfig.GetInitialPoolCoins() != 0 {
		t.Fatalf("dynamic_v3 legacy initial pool was forwarded: %d", protoConfig.GetInitialPoolCoins())
	}
	if protoConfig.GetLowWaterNonzeroFactorPpm() != 700_000 || protoConfig.GetHighWaterNonzeroFactorPpm() != 1_300_000 || protoConfig.GetRechargeBoostFactorPpm() != 1_100_000 {
		t.Fatalf("dynamic factors lost precision in admin mapping: %+v", protoConfig)
	}
	if got := protoConfig.GetJackpotMultiplierPpms(); len(got) != 3 || got[0] != 200_000_000 || got[2] != 1_000_000_000 {
		t.Fatalf("jackpot multiplier ppms=%v, want 200x/500x/1000x", got)
	}
	stage := protoConfig.GetStages()[0]
	if stage.GetMinRecharge_7DCoins() != 7_000 || stage.GetMinRecharge_30DCoins() != 30_000 {
		t.Fatalf("stage recharge thresholds were not mapped: %+v", stage)
	}

	dto := configFromProto(protoConfig)
	if dto.StrategyVersion != "dynamic_v3" || dto.ProfitRatePercent != 1 || dto.AnchorRatePercent != 1 {
		t.Fatalf("strategy/fund split DTO mismatch: %+v", dto)
	}
	if dto.InitialPoolCoins != 0 {
		t.Fatalf("dynamic_v3 DTO exposed legacy initial pool: %d", dto.InitialPoolCoins)
	}
	if dto.LowWaterNonzeroFactorPercent != 70 || dto.HighWaterNonzeroFactorPercent != 130 || dto.RechargeBoostFactorPercent != 110 {
		t.Fatalf("dynamic factor DTO mismatch: %+v", dto)
	}
	if len(dto.JackpotMultipliers) != 3 || dto.JackpotMultipliers[1] != 500 || dto.JackpotUser72hRTPMaxPercent != 96 {
		t.Fatalf("jackpot DTO mismatch: %+v", dto)
	}
	if dto.MaxSinglePayout != 10_000 || dto.AnchorDailyPayoutCap != 60_000 || dto.Stages[0].MinRecharge30DCoins != 30_000 {
		t.Fatalf("risk/stage DTO mismatch: %+v", dto)
	}
}
