package main

import (
	"testing"
	"time"
)

// TestRateLimiter_AllowsUpToCapAndBlocksRest verifies the WO-018 contract:
// 10 calls from the same IP succeed; the 11th is denied.
func TestRateLimiter_AllowsUpToCapAndBlocksRest(t *testing.T) {
	rl := NewRateLimiter(10)

	for i := 1; i <= 10; i++ {
		if !rl.Allow("203.0.113.1") {
			t.Fatalf("call %d unexpectedly denied", i)
		}
	}
	if rl.Allow("203.0.113.1") {
		t.Fatalf("11th call must be denied")
	}
}

// TestRateLimiter_IsolatesByIP confirms that buckets are keyed per-IP.
func TestRateLimiter_IsolatesByIP(t *testing.T) {
	rl := NewRateLimiter(2)
	for i := range 2 {
		if !rl.Allow("a") {
			t.Fatalf("call %d from 'a' should succeed", i+1)
		}
	}
	if rl.Allow("a") {
		t.Fatalf("third call from 'a' should be denied")
	}
	if !rl.Allow("b") {
		t.Fatalf("first call from 'b' should succeed even after 'a' is full")
	}
}

// TestRateLimiter_WindowResetsAfterHour verifies the fixed-window semantics:
// once a bucket's resetsAt is in the past, the next Allow() call starts a
// fresh window with count=1. This guards against a regression where the
// limiter forgets to clear/reset stale buckets and locks out legitimate
// traffic indefinitely.
//
// We exercise this by mutating the bucket's resetsAt directly to a time in
// the past — equivalent to fast-forwarding the clock without waiting an hour.
// The helper accesses the unexported buckets map; this is safe because
// _test.go files compile inside the same package.
func TestRateLimiter_WindowResetsAfterHour(t *testing.T) {
	rl := NewRateLimiter(3)
	const ip = "198.51.100.7"

	// Fill the bucket.
	for i := range 3 {
		if !rl.Allow(ip) {
			t.Fatalf("call %d should succeed", i+1)
		}
	}
	if rl.Allow(ip) {
		t.Fatalf("4th call should be denied (bucket full)")
	}

	// Fast-forward the clock by pushing resetsAt into the past.
	rl.mu.Lock()
	b, ok := rl.buckets[ip]
	if !ok {
		rl.mu.Unlock()
		t.Fatalf("bucket missing for ip %s after filling", ip)
	}
	b.resetsAt = time.Now().Add(-time.Hour - time.Second)
	rl.mu.Unlock()

	// Next call must reset the window and succeed.
	if !rl.Allow(ip) {
		t.Fatalf("Allow() must reset the bucket after window expiry — got false")
	}

	// Bucket count must be 1 (fresh window), not 4.
	rl.mu.Lock()
	got := rl.buckets[ip].count
	rl.mu.Unlock()
	if got != 1 {
		t.Errorf("bucket count after reset: got %d, want 1 (fresh window)", got)
	}
}
