<?php

declare(strict_types=1);

namespace Tests\Unit\Framework\RateLimit;

use App\Framework\RateLimit\RateLimiter;
use App\Framework\RateLimit\RateLimitResult;
use App\Framework\RateLimit\Storage\StorageInterface;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
use App\Framework\RateLimit\TokenBucket;

describe('Token Bucket Rate Limiter', function () {
    beforeEach(function () {
        $this->currentTime = 1000;

        // Create test time provider
        $this->timeProvider = new class($this->currentTime) implements TimeProviderInterface {
            public function __construct(private int &$time) {}

            public function getCurrentTime(): int
            {
                return $this->time;
            }
        };

        // Create in-memory storage
        $this->storage = new class implements StorageInterface {
            private array $requests = [];
            private array $buckets = [];
            private array $baselines = [];

            public function getRequestsInWindow(string $key, int $windowStart, int $windowEnd): array
            {
                if (!isset($this->requests[$key])) {
                    return [];
                }

                return array_filter(
                    $this->requests[$key],
                    fn($timestamp) => $timestamp >= $windowStart && $timestamp <= $windowEnd
                );
            }

            public function addRequest(string $key, int $timestamp, int $ttl): void
            {
                if (!isset($this->requests[$key])) {
                    $this->requests[$key] = [];
                }
                $this->requests[$key][] = $timestamp;
            }

            public function getTokenBucket(string $key): ?TokenBucket
            {
                return $this->buckets[$key] ?? null;
            }

            public function saveTokenBucket(string $key, TokenBucket $bucket): void
            {
                $this->buckets[$key] = $bucket;
            }

            public function clear(string $key): void
            {
                unset($this->requests[$key]);
                unset($this->buckets[$key]);
                unset($this->baselines[$key]);
            }

            public function getBaseline(string $key): ?array
            {
                return $this->baselines[$key] ?? null;
            }

            public function updateBaseline(string $key, int $rate): void
            {
                if (!isset($this->baselines[$key])) {
                    $this->baselines[$key] = [];
                }
                $this->baselines[$key][] = $rate;
            }
        };

        $this->rateLimiter = new RateLimiter($this->storage, $this->timeProvider);
    });

    describe('TokenBucket Value Object', function () {
        it('creates token bucket with capacity and tokens', function () {
            $bucket = new TokenBucket(
                capacity: 100,
                tokens: 75,
                lastRefill: 1000
            );

            expect($bucket->capacity)->toBe(100);
            expect($bucket->tokens)->toBe(75);
            expect($bucket->lastRefill)->toBe(1000);
        });

        it('checks if can consume tokens', function () {
            $bucket = new TokenBucket(100, 75, 1000);

            expect($bucket->canConsume(50))->toBeTrue();
            expect($bucket->canConsume(75))->toBeTrue();
            expect($bucket->canConsume(76))->toBeFalse();
            expect($bucket->canConsume(100))->toBeFalse();
        });

        it('consumes tokens and returns new bucket', function () {
            $bucket = new TokenBucket(100, 75, 1000);
            $newBucket = $bucket->consume(25);

            // Original bucket unchanged (immutable)
            expect($bucket->tokens)->toBe(75);

            // New bucket has reduced tokens
            expect($newBucket->tokens)->toBe(50);
            expect($newBucket->capacity)->toBe(100);
            expect($newBucket->lastRefill)->toBe(1000);
        });

        it('throws when consuming more tokens than available', function () {
            $bucket = new TokenBucket(100, 50, 1000);

            expect(fn() => $bucket->consume(51))
                ->toThrow(\InvalidArgumentException::class, 'Not enough tokens available');
        });

        it('refills tokens up to capacity', function () {
            $bucket = new TokenBucket(100, 25, 1000);
            $refilled = $bucket->refill(50, 1050);

            // Tokens refilled but capped at capacity
            expect($refilled->tokens)->toBe(75);
            expect($refilled->lastRefill)->toBe(1050);
        });

        it('caps refill at capacity', function () {
            $bucket = new TokenBucket(100, 80, 1000);
            $refilled = $bucket->refill(50, 1050);

            // Should cap at 100, not go to 130
            expect($refilled->tokens)->toBe(100);
            expect($refilled->capacity)->toBe(100);
        });

        it('allows zero token consumption', function () {
            $bucket = new TokenBucket(100, 50, 1000);
            $newBucket = $bucket->consume(0);

            expect($newBucket->tokens)->toBe(50);
        });

        it('handles full bucket consumption', function () {
            $bucket = new TokenBucket(100, 100, 1000);
            $empty = $bucket->consume(100);

            expect($empty->tokens)->toBe(0);
            expect($empty->canConsume(1))->toBeFalse();
        });
    });

    describe('RateLimiter Token Bucket Integration', function () {
        it('allows request when tokens available', function () {
            $result = $this->rateLimiter->checkTokenBucket(
                key: 'user:123',
                capacity: 10,
                refillRate: 1,
                tokens: 1
            );

            expect($result->isAllowed())->toBeTrue();
            expect($result->getLimit())->toBe(10);
            expect($result->getCurrent())->toBe(9); // 10 - 1 consumed
        });

        it('blocks request when tokens exhausted', function () {
            // Consume all tokens
            for ($i = 0; $i < 10; $i++) {
                $this->rateLimiter->checkTokenBucket('user:123', 10, 1, 1);
            }

            // Next request should be blocked
            $result = $this->rateLimiter->checkTokenBucket('user:123', 10, 1, 1);

            expect($result->isAllowed())->toBeFalse();
            expect($result->isExceeded())->toBeTrue();
            expect($result->getCurrent())->toBe(0);
            expect($result->getRetryAfter())->toBeGreaterThan(0);
        });

        it('refills tokens over time', function () {
            // Consume 5 tokens
            $this->rateLimiter->checkTokenBucket('user:456', 10, 1, 5);

            // Advance time by 3 seconds (refill rate: 1 token/second)
            $this->currentTime += 3;

            // Should have refilled 3 tokens (5 consumed - 3 refilled = 2 consumed)
            $result = $this->rateLimiter->checkTokenBucket('user:456', 10, 1, 0);

            expect($result->isAllowed())->toBeTrue();
            expect($result->getCurrent())->toBe(8); // 10 capacity - 2 net consumed
        });

        it('calculates correct retry after when tokens exhausted', function () {
            // Exhaust all tokens
            $this->rateLimiter->checkTokenBucket('user:789', 10, 2, 10);

            // Try to consume 5 more tokens
            $result = $this->rateLimiter->checkTokenBucket('user:789', 10, 2, 5);

            // Need 5 tokens, refill rate is 2 tokens/second
            // intval(5/2) = 2 seconds (not ceil, but floor/truncate)
            expect($result->getRetryAfter())->toBe(2);
        });

        it('allows consuming multiple tokens at once', function () {
            $result = $this->rateLimiter->checkTokenBucket(
                key: 'batch:user',
                capacity: 100,
                refillRate: 10,
                tokens: 25
            );

            expect($result->isAllowed())->toBeTrue();
            expect($result->getCurrent())->toBe(75); // 100 - 25
        });

        it('handles concurrent requests correctly', function () {
            // First request consumes 3 tokens
            $result1 = $this->rateLimiter->checkTokenBucket('user:concurrent', 10, 1, 3);
            expect($result1->isAllowed())->toBeTrue();

            // Second request (same time) consumes 4 tokens
            $result2 = $this->rateLimiter->checkTokenBucket('user:concurrent', 10, 1, 4);
            expect($result2->isAllowed())->toBeTrue();
            expect($result2->getCurrent())->toBe(3); // 10 - 3 - 4 = 3
        });

        it('creates new bucket for first request', function () {
            $result = $this->rateLimiter->checkTokenBucket('new:user', 50, 5, 1);

            // First request should have full capacity minus consumed
            expect($result->isAllowed())->toBeTrue();
            expect($result->getCurrent())->toBe(49); // 50 - 1
        });

        it('respects capacity limit during refill', function () {
            // Start with partial tokens
            $this->rateLimiter->checkTokenBucket('user:refill', 10, 1, 8);

            // Advance time significantly (should refill to capacity, not beyond)
            $this->currentTime += 100;

            $result = $this->rateLimiter->checkTokenBucket('user:refill', 10, 1, 1);

            // Should have capacity minus current consumption, not more
            expect($result->getCurrent())->toBe(9); // Capped at 10, then -1
        });

        it('handles zero refill rate edge case', function () {
            // Consume 5 tokens
            $this->rateLimiter->checkTokenBucket('user:zero', 10, 0, 5);

            // Advance time
            $this->currentTime += 10;

            // With refill rate 0, no tokens should refill
            $result = $this->rateLimiter->checkTokenBucket('user:zero', 10, 0, 0);

            expect($result->getCurrent())->toBe(5); // Still only 5 tokens
        });

        it('calculates remaining requests correctly', function () {
            $this->rateLimiter->checkTokenBucket('user:remaining', 20, 2, 7);

            $result = $this->rateLimiter->checkTokenBucket('user:remaining', 20, 2, 0);

            expect($result->getRemainingRequests())->toBe(7); // 20 - 13 consumed
        });
    });

    describe('RateLimitResult', function () {
        it('creates allowed result', function () {
            $result = RateLimitResult::allowed(100, 75);

            expect($result->isAllowed())->toBeTrue();
            expect($result->isExceeded())->toBeFalse();
            expect($result->getLimit())->toBe(100);
            expect($result->getCurrent())->toBe(75);
            expect($result->getRemainingRequests())->toBe(25);
            expect($result->getRetryAfter())->toBeNull();
        });

        it('creates exceeded result', function () {
            $result = RateLimitResult::exceeded(100, 0, 60);

            expect($result->isAllowed())->toBeFalse();
            expect($result->isExceeded())->toBeTrue();
            expect($result->getLimit())->toBe(100);
            expect($result->getCurrent())->toBe(0);
            expect($result->getRemainingRequests())->toBe(100);
            expect($result->getRetryAfter())->toBe(60);
        });

        it('handles zero remaining requests', function () {
            $result = RateLimitResult::allowed(10, 10);

            expect($result->getRemainingRequests())->toBe(0);
        });

        it('prevents negative remaining requests', function () {
            $result = RateLimitResult::exceeded(10, 15, 60);

            // Should never return negative
            expect($result->getRemainingRequests())->toBe(0);
        });
    });

    describe('Edge Cases and Stress Testing', function () {
        it('handles rapid successive requests', function () {
            $key = 'stress:rapid';
            $allowed = 0;
            $blocked = 0;

            // Make 100 rapid requests
            for ($i = 0; $i < 100; $i++) {
                $result = $this->rateLimiter->checkTokenBucket($key, 50, 10, 1);
                if ($result->isAllowed()) {
                    $allowed++;
                } else {
                    $blocked++;
                }
            }

            // First 50 should be allowed, rest blocked
            expect($allowed)->toBe(50);
            expect($blocked)->toBe(50);
        });

        it('handles burst then recovery pattern', function () {
            $key = 'burst:recovery';

            // Burst: consume all tokens
            $this->rateLimiter->checkTokenBucket($key, 20, 5, 20);

            // Immediate request: should fail
            $result1 = $this->rateLimiter->checkTokenBucket($key, 20, 5, 1);
            expect($result1->isAllowed())->toBeFalse();

            // Wait for refill (4 seconds = 20 tokens at rate 5/sec)
            $this->currentTime += 4;

            // Should now succeed
            $result2 = $this->rateLimiter->checkTokenBucket($key, 20, 5, 1);
            expect($result2->isAllowed())->toBeTrue();
        });

        it('handles very large capacity buckets', function () {
            $result = $this->rateLimiter->checkTokenBucket(
                'large:bucket',
                capacity: 1000000,
                refillRate: 10000,
                tokens: 50000
            );

            expect($result->isAllowed())->toBeTrue();
            expect($result->getCurrent())->toBe(950000);
        });

        it('handles very small refill rates', function () {
            // Rate of 1 token/sec (slow but not zero to avoid division by zero)
            $this->rateLimiter->checkTokenBucket('slow:refill', 10, 1, 10);

            // Advance 5 seconds (should refill 5 tokens)
            $this->currentTime += 5;

            $result = $this->rateLimiter->checkTokenBucket('slow:refill', 10, 1, 0);

            // Should have 5 tokens after refill
            expect($result->getCurrent())->toBe(5);
        });

        it('maintains bucket state across multiple operations', function () {
            $key = 'stateful:bucket';

            // Operation 1: consume 3
            $r1 = $this->rateLimiter->checkTokenBucket($key, 10, 1, 3);
            expect($r1->getCurrent())->toBe(7);

            // Operation 2: consume 2 (no time passed)
            $r2 = $this->rateLimiter->checkTokenBucket($key, 10, 1, 2);
            expect($r2->getCurrent())->toBe(5);

            // Advance time: refill 3 tokens
            $this->currentTime += 3;

            // Operation 3: consume 1 (should have 5 + 3 - 1 = 7)
            $r3 = $this->rateLimiter->checkTokenBucket($key, 10, 1, 1);
            expect($r3->getCurrent())->toBe(7);
        });
    });

    describe('Different User Isolation', function () {
        it('maintains separate buckets for different users', function () {
            // User A consumes tokens
            $this->rateLimiter->checkTokenBucket('user:A', 10, 1, 8);

            // User B should have full bucket
            $resultB = $this->rateLimiter->checkTokenBucket('user:B', 10, 1, 1);
            expect($resultB->getCurrent())->toBe(9);

            // User A should still have reduced tokens
            $resultA = $this->rateLimiter->checkTokenBucket('user:A', 10, 1, 0);
            expect($resultA->getCurrent())->toBe(2);
        });

        it('allows same capacity but different refill rates per user', function () {
            // Fast refill user
            $this->rateLimiter->checkTokenBucket('user:fast', 10, 10, 10);
            $this->currentTime += 1;
            $fastResult = $this->rateLimiter->checkTokenBucket('user:fast', 10, 10, 0);
            expect($fastResult->getCurrent())->toBe(10); // Fully refilled in 1 second

            // Slow refill user
            $this->rateLimiter->checkTokenBucket('user:slow', 10, 1, 10);
            $this->currentTime += 1;
            $slowResult = $this->rateLimiter->checkTokenBucket('user:slow', 10, 1, 0);
            expect($slowResult->getCurrent())->toBe(1); // Only 1 token refilled
        });
    });

    describe('Reset Functionality', function () {
        it('resets bucket to initial state', function () {
            $key = 'user:reset';

            // Consume some tokens
            $this->rateLimiter->checkTokenBucket($key, 10, 1, 7);

            // Reset
            $this->rateLimiter->reset($key);

            // Should have full capacity again
            $result = $this->rateLimiter->checkTokenBucket($key, 10, 1, 1);
            expect($result->getCurrent())->toBe(9); // Full bucket minus current consumption
        });
    });
});
