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"
)

type experimentClientStub struct {
	luckygiftadmin.Client
	createRequest    *luckygiftv1.CreateLuckyGiftExperimentRequest
	createResponse   *luckygiftv1.CreateLuckyGiftExperimentResponse
	updateRequest    *luckygiftv1.UpdateLuckyGiftExperimentRequest
	updateResponse   *luckygiftv1.UpdateLuckyGiftExperimentResponse
	overridesRequest *luckygiftv1.SetLuckyGiftExperimentOverridesRequest
	endRequest       *luckygiftv1.EndLuckyGiftExperimentRequest
	endResponse      *luckygiftv1.EndLuckyGiftExperimentResponse
	err              error
}

func (s *experimentClientStub) CreateLuckyGiftExperiment(_ context.Context, request *luckygiftv1.CreateLuckyGiftExperimentRequest) (*luckygiftv1.CreateLuckyGiftExperimentResponse, error) {
	s.createRequest = request
	return s.createResponse, s.err
}

func (s *experimentClientStub) UpdateLuckyGiftExperiment(_ context.Context, request *luckygiftv1.UpdateLuckyGiftExperimentRequest) (*luckygiftv1.UpdateLuckyGiftExperimentResponse, error) {
	s.updateRequest = request
	return s.updateResponse, s.err
}

func (s *experimentClientStub) SetLuckyGiftExperimentOverrides(_ context.Context, request *luckygiftv1.SetLuckyGiftExperimentOverridesRequest) (*luckygiftv1.SetLuckyGiftExperimentOverridesResponse, error) {
	s.overridesRequest = request
	return &luckygiftv1.SetLuckyGiftExperimentOverridesResponse{
		Experiment: &luckygiftv1.LuckyGiftExperiment{ExperimentId: request.GetExperimentId(), Overrides: request.GetUpserts()},
	}, s.err
}

func (s *experimentClientStub) EndLuckyGiftExperiment(_ context.Context, request *luckygiftv1.EndLuckyGiftExperimentRequest) (*luckygiftv1.EndLuckyGiftExperimentResponse, error) {
	s.endRequest = request
	return s.endResponse, s.err
}

func experimentTestContext(t *testing.T, method, target, body string) (*gin.Context, *httptest.ResponseRecorder) {
	t.Helper()
	recorder := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(recorder)
	c.Request = httptest.NewRequest(method, target, bytes.NewBufferString(body))
	c.Request.Header.Set("Content-Type", "application/json")
	c.Set(middleware.ContextUserID, uint(7))
	c.Set(middleware.ContextRequestID, "request-1")
	return c, recorder
}

