package services

import (
	"testing"
	"time"

	"github.com/google/uuid"
)

// allowInviteTestService builds a bare InviteService that exercises the
// rate-limiter without touching SQL or SMTP. The DB and mail fields stay
// nil — only the counter logic is under test here.
func allowInviteTestService() *InviteService {
	return &InviteService{
		allowedDomains: func() []string { return []string{"hlc.com"} },
		sentBy:         map[uuid.UUID][]time.Time{},
		clock:          func() time.Time { return time.Now() },
	}
}

// Fills the slot up to the cap, then asserts the next call is refused.
func TestInviteRateLimit_WithinWindow(t *testing.T) {
	s := allowInviteTestService()
	uid := uuid.New()
	for i := 0; i < InviteRateLimit; i++ {
		if !s.allowInvite(uid) {
			t.Fatalf("slot %d should succeed", i)
		}
	}
	if s.allowInvite(uid) {
		t.Fatal("slot beyond cap should be denied")
	}
}

// After the window rolls over, old stamps are dropped and the user is
// allowed to send again. This is the main invariant behind the 24h cap.
func TestInviteRateLimit_WindowRollover(t *testing.T) {
	s := allowInviteTestService()
	uid := uuid.New()

	now := time.Date(2026, 4, 20, 10, 0, 0, 0, time.UTC)
	s.clock = func() time.Time { return now }

	for i := 0; i < InviteRateLimit; i++ {
		if !s.allowInvite(uid) {
			t.Fatalf("slot %d should succeed at t=now", i)
		}
	}
	if s.allowInvite(uid) {
		t.Fatal("11th slot should be denied at t=now")
	}

	// Jump well past the window — all stamps should age out.
	s.clock = func() time.Time { return now.Add(InviteRateWindow + time.Minute) }
	if !s.allowInvite(uid) {
		t.Fatal("slot after rollover should succeed")
	}
	if got := s.RemainingToday(uid); got != InviteRateLimit-1 {
		t.Errorf("RemainingToday after rollover = %d, want %d", got, InviteRateLimit-1)
	}
}

// Concurrent senders must not be able to double-spend the last slot — the
// limiter holds the mutex across the check-and-insert.
func TestInviteRateLimit_Concurrent(t *testing.T) {
	s := allowInviteTestService()
	uid := uuid.New()

	done := make(chan bool, InviteRateLimit*2)
	for i := 0; i < InviteRateLimit*2; i++ {
		go func() { done <- s.allowInvite(uid) }()
	}
	granted := 0
	for i := 0; i < InviteRateLimit*2; i++ {
		if <-done {
			granted++
		}
	}
	if granted != InviteRateLimit {
		t.Errorf("granted=%d, want %d", granted, InviteRateLimit)
	}
}

// domainAllowed must match case-insensitively and fail-closed when the
// whitelist is empty.
func TestDomainAllowed(t *testing.T) {
	s := &InviteService{allowedDomains: func() []string { return []string{"hlc.com", "HLC.de"} }}

	allow := map[string]bool{
		"alice@hlc.com":     true,
		"alice@HLC.COM":     true,
		"alice@hlc.de":      true,
		"alice@hlc.com.evil.ru": false,
		"alice@example.org": false,
		"malformed":         false,
	}
	for addr, want := range allow {
		if got := s.domainAllowed(addr); got != want {
			t.Errorf("domainAllowed(%q) = %v, want %v", addr, got, want)
		}
	}

	// Empty whitelist = deny everything.
	s.allowedDomains = func() []string { return nil }
	if s.domainAllowed("alice@hlc.com") {
		t.Error("empty whitelist should deny")
	}
}
