package httpapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/gin-gonic/gin"
	"github.com/glebarez/sqlite"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"

	"dudu/server/internal/auth"
	"dudu/server/internal/config"
	"dudu/server/internal/quota"
	"dudu/server/internal/store"
	"dudu/server/pkg/protocol"
)

func newAPI(t *testing.T) (*httptest.Server, *gorm.DB, *redis.Client) {
	t.Helper()
	gin.SetMode(gin.TestMode)
	mr := miniredis.RunT(t)
	rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatal(err)
	}
	if err := db.AutoMigrate(store.AllModels()...); err != nil {
		t.Fatal(err)
	}
	if err := store.SeedPacks(db); err != nil {
		t.Fatal(err)
	}
	cfg := config.Config{} // 全部外部依赖走 mock
	jwt := auth.NewJWT("test-secret", time.Hour, rdb)
	r := gin.New()
	Register(r, &Deps{Cfg: cfg, DB: db, RDB: rdb, JWT: jwt, Quota: quota.New(rdb, db)})
	srv := httptest.NewServer(r)
	t.Cleanup(srv.Close)
	return srv, db, rdb
}

func postJSON(t *testing.T, url, token string, body any) *http.Response {
	t.Helper()
	b, _ := json.Marshal(body)
	req, _ := http.NewRequest("POST", url, bytes.NewReader(b))
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	return resp
}

func getJSON(t *testing.T, url, token string, out any) int {
	t.Helper()
	req, _ := http.NewRequest("GET", url, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	if out != nil {
		_ = json.NewDecoder(resp.Body).Decode(out)
	}
	return resp.StatusCode
}

func login(t *testing.T, srv *httptest.Server, code string) string {
	t.Helper()
	resp := postJSON(t, srv.URL+"/v1/auth/wechat", "", protocol.AuthWechatRequest{Code: code})
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		t.Fatalf("login failed: %d", resp.StatusCode)
	}
	var tr protocol.AuthTokenResponse
	_ = json.NewDecoder(resp.Body).Decode(&tr)
	return tr.Token
}

func TestMobileLoginAndMe(t *testing.T) {
	srv, _, _ := newAPI(t)
	token := login(t, srv, "wangjia99")

	var me protocol.MeResponse
	if code := getJSON(t, srv.URL+"/v1/me", token, &me); code != 200 {
		t.Fatalf("me failed: %d", code)
	}
	if me.AccountState != protocol.AccountTrial || me.BalanceSeconds != 0 {
		t.Fatalf("new user should be trial: %+v", me)
	}
	if !strings.HasSuffix(me.NicknameMasked, "***") {
		t.Fatalf("nickname not masked: %s", me.NicknameMasked)
	}
	// 同 code 再登录 = 同一用户
	token2 := login(t, srv, "wangjia99")
	var me2 protocol.MeResponse
	getJSON(t, srv.URL+"/v1/me", token2, &me2)
	if me2.UserID != me.UserID {
		t.Fatal("same code should map to same user")
	}
	// 未带 token 拒绝
	if code := getJSON(t, srv.URL+"/v1/me", "", nil); code != 401 {
		t.Fatalf("want 401, got %d", code)
	}
}

func TestQrLoginFlow(t *testing.T) {
	srv, _, _ := newAPI(t)
	resp := postJSON(t, srv.URL+"/v1/auth/qr", "", nil)
	var qr protocol.AuthQrResponse
	_ = json.NewDecoder(resp.Body).Decode(&qr)
	resp.Body.Close()
	if qr.State == "" || !strings.Contains(qr.QrURL, qr.State) {
		t.Fatalf("bad qr resp: %+v", qr)
	}
	// pending
	var st protocol.AuthQrStatusResponse
	getJSON(t, srv.URL+"/v1/auth/qr/"+qr.State, "", &st)
	if st.Status != "pending" {
		t.Fatalf("want pending, got %s", st.Status)
	}
	// 手机授权回调
	if code := getJSON(t, srv.URL+"/v1/auth/wechat/callback?code=abc&state="+qr.State, "", nil); code != 200 {
		t.Fatalf("callback failed: %d", code)
	}
	// confirmed + token 可用
	getJSON(t, srv.URL+"/v1/auth/qr/"+qr.State, "", &st)
	if st.Status != "confirmed" || st.Token == "" {
		t.Fatalf("want confirmed with token, got %+v", st)
	}
	if code := getJSON(t, srv.URL+"/v1/me", st.Token, nil); code != 200 {
		t.Fatalf("token unusable: %d", code)
	}
	// 一次性：再次轮询已失效
	getJSON(t, srv.URL+"/v1/auth/qr/"+qr.State, "", &st)
	if st.Status != "expired" {
		t.Fatalf("state should be one-shot, got %s", st.Status)
	}
}

