package server

import (
	"context"
	"fmt"
	"net/http"
	"regexp"
	"slices"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
	"github.com/perfect-panel/server/internal/model/node"
	"github.com/perfect-panel/server/internal/model/subscribe"
	"github.com/perfect-panel/server/internal/model/user"
	"github.com/perfect-panel/server/internal/svc"
	"github.com/perfect-panel/server/pkg/logger"
	"github.com/perfect-panel/server/pkg/speedlimit"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// TestNormalizeServerUserListProtocol 验证客户端 hysteria2 兼容字段被映射回
// DB 规范名 "hysteria"；其他协议必须原样返回，不可被误改。
// 缺这一步会导致 hysteria2 永远查不到节点、永远走兜底，HIF-1 表象。
func TestNormalizeServerUserListProtocol(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want string
	}{
		{"hysteria2 maps to hysteria", "hysteria2", "hysteria"},
		{"hysteria unchanged", "hysteria", "hysteria"},
		{"trojan unchanged", "trojan", "trojan"},
		{"vless unchanged", "vless", "vless"},
		{"vmess unchanged", "vmess", "vmess"},
		{"tuic unchanged", "tuic", "tuic"},
		{"shadowsocks unchanged", "shadowsocks", "shadowsocks"},
		{"anytls unchanged", "anytls", "anytls"},
		{"empty unchanged", "", ""},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := normalizeServerUserListProtocol(c.in)
			if got != c.want {
				t.Errorf("normalizeServerUserListProtocol(%q) = %q, want %q", c.in, got, c.want)
			}
		})
	}
}

// TestServerUserListCacheKey_ProtocolIsolation 验证缓存 key 在 server_id 相同、
// protocol 不同（如 trojan vs tuic）时一定不同——这是 HIF-1 的根因防回归。
// 若 key 重叠，tuic 的兜底假用户会污染 trojan 的真实用户列表。
func TestServerUserListCacheKey_ProtocolIsolation(t *testing.T) {
	const serverId int64 = 42
	build := func(protocol string) string {
		return fmt.Sprintf("%s%d:%s", node.ServerUserListCacheKey, serverId, normalizeServerUserListProtocol(protocol))
	}

	protocols := []string{"trojan", "vless", "vmess", "tuic", "shadowsocks", "anytls"}
	seen := make(map[string]string, len(protocols))
	for _, p := range protocols {
		key := build(p)
		if other, exists := seen[key]; exists {
			t.Fatalf("cache key collision: %q (%q) == %q (%q)", p, key, other, key)
		}
		seen[key] = p
	}

	// hysteria 与 hysteria2 必须落到同一 key，否则 hysteria2 客户端拿不到 hysteria 节点列表
	hysteriaKey := build("hysteria")
	hysteria2Key := build("hysteria2")
	if hysteriaKey != hysteria2Key {
		t.Errorf("hysteria/hysteria2 expected to share cache key, got %q vs %q", hysteriaKey, hysteria2Key)
	}
}

// TestServerUserListCacheKey_ShapeMatchesDelPath 验证 GetServerUserList 写入的
// cache key 形态与 ServerUserListCacheKeysForServer（Del 路径）枚举出的 key
// 形态完全一致。形态一旦漂移，Del 会清不到刚写入的 key，HIF-1 重现。
func TestServerUserListCacheKey_ShapeMatchesDelPath(t *testing.T) {
	const serverId int64 = 7
	writeShape := fmt.Sprintf("%s%d:%s", node.ServerUserListCacheKey, serverId, normalizeServerUserListProtocol("trojan"))

	delKeys := node.ServerUserListCacheKeysForServer(serverId)
	if !slices.Contains(delKeys, writeShape) {
		t.Fatalf("write-path key %q not present in Del-path enumeration %v", writeShape, delKeys)
	}
}

