package service

import (
	"fmt"
	"strings"
	"testing"
	"time"
)

// TestGuardKeyNormalization 用户名去空白并按字节截断,高基数超长输入收敛。
func TestGuardKeyNormalization(t *testing.T) {
	tests := []struct {
		name string
		ip   string
		user string
		want string
	}{
		{name: "常规", ip: "1.2.3.4", user: "admin", want: "1.2.3.4|admin"},
		{name: "去空白", ip: "1.2.3.4", user: "  admin  ", want: "1.2.3.4|admin"},
		{name: "超长截断", ip: "1.2.3.4", user: strings.Repeat("a", 100), want: "1.2.3.4|" + strings.Repeat("a", guardUserMax)},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := guardKey(tt.ip, tt.user); got != tt.want {
				t.Errorf("guardKey = %q, want %q", got, tt.want)
			}
		})
	}
}

// TestGuardCapacityBounded 高基数唯一 key 持续失败,条目数不超过上限。
func TestGuardCapacityBounded(t *testing.T) {
	g := newLoginGuard()
	now := time.Now()
	for i := 0; i < maxGuardEntries*2; i++ {
		g.fail(fmt.Sprintf("1.2.3.4|user-%d", i), now, 100, time.Hour)
	}
	if n := len(g.failures); n > maxGuardEntries {
		t.Errorf("failures 条目 = %d, want ≤ %d", n, maxGuardEntries)
	}
}

// TestGuardCapacitySweepExpired 容量满时优先清理窗口外条目,而非驱逐活跃条目。
func TestGuardCapacitySweepExpired(t *testing.T) {
	g := newLoginGuard()
	base := time.Now()
	window := time.Hour
	// 填满:全部为「早已过窗」的旧失败
	for i := 0; i < maxGuardEntries; i++ {
		g.fail(fmt.Sprintf("old|%d", i), base.Add(-2*window), 100, window)
	}
	// 新失败触发清理:过期条目被清,新条目正常记录
	g.fail("fresh|user", base, 100, window)
	if _, ok := g.failures["fresh|user"]; !ok {
		t.Error("新条目未记录")
	}
	if n := len(g.failures); n > maxGuardEntries {
		t.Errorf("清理后条目 = %d, want ≤ %d", n, maxGuardEntries)
	}
}

// TestGuardEvictOldestKeepsNewest 无过期可清时驱逐最旧,最新条目保留。
func TestGuardEvictOldestKeepsNewest(t *testing.T) {
	g := newLoginGuard()
	base := time.Now()
	window := 24 * time.Hour
	for i := 0; i < maxGuardEntries; i++ {
		// 均在窗口内,时间递增;user-0 最旧
		g.fail(fmt.Sprintf("ip|user-%d", i), base.Add(time.Duration(i)*time.Second), 100, window)
	}
	g.fail("ip|newest", base.Add(time.Duration(maxGuardEntries+1)*time.Second), 100, window)
	if _, ok := g.failures["ip|newest"]; !ok {
		t.Error("最新条目未记录")
	}
	if _, ok := g.failures["ip|user-0"]; ok {
		t.Error("最旧条目未被驱逐")
	}
}

// TestGuardLockCapacityBounded 高基数唯一 key 全部打满阈值转入锁定,
// 锁定期内(无可清理的过期锁)条目数仍不超过上限,最新锁保留、最早锁被驱逐。
func TestGuardLockCapacityBounded(t *testing.T) {
	g := newLoginGuard()
	base := time.Now()
	window := 15 * time.Minute
	total := maxGuardEntries + 100
	for i := 0; i < total; i++ {
		key := fmt.Sprintf("1.2.3.4|user-%d", i)
		// 每个 key 打满 2 次(阈值)即转入锁定;时间递增,i 越小锁定越早
		at := base.Add(time.Duration(i) * time.Millisecond)
		g.fail(key, at, 2, window)
		if !g.fail(key, at, 2, window) {
			t.Fatalf("key %s 未按阈值转入锁定", key)
		}
	}
	if n := len(g.lockedAt); n > maxGuardEntries {
		t.Errorf("lockedAt 条目 = %d, want ≤ %d", n, maxGuardEntries)
	}
	if _, ok := g.lockedAt[fmt.Sprintf("1.2.3.4|user-%d", total-1)]; !ok {
		t.Error("最新锁定条目未保留")
	}
	if _, ok := g.lockedAt["1.2.3.4|user-0"]; ok {
		t.Error("最早锁定条目未被驱逐")
	}
}