func TestOrderAndPayNotifyIdempotent(t *testing.T) {
	srv, db, _ := newAPI(t)
	token := login(t, srv, "buyer1")

	// packs
	var packs protocol.PacksResponse
	getJSON(t, srv.URL+"/v1/packs", "", &packs)
	if len(packs.Packs) != 3 {
		t.Fatalf("want 3 packs, got %d", len(packs.Packs))
	}

	// 下单 500 分钟
	resp := postJSON(t, srv.URL+"/v1/orders", token, protocol.CreateOrderRequest{PackID: "pack_500", Channel: "native"})
	var order protocol.CreateOrderResponse
	_ = json.NewDecoder(resp.Body).Decode(&order)
	resp.Body.Close()
	if order.OrderID == "" || !strings.Contains(order.CodeURL, order.OrderID) {
		t.Fatalf("bad order resp: %+v", order)
	}

	// 金额不符 → 拒绝入账
	bad := postJSON(t, srv.URL+"/v1/pay/notify", "", map[string]any{
		"order_id": order.OrderID, "transaction_id": "txn-bad", "amount_cents": 100,
	})
	bad.Body.Close()
	if bad.StatusCode != 500 {
		t.Fatalf("amount mismatch should fail, got %d", bad.StatusCode)
	}

	// 正确回调 → paid + 入账 30000s
	notify := map[string]any{"order_id": order.OrderID, "transaction_id": "txn-1", "amount_cents": 3900}
	ok := postJSON(t, srv.URL+"/v1/pay/notify", "", notify)
	ok.Body.Close()
	if ok.StatusCode != 200 {
		t.Fatalf("notify failed: %d", ok.StatusCode)
	}
	var ostat protocol.OrderStatusResponse
	getJSON(t, srv.URL+"/v1/orders/"+order.OrderID, token, &ostat)
	if ostat.Status != "paid" || ostat.BalanceSeconds != 30000 {
		t.Fatalf("want paid 30000s, got %+v", ostat)
	}

	// 重复回调幂等：余额不变
	dup := postJSON(t, srv.URL+"/v1/pay/notify", "", notify)
	dup.Body.Close()
	var me protocol.MeResponse
	getJSON(t, srv.URL+"/v1/me", token, &me)
	if me.BalanceSeconds != 30000 || me.AccountState != protocol.AccountOK {
		t.Fatalf("idempotency broken: %+v", me)
	}
	var n int64
	db.Model(&store.BalanceLedger{}).Where("reason = ?", store.LedgerPurchase).Count(&n)
	if n != 1 {
		t.Fatalf("want 1 purchase ledger, got %d", n)
	}
}

// minimalPNG 合法 PNG 头部的最小字节序列（仅用于 magic bytes 校验）。
var minimalPNG = []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 0, 0, 0, 0}

func postFeedback(t *testing.T, srv *httptest.Server, token, content string, images ...[]byte) *http.Response {
	t.Helper()
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	_ = w.WriteField("content", content)
	_ = w.WriteField("diagnostics", `{"app_version":"0.1.0"}`)
	for i, img := range images {
		fw, _ := w.CreateFormFile("images[]", fmt.Sprintf("s%d.png", i))
		_, _ = fw.Write(img)
	}
	w.Close()
	req, _ := http.NewRequest("POST", srv.URL+"/v1/feedback", &buf)
	req.Header.Set("Content-Type", w.FormDataContentType())
	req.Header.Set("Authorization", "Bearer "+token)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	return resp
}

func TestFeedback(t *testing.T) {
	srv, db, _ := newAPI(t)
	token := login(t, srv, "fbuser")
	t.Cleanup(func() { _ = removeUploads() })

	// 正常提交（1 张合法 PNG）
	resp := postFeedback(t, srv, token, "浮层有时不消失", minimalPNG)
	if resp.StatusCode != 200 {
		t.Fatalf("feedback failed: %d", resp.StatusCode)
	}
	var fr protocol.FeedbackResponse
	_ = json.NewDecoder(resp.Body).Decode(&fr)
	resp.Body.Close()
	var fb store.Feedback
	if err := db.First(&fb, "id = ?", fr.FeedbackID).Error; err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(string(fb.Images), fr.FeedbackID) {
		t.Fatalf("images keys missing: %s", fb.Images)
	}

	// 伪图片拒绝
	resp = postFeedback(t, srv, token, "假图", []byte("not an image"))
	resp.Body.Close()
	if resp.StatusCode != 400 {
		t.Fatalf("fake image should 400, got %d", resp.StatusCode)
	}

	// 空内容拒绝
	resp = postFeedback(t, srv, token, "  ")
	resp.Body.Close()
	if resp.StatusCode != 400 {
		t.Fatalf("empty content should 400, got %d", resp.StatusCode)
	}

	// 限频：已用 3 次（含失败计数），再提交 7 次到第 10 次 ok，第 11 次 429
	for i := 0; i < 7; i++ {
		r := postFeedback(t, srv, token, "继续反馈")
		r.Body.Close()
		if r.StatusCode != 200 {
			t.Fatalf("feedback %d failed: %d", i, r.StatusCode)
		}
	}
	r := postFeedback(t, srv, token, "第 11 条")
	r.Body.Close()
	if r.StatusCode != 429 {
		t.Fatalf("11th should 429, got %d", r.StatusCode)
	}
}