func TestServerUserSpeedLimitInputs(t *testing.T) {
	planTrafficLimit := `[{"stat_type":"day","stat_value":1,"traffic_usage":10,"speed_limit":5}]`
	userTrafficLimit := `[{"stat_type":"day","stat_value":1,"traffic_usage":1,"speed_limit":3}]`

	cases := []struct {
		name             string
		sub              *subscribe.Subscribe
		userSub          *user.Subscribe
		wantSpeedLimit   int64
		wantTrafficLimit string
	}{
		{
			name: "未设置用户覆盖时返回套餐基础限速",
			sub: &subscribe.Subscribe{
				SpeedLimit:   100,
				TrafficLimit: planTrafficLimit,
			},
			userSub:          &user.Subscribe{},
			wantSpeedLimit:   100,
			wantTrafficLimit: planTrafficLimit,
		},
		{
			name: "用户 speed_limit 覆盖套餐基础限速",
			sub: &subscribe.Subscribe{
				SpeedLimit:   100,
				TrafficLimit: planTrafficLimit,
			},
			userSub: &user.Subscribe{
				SpeedLimit: 30,
			},
			wantSpeedLimit:   30,
			wantTrafficLimit: planTrafficLimit,
		},
		{
			name: "用户 traffic_limit 覆盖套餐规则",
			sub: &subscribe.Subscribe{
				SpeedLimit:   100,
				TrafficLimit: planTrafficLimit,
			},
			userSub: &user.Subscribe{
				TrafficLimit: &userTrafficLimit,
			},
			wantSpeedLimit:   100,
			wantTrafficLimit: userTrafficLimit,
		},
		{
			name: "用户 traffic_limit 为空时保留套餐规则",
			sub: &subscribe.Subscribe{
				SpeedLimit:   100,
				TrafficLimit: planTrafficLimit,
			},
			userSub: &user.Subscribe{
				TrafficLimit: ptrString(""),
			},
			wantSpeedLimit:   100,
			wantTrafficLimit: planTrafficLimit,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			gotSpeedLimit, gotTrafficLimit := serverUserSpeedLimitInputs(tc.sub, tc.userSub)
			if gotSpeedLimit != tc.wantSpeedLimit {
				t.Fatalf("speedLimit = %d, want %d", gotSpeedLimit, tc.wantSpeedLimit)
			}
			if gotTrafficLimit != tc.wantTrafficLimit {
				t.Fatalf("trafficLimit = %q, want %q", gotTrafficLimit, tc.wantTrafficLimit)
			}
		})
	}
}

func TestCalculateServerUserSpeedLimits_BatchTrafficRules(t *testing.T) {
	db, mock, cleanup := newServerUserListTestDB(t)
	defer cleanup()

	ctx, _ := gin.CreateTestContext(nil)
	ctx.Request = httptestNewRequest()
	logic := &GetServerUserListLogic{
		Logger: logger.WithContext(ctx.Request.Context()),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}

	trafficLimit := `[{"stat_type":"day","stat_value":1,"traffic_usage":10,"speed_limit":5}]`
	candidates := map[int64]serverUserSpeedLimitCandidate{
		101: {userID: 1001, userSubscribeID: 101, baseSpeed: 0, trafficLimit: trafficLimit},
		102: {userID: 1002, userSubscribeID: 102, baseSpeed: 0, trafficLimit: trafficLimit},
		103: {userID: 1003, userSubscribeID: 103, baseSpeed: 20, trafficLimit: trafficLimit},
	}

	mock.ExpectQuery("FROM `traffic_log`").
		WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()).
		WillReturnRows(sqlmock.NewRows([]string{"user_id", "user_subscribe_id", "upload", "download"}).
			AddRow(1001, 101, gb(4), gb(5)).
			AddRow(1002, 102, gb(4), gb(6)).
			AddRow(1003, 103, gb(12), int64(0)))

	got := logic.calculateServerUserSpeedLimits(candidates)
	assertSpeedLimit(t, got, 101, 0)
	assertSpeedLimit(t, got, 102, 5)
	assertSpeedLimit(t, got, 103, 5)
	assertServerUserListExpectations(t, mock)
}