func TestCreateLuckyGiftExperimentReusesConfigNormalizationAndPPM(t *testing.T) {
	gin.SetMode(gin.TestMode)
	client := &experimentClientStub{createResponse: &luckygiftv1.CreateLuckyGiftExperimentResponse{
		Experiment: &luckygiftv1.LuckyGiftExperiment{
			AppCode: "lalu", PoolId: "super_lucky", ExperimentId: "lucky_exp_1", Name: "rtp push",
			ControlRuleVersion: 9, TreatmentRuleVersion: 10, TreatmentTrafficPpm: 50_000, Status: "running",
			Overrides: []*luckygiftv1.LuckyGiftExperimentOverride{{UserKey: "1001", Arm: "treatment"}},
			ArmStats:  []*luckygiftv1.LuckyGiftExperimentArmStats{{Arm: "control", RuleVersion: 9, ActualRtpPpm: 955_000}},
		},
		TreatmentConfig: &luckygiftv1.LuckyGiftRuleConfig{AppCode: "lalu", PoolId: "super_lucky", RuleVersion: 10, TargetRtpPpm: 960_000},
	}}
	body := `{
		"name": " rtp push ",
		"treatment_traffic_percent": 5,
		"overrides": [{"user_key": " 1001 ", "arm": " TREATMENT "}],
		"config": {
			"pool_id": "super_lucky",
			"strategy_version": "DYNAMIC_V3",
			"enabled": true,
			"target_rtp_percent": 96,
			"initial_pool_coins": 1000000,
			"stages": [{"stage": "normal", "tiers": [{"multiplier": 0.5, "probability_percent": 22, "enabled": true}]}]
		}
	}`
	c, recorder := experimentTestContext(t, http.MethodPost, "/admin/ops-center/lucky-gifts/experiments?app_code=LALU", body)
	New(client, time.Second, nil).CreateLuckyGiftExperiment(c)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	request := client.createRequest
	if request == nil {
		t.Fatal("owner create request was not sent")
	}
	if request.GetName() != "rtp push" || request.GetTreatmentTrafficPpm() != 50_000 || request.GetOperatorAdminId() != 7 {
		t.Fatalf("owner request=%+v", request)
	}
	if request.GetMeta().GetAppCode() != "lalu" || request.GetMeta().GetRequestId() != "request-1" {
		t.Fatalf("owner request meta=%+v", request.GetMeta())
	}
	if len(request.GetOverrides()) != 1 || request.GetOverrides()[0].GetUserKey() != "1001" || request.GetOverrides()[0].GetArm() != "treatment" {
		t.Fatalf("owner request overrides=%+v", request.GetOverrides())
	}
	config := request.GetTreatmentConfig()
	if config.GetAppCode() != "lalu" || config.GetStrategyVersion() != "dynamic_v3" {
		t.Fatalf("treatment config app/strategy=%+v", config)
	}
	if config.GetTargetRtpPpm() != 960_000 {
		t.Fatalf("treatment config target rtp ppm=%d, want 960000", config.GetTargetRtpPpm())
	}
	if config.GetInitialPoolCoins() != 0 {
		t.Fatalf("dynamic_v3 initial pool coins should be forced to 0, got %d", config.GetInitialPoolCoins())
	}
	tier := config.GetStages()[0].GetTiers()[0]
	if tier.GetTierId() != "normal_0_5x" || tier.GetBaseWeightPpm() != 220_000 {
		t.Fatalf("treatment config tier=%+v", tier)
	}
	var envelope struct {
		Code int `json:"code"`
		Data struct {
			Experiment struct {
				ExperimentID            string  `json:"experiment_id"`
				TreatmentTrafficPercent float64 `json:"treatment_traffic_percent"`
				ArmStats                []struct {
					Arm              string  `json:"arm"`
					ActualRTPPercent float64 `json:"actual_rtp_percent"`
				} `json:"arm_stats"`
			} `json:"experiment"`
			TreatmentConfig struct {
				RuleVersion      int64   `json:"rule_version"`
				TargetRTPPercent float64 `json:"target_rtp_percent"`
			} `json:"treatment_config"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != 0 || envelope.Data.Experiment.ExperimentID != "lucky_exp_1" || envelope.Data.Experiment.TreatmentTrafficPercent != 5 {
		t.Fatalf("response=%s", recorder.Body.String())
	}
	if len(envelope.Data.Experiment.ArmStats) != 1 || envelope.Data.Experiment.ArmStats[0].ActualRTPPercent != 95.5 {
		t.Fatalf("arm stats response=%s", recorder.Body.String())
	}
	if envelope.Data.TreatmentConfig.RuleVersion != 10 || envelope.Data.TreatmentConfig.TargetRTPPercent != 96 {
		t.Fatalf("treatment config response=%s", recorder.Body.String())
	}
}

func TestUpdateLuckyGiftExperimentOmittedFieldsKeepOwnerDefaults(t *testing.T) {
	gin.SetMode(gin.TestMode)
	// 缺省流量以 proto3 optional 的"字段不存在"表达（wantTraffic=nil），显式 0 必须以存在的 0 传递；
	// 这是防止"仅改状态的调用把放量静默清零"的契约核心。
	tests := []struct {
		name        string
		body        string
		wantTraffic *int64
		wantStatus  string
	}{
		{name: "omitted fields", body: `{}`, wantTraffic: nil, wantStatus: ""},
		{name: "explicit zero traffic", body: `{"treatment_traffic_percent": 0}`, wantTraffic: int64Ptr(0), wantStatus: ""},
		{name: "pause only", body: `{"status": "paused"}`, wantTraffic: nil, wantStatus: "paused"},
		{name: "both fields", body: `{"treatment_traffic_percent": 12.5, "status": "running"}`, wantTraffic: int64Ptr(125_000), wantStatus: "running"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			client := &experimentClientStub{updateResponse: &luckygiftv1.UpdateLuckyGiftExperimentResponse{
				Experiment: &luckygiftv1.LuckyGiftExperiment{ExperimentId: "lucky_exp_1", Status: "running"},
			}}
			c, recorder := experimentTestContext(t, http.MethodPatch, "/admin/ops-center/lucky-gifts/experiments/lucky_exp_1?app_code=lalu", tt.body)
			c.Params = gin.Params{{Key: "experimentId", Value: "lucky_exp_1"}}
			New(client, time.Second, nil).UpdateLuckyGiftExperiment(c)
			if recorder.Code != http.StatusOK {
				t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
			}
			request := client.updateRequest
			if request == nil || request.GetExperimentId() != "lucky_exp_1" || request.GetOperatorAdminId() != 7 {
				t.Fatalf("owner request=%+v", request)
			}
			if (request.TreatmentTrafficPpm == nil) != (tt.wantTraffic == nil) ||
				(tt.wantTraffic != nil && request.GetTreatmentTrafficPpm() != *tt.wantTraffic) ||
				request.GetStatus() != tt.wantStatus {
				t.Fatalf("traffic=%v status=%q, want traffic=%v status=%q", request.TreatmentTrafficPpm, request.GetStatus(), tt.wantTraffic, tt.wantStatus)
			}
		})
	}
}

func int64Ptr(v int64) *int64 { return &v }

func TestSetLuckyGiftExperimentOverridesForwardsUpsertsAndRemoves(t *testing.T) {
	gin.SetMode(gin.TestMode)
	client := &experimentClientStub{}
	body := `{"upserts": [{"user_key": "ext:abc", "arm": "control"}], "removes": [" 1001 "]}`
	c, recorder := experimentTestContext(t, http.MethodPut, "/admin/ops-center/lucky-gifts/experiments/lucky_exp_1/overrides?app_code=lalu", body)
	c.Params = gin.Params{{Key: "experimentId", Value: "lucky_exp_1"}}
	New(client, time.Second, nil).SetLuckyGiftExperimentOverrides(c)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	request := client.overridesRequest
	if request == nil || request.GetExperimentId() != "lucky_exp_1" || request.GetOperatorAdminId() != 7 {
		t.Fatalf("owner request=%+v", request)
	}
	if len(request.GetUpserts()) != 1 || request.GetUpserts()[0].GetUserKey() != "ext:abc" || request.GetUpserts()[0].GetArm() != "control" {
		t.Fatalf("owner upserts=%+v", request.GetUpserts())
	}
	if len(request.GetRemoves()) != 1 || request.GetRemoves()[0] != "1001" {
		t.Fatalf("owner removes=%+v", request.GetRemoves())
	}
	var envelope struct {
		Data struct {
			Experiment struct {
				ExperimentID string `json:"experiment_id"`
			} `json:"experiment"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Data.Experiment.ExperimentID != "lucky_exp_1" {
		t.Fatalf("response=%s", recorder.Body.String())
	}
}

func TestEndLuckyGiftExperimentReturnsExperimentAndFinalConfig(t *testing.T) {
	gin.SetMode(gin.TestMode)
	client := &experimentClientStub{endResponse: &luckygiftv1.EndLuckyGiftExperimentResponse{
		Experiment: &luckygiftv1.LuckyGiftExperiment{
			ExperimentId: "lucky_exp_1", Status: "completed", WinnerArm: "treatment",
			WinnerRuleVersion: 10, FinalRuleVersion: 11,
		},
		FinalConfig: &luckygiftv1.LuckyGiftRuleConfig{PoolId: "super_lucky", RuleVersion: 11, TargetRtpPpm: 960_000},
	}}
	c, recorder := experimentTestContext(t, http.MethodPost, "/admin/ops-center/lucky-gifts/experiments/lucky_exp_1/end?app_code=lalu", `{"winner_arm": "Treatment"}`)
	c.Params = gin.Params{{Key: "experimentId", Value: "lucky_exp_1"}}
	New(client, time.Second, nil).EndLuckyGiftExperiment(c)
	if recorder.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.endRequest == nil || client.endRequest.GetWinnerArm() != "treatment" || client.endRequest.GetOperatorAdminId() != 7 {
		t.Fatalf("owner request=%+v", client.endRequest)
	}
	var envelope struct {
		Code int `json:"code"`
		Data struct {
			Experiment struct {
				Status           string `json:"status"`
				WinnerArm        string `json:"winner_arm"`
				FinalRuleVersion int64  `json:"final_rule_version"`
			} `json:"experiment"`
			FinalConfig struct {
				RuleVersion      int64   `json:"rule_version"`
				TargetRTPPercent float64 `json:"target_rtp_percent"`
			} `json:"final_config"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != 0 || envelope.Data.Experiment.Status != "completed" || envelope.Data.Experiment.WinnerArm != "treatment" || envelope.Data.Experiment.FinalRuleVersion != 11 {
		t.Fatalf("experiment response=%s", recorder.Body.String())
	}
	if envelope.Data.FinalConfig.RuleVersion != 11 || envelope.Data.FinalConfig.TargetRTPPercent != 96 {
		t.Fatalf("final config response=%s", recorder.Body.String())
	}
}

func TestListLuckyGiftExperimentsRequiresAppCode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, recorder := experimentTestContext(t, http.MethodGet, "/admin/ops-center/lucky-gifts/experiments", "")
	New(&experimentClientStub{}, time.Second, nil).ListLuckyGiftExperiments(c)
	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status=%d want=400 body=%s", recorder.Code, recorder.Body.String())
	}
}