func removeUploads() error { return nil } // LocalStorage 写入 var/uploads，测试容忍残留

func TestMetricsBatchAndAggregate(t *testing.T) {
	srv, db, _ := newAPI(t)
	body := protocol.MetricsBatchRequest{
		DeviceID: "dev-m1", Platform: "mac", AppVersion: "0.1.0",
		Events: []protocol.MetricEvent{
			{Event: "asr.first_partial_ms", Props: map[string]any{"ms": 320}, ClientTs: 1},
			{Event: "asr.first_partial_ms", Props: map[string]any{"ms": 480}, ClientTs: 2},
			{Event: "unknown.event", Props: map[string]any{"x": 1}, ClientTs: 3}, // 白名单外丢弃
		},
	}
	resp := postJSON(t, srv.URL+"/v1/metrics/batch", "", body)
	resp.Body.Close()
	if resp.StatusCode != 204 {
		t.Fatalf("want 204, got %d", resp.StatusCode)
	}
	// 等异步 writer 落库
	var n int64
	for i := 0; i < 60; i++ {
		db.Model(&store.MetricEvent{}).Count(&n)
		if n == 2 {
			break
		}
		time.Sleep(100 * time.Millisecond)
	}
	if n != 2 {
		t.Fatalf("want 2 events stored (whitelist), got %d", n)
	}
}

// 邮箱验证码登录 E2E：发码（60s 冷却）→ 取码登录 → token 可用 → 码一次性。
func TestEmailLogin(t *testing.T) {
	srv, _, rdb := newAPI(t)

	// 发码
	resp := postJSON(t, srv.URL+"/v1/auth/email/code", "", map[string]string{"email": "Dev@Example.com"})
	if resp.StatusCode != http.StatusNoContent {
		t.Fatalf("send code: want 204, got %d", resp.StatusCode)
	}
	// 冷却期内再发 → 429
	resp = postJSON(t, srv.URL+"/v1/auth/email/code", "", map[string]string{"email": "dev@example.com"})
	if resp.StatusCode != http.StatusTooManyRequests {
		t.Fatalf("cooldown: want 429, got %d", resp.StatusCode)
	}

	code, err := rdb.Get(t.Context(), store.KeyAuthEmail("dev@example.com")).Result()
	if err != nil {
		t.Fatal(err)
	}

	// 错码拒绝
	resp = postJSON(t, srv.URL+"/v1/auth/email", "", map[string]string{"email": "dev@example.com", "code": "000000"})
	if code != "000000" && resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("wrong code: want 400, got %d", resp.StatusCode)
	}

	// 正确码登录
	resp = postJSON(t, srv.URL+"/v1/auth/email", "", map[string]string{"email": "DEV@example.com", "code": code})
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("login: want 200, got %d", resp.StatusCode)
	}
	var tok protocol.AuthTokenResponse
	_ = json.NewDecoder(resp.Body).Decode(&tok)
	if tok.Token == "" || tok.User.UserID == "" {
		t.Fatalf("empty token/user: %+v", tok)
	}

	// token 可访问 /v1/me
	req, _ := http.NewRequest("GET", srv.URL+"/v1/me", nil)
	req.Header.Set("Authorization", "Bearer "+tok.Token)
	meResp, err := http.DefaultClient.Do(req)
	if err != nil || meResp.StatusCode != http.StatusOK {
		t.Fatalf("me with email token: %v %d", err, meResp.StatusCode)
	}

	// 码一次性：复用被拒
	resp = postJSON(t, srv.URL+"/v1/auth/email", "", map[string]string{"email": "dev@example.com", "code": code})
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("code reuse: want 400, got %d", resp.StatusCode)
	}
}