func TestCalculateServerUserSpeedLimits_UserTrafficLimitOverride(t *testing.T) {
	db, mock, cleanup := newServerUserListTestDB(t)
	defer cleanup()

	ctx, _ := gin.CreateTestContext(nil)
	ctx.Request = httptestNewRequest()
	logic := &GetServerUserListLogic{
		Logger: logger.WithContext(ctx.Request.Context()),
		ctx:    ctx,
		svcCtx: &svc.ServiceContext{DB: db},
	}

	candidates := map[int64]serverUserSpeedLimitCandidate{
		201: {
			userID:          2001,
			userSubscribeID: 201,
			baseSpeed:       50,
			trafficLimit:    `[{"stat_type":"day","stat_value":1,"traffic_usage":2,"speed_limit":3}]`,
		},
		202: {
			userID:          2002,
			userSubscribeID: 202,
			baseSpeed:       50,
			trafficLimit:    "",
		},
	}

	mock.ExpectQuery("FROM `traffic_log`").
		WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg()).
		WillReturnRows(sqlmock.NewRows([]string{"user_id", "user_subscribe_id", "upload", "download"}).
			AddRow(2001, 201, gb(2), int64(0)))

	got := logic.calculateServerUserSpeedLimits(candidates)
	assertSpeedLimit(t, got, 201, 3)
	assertSpeedLimit(t, got, 202, 50)
	assertServerUserListExpectations(t, mock)
}

func TestServerUserTrafficRuleWindow(t *testing.T) {
	now := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)

	cases := []struct {
		name string
		rule speedlimit.TrafficLimitRule
		want time.Time
		ok   bool
	}{
		{
			name: "hour window",
			rule: speedlimit.TrafficLimitRule{StatType: "hour", StatValue: 2},
			want: now.Add(-2 * time.Hour),
			ok:   true,
		},
		{
			name: "day window",
			rule: speedlimit.TrafficLimitRule{StatType: "day", StatValue: 1},
			want: now.AddDate(0, 0, -1),
			ok:   true,
		},
		{
			name: "invalid stat type",
			rule: speedlimit.TrafficLimitRule{StatType: "week", StatValue: 1},
			ok:   false,
		},
		{
			name: "invalid stat value",
			rule: speedlimit.TrafficLimitRule{StatType: "day", StatValue: 0},
			ok:   false,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, ok := serverUserTrafficRuleWindow(tc.rule, now)
			if ok != tc.ok {
				t.Fatalf("ok = %v, want %v", ok, tc.ok)
			}
			if !ok {
				return
			}
			if !got.start.Equal(tc.want) || !got.end.Equal(now) {
				t.Fatalf("window = [%s, %s), want [%s, %s)", got.start, got.end, tc.want, now)
			}
		})
	}
}

func newServerUserListTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
	t.Helper()

	sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
		if matched, _ := regexp.MatchString(expectedSQL, actualSQL); matched {
			return nil
		}
		t.Fatalf("unexpected SQL\nexpected pattern: %s\nactual: %s", expectedSQL, actualSQL)
		return nil
	})))
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}

	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn:                      sqlDB,
		SkipInitializeWithVersion: true,
	}), &gorm.Config{})
	if err != nil {
		_ = sqlDB.Close()
		t.Fatalf("open gorm db: %v", err)
	}

	return db, mock, func() {
		_ = sqlDB.Close()
	}
}

func assertServerUserListExpectations(t *testing.T, mock sqlmock.Sqlmock) {
	t.Helper()

	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func assertSpeedLimit(t *testing.T, got map[int64]int64, userSubscribeID int64, want int64) {
	t.Helper()

	if got[userSubscribeID] != want {
		t.Fatalf("speed limit for user_subscribe_id=%d = %d, want %d", userSubscribeID, got[userSubscribeID], want)
	}
}

func gb(value int64) int64 {
	return value * 1024 * 1024 * 1024
}

func ptrString(value string) *string {
	return &value
}

func httptestNewRequest() *http.Request {
	req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/v1/server/user", nil)
	return req
}
