package luckygift_test

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

	"hyapp-luck-gateway/internal/config"
	"hyapp-luck-gateway/internal/integration/luckygiftclient"
	"hyapp-luck-gateway/internal/modules/luckygift"
	"hyapp-luck-gateway/internal/router"
)

type fakeClient struct {
	got   luckygiftclient.SendCommand
	calls int
}

func (f *fakeClient) SendLuckyGift(_ context.Context, cmd luckygiftclient.SendCommand) (luckygiftclient.SendResult, error) {
	f.calls++
	f.got = cmd
	return luckygiftclient.SendResult{DrawID: "draw-1", RequestID: cmd.RequestID, AppCode: cmd.AppCode, RewardStatus: "granted"}, nil
}

func TestSendAcceptsAllowedAppAndComputesAmount(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan,yumi")
	body := []byte(`{"app_code":"Aslan","request_id":"req-1","external_user_id":"u-1001","device_id":"device-attested-by-aslan","paid_at_ms":1700000000000,"gift_count":3,"unit_amount":7,"currency":"COIN","metadata":{"gift":"rose"}}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if client.calls != 1 {
		t.Fatalf("client calls=%d", client.calls)
	}
	if client.got.AppCode != "aslan" || client.got.RequestID != "req-1" || client.got.DeviceID != "device-attested-by-aslan" || client.got.PaidAtMS != 1700000000000 || client.got.TotalAmount != 21 || client.got.GiftCount != 3 {
		t.Fatalf("unexpected command: %+v", client.got)
	}
}

func TestSendKeepsMissingDeviceCompatibleForFixedV2OwnerDecision(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	// luck-gateway 不读规则版本，因此旧 fixed_v2 调用方可继续缺省 device_id；
	// 如果本 App 已发布 dynamic_v3，lucky-gift owner 会在规则事务内明确拒绝。
	body := []byte(`{"app_code":"aslan","request_id":"req-fixed-legacy","external_user_id":"u-legacy","gift_count":1,"unit_amount":7}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK || client.calls != 1 || client.got.DeviceID != "" {
		t.Fatalf("fixed-compatible request status=%d calls=%d command=%+v body=%s", rec.Code, client.calls, client.got, rec.Body.String())
	}
}

func TestSendRejectsPoolOverride(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	body := []byte(`{"app_code":"aslan","request_id":"req-1","external_user_id":"u-1001","pool_id":"pool-1","paid_at_ms":1700000000000,"gift_count":1,"unit_amount":7}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if client.calls != 0 {
		t.Fatalf("client should not be called, calls=%d", client.calls)
	}
}

func TestSendRejectsNegativePaidAt(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	body := []byte(`{"app_code":"aslan","request_id":"req-negative-paid-at","external_user_id":"u-1001","paid_at_ms":-1,"gift_count":1,"unit_amount":7}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest || client.calls != 0 {
		t.Fatalf("negative paid_at_ms status=%d calls=%d body=%s", rec.Code, client.calls, rec.Body.String())
	}
}

func TestSendRejectsDisallowedAppBeforeClientCall(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan,yumi")
	body := []byte(`{"app_code":"hyapp","request_id":"req-1","external_user_id":"u-1001","gift_count":1,"unit_amount":9}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusForbidden {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if client.calls != 0 {
		t.Fatalf("client should not be called, calls=%d", client.calls)
	}
}

func TestSendRejectsInvalidAmount(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	body, _ := json.Marshal(map[string]any{
		"app_code":         "aslan",
		"request_id":       "req-1",
		"external_user_id": "u-1",
		"gift_count":       0,
		"unit_amount":      10,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if client.calls != 0 {
		t.Fatalf("client should not be called, calls=%d", client.calls)
	}
}

func TestSendRejectsMoreThan999SequentialDraws(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	body := []byte(`{"app_code":"aslan","request_id":"req-1","external_user_id":"u-1","gift_count":1000,"unit_amount":1}`)
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest || client.calls != 0 {
		t.Fatalf("oversized sequential draw status=%d calls=%d body=%s", rec.Code, client.calls, rec.Body.String())
	}
}

func TestSendRejectsTooLongExternalFields(t *testing.T) {
	client := &fakeClient{}
	handler := testRouter(client, "aslan")
	body, _ := json.Marshal(map[string]any{
		"app_code":         "aslan",
		"request_id":       "req-1",
		"external_user_id": strings.Repeat("u", 129),
		"gift_count":       1,
		"unit_amount":      10,
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/lucky-gifts/send", bytes.NewReader(body))
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	if client.calls != 0 {
		t.Fatalf("client should not be called, calls=%d", client.calls)
	}
}

func testRouter(client *fakeClient, allowedApps string) http.Handler {
	cfg := config.Config{AllowedApps: parseAllowedAppsForTest(allowedApps)}
	return router.New(cfg, router.Handlers{LuckyGift: luckygift.New(client, time.Second, cfg.AllowedApps)})
}

func parseAllowedAppsForTest(raw string) map[string]struct{} {
	out := map[string]struct{}{}
	for _, app := range strings.Split(raw, ",") {
		app = strings.ToLower(strings.TrimSpace(app))
		if app != "" {
			out[app] = struct{}{}
		}
	}
	return out
}
