package middleware

import (
	"context"
	"errors"
	"sync"
	"testing"
	"time"

	"security-center-backend/internal/model"
	"security-center-backend/internal/repository"
)

// fakeAPIKeyRepo 记录 TouchLastUsed 调用次数, 其余方法空实现。
type fakeAPIKeyRepo struct {
	mu       sync.Mutex
	touches  int
	touchErr error
}

func (f *fakeAPIKeyRepo) TouchLastUsed(_ context.Context, _ uint64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.touches++
	return f.touchErr
}

func (f *fakeAPIKeyRepo) touchCount() int {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.touches
}

func (f *fakeAPIKeyRepo) Create(_ context.Context, _ *model.APIKey) error { return nil }

func (f *fakeAPIKeyRepo) FindByHash(_ context.Context, _ string) (*model.APIKey, error) {
	return nil, nil
}

func (f *fakeAPIKeyRepo) FindByIDForOwner(_ context.Context, _, _ uint64) (*model.APIKey, error) {
	return nil, nil
}

func (f *fakeAPIKeyRepo) ListByOwner(_ context.Context, _ uint64) ([]*model.APIKey, error) {
	return nil, nil
}

func (f *fakeAPIKeyRepo) Revoke(_ context.Context, _, _ uint64) error { return nil }

func (f *fakeAPIKeyRepo) FindByID(_ context.Context, _ uint64) (*model.APIKey, error) {
	return nil, nil
}

func (f *fakeAPIKeyRepo) ListAll(_ context.Context, _ repository.ListAPIKeysFilter) ([]*model.APIKey, error) {
	return nil, nil
}

// resetTouchState 清空包级降频状态, 并在测试结束后恢复真实时钟。
func resetTouchState(t *testing.T) {
	t.Helper()
	touchLastUsedMu.Lock()
	touchLastUsedSeen = make(map[uint64]time.Time)
	touchLastUsedMu.Unlock()
	t.Cleanup(func() {
		now = time.Now
		touchLastUsedMu.Lock()
		touchLastUsedSeen = make(map[uint64]time.Time)
		touchLastUsedMu.Unlock()
	})
}

// waitForTouches 轮询等待异步 goroutine 累计写满 want 次。
func waitForTouches(t *testing.T, repo *fakeAPIKeyRepo, want int) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if repo.touchCount() >= want {
			return
		}
		time.Sleep(5 * time.Millisecond)
	}
	t.Fatalf("expected %d TouchLastUsed calls, got %d", want, repo.touchCount())
}

func TestTouchLastUsedAsyncFirstCallWrites(t *testing.T) {
	resetTouchState(t)
	repo := &fakeAPIKeyRepo{}

	touchLastUsedAsync(repo, 42)

	waitForTouches(t, repo, 1)
}

func TestTouchLastUsedAsyncRateLimitedWithinWindow(t *testing.T) {
	resetTouchState(t)
	repo := &fakeAPIKeyRepo{}

	for i := 0; i < 5; i++ {
		touchLastUsedAsync(repo, 42)
	}

	waitForTouches(t, repo, 1)
	// 等一小段时间确认没有额外 goroutine 写入。
	time.Sleep(50 * time.Millisecond)
	if got := repo.touchCount(); got != 1 {
		t.Fatalf("expected 1 TouchLastUsed call within 60s window, got %d", got)
	}
}

func TestTouchLastUsedAsyncWritesAgainAfterWindow(t *testing.T) {
	resetTouchState(t)
	repo := &fakeAPIKeyRepo{}

	base := time.Now()
	now = func() time.Time { return base }
	touchLastUsedAsync(repo, 42)
	waitForTouches(t, repo, 1)

	// 假时钟推进 61s, 窗口过期后应再写一次。
	now = func() time.Time { return base.Add(61 * time.Second) }
	touchLastUsedAsync(repo, 42)
	waitForTouches(t, repo, 2)
}

func TestTouchLastUsedAsyncRepoErrorDoesNotPanic(t *testing.T) {
	resetTouchState(t)
	repo := &fakeAPIKeyRepo{touchErr: errors.New("db down")}

	touchLastUsedAsync(repo, 42)

	waitForTouches(t, repo, 1)
	// 给 goroutine 一点时间跑完错误分支, 确认无 panic。
	time.Sleep(20 * time.Millisecond)
}
