<?php

declare(strict_types=1);

namespace Tests\Security\AuthenticationTests;

use Tests\Security\SecurityTestCase;

/**
 * Brute Force Protection tests
 *
 * Tests rate limiting, account lockout, and brute force attack prevention
 */
final readonly class BruteForceProtectionTest extends SecurityTestCase
{
    /**
     * Test rate limiting on login attempts
     */
    public function testEnforcesRateLimitOnLoginAttempts(): void
    {
        $ipAddress = '203.0.113.42';
        $maxAttempts = 5;
        $windowSeconds = 300;  // 5 minutes

        // Simulate failed login attempts
        $attempts = [];
        for ($i = 0; $i < 10; $i++) {
            $attempts[] = [
                'ip' => $ipAddress,
                'timestamp' => time() - ($i * 30),  // 30 seconds apart
                'success' => false
            ];
        }

        $isRateLimited = $this->checkRateLimit(
            ipAddress: $ipAddress,
            attempts: $attempts,
            maxAttempts: $maxAttempts,
            windowSeconds: $windowSeconds
        );

        if (!$isRateLimited) {
            throw new \RuntimeException(
                'Rate limiting not enforced after ' . count($attempts) . ' attempts'
            );
        }

        echo "✅ Rate limiting enforced ({$maxAttempts} attempts per {$windowSeconds}s)\n";
    }

    /**
     * Test account lockout after failed attempts
     */
    public function testLocksAccountAfterFailedAttempts(): void
    {
        $username = 'testuser@example.com';
        $maxFailedAttempts = 5;
        $lockoutDurationSeconds = 900;  // 15 minutes

        // Simulate failed attempts
        $failedAttempts = [];
        for ($i = 0; $i < 6; $i++) {
            $failedAttempts[] = [
                'username' => $username,
                'timestamp' => time() - ($i * 10),
                'success' => false
            ];
        }

        $lockoutInfo = $this->checkAccountLockout(
            username: $username,
            attempts: $failedAttempts,
            maxFailedAttempts: $maxFailedAttempts,
            lockoutDuration: $lockoutDurationSeconds
        );

        if (!$lockoutInfo['is_locked']) {
            throw new \RuntimeException(
                'Account not locked after ' . count($failedAttempts) . ' failed attempts'
            );
        }

        echo "✅ Account locked after {$maxFailedAttempts} failed attempts\n";
        echo "   Lockout duration: {$lockoutDurationSeconds}s\n";
    }

    /**
     * Test progressive delay on failed attempts
     */
    public function testImplementsProgressiveDelay(): void
    {
        $attempts = [1, 2, 3, 4, 5];
        $delays = [];

        foreach ($attempts as $attemptNumber) {
            $delay = $this->calculateProgressiveDelay($attemptNumber);
            $delays[$attemptNumber] = $delay;
        }

        // Verify delay increases with each attempt
        for ($i = 1; $i < count($delays); $i++) {
            if ($delays[$i] <= $delays[$i - 1]) {
                throw new \RuntimeException(
                    "Progressive delay not increasing: attempt {$i} has delay {$delays[$i]}s, " .
                    "previous was {$delays[$i-1]}s"
                );
            }
        }

        echo "✅ Progressive delay implemented:\n";
        foreach ($delays as $attempt => $delay) {
            echo "   Attempt {$attempt}: {$delay}s delay\n";
        }
    }

    /**
     * Test CAPTCHA requirement after suspicious activity
     */
    public function testRequiresCaptchaAfterSuspiciousActivity(): void
    {
        $ipAddress = '203.0.113.42';
        $failedAttempts = 3;

        $requiresCaptcha = $this->shouldRequireCaptcha(
            ipAddress: $ipAddress,
            failedAttempts: $failedAttempts,
            captchaThreshold: 3
        );

        if (!$requiresCaptcha) {
            throw new \RuntimeException(
                'CAPTCHA not required after ' . $failedAttempts . ' failed attempts'
            );
        }

        echo "✅ CAPTCHA required after {$failedAttempts} failed attempts\n";
    }

    /**
     * Test distributed brute force detection
     */
    public function testDetectsDistributedBruteForce(): void
    {
        $username = 'admin@example.com';

        // Simulate attempts from multiple IPs
        $attempts = [
            ['ip' => '203.0.113.1', 'username' => $username, 'timestamp' => time() - 100],
            ['ip' => '203.0.113.2', 'username' => $username, 'timestamp' => time() - 90],
            ['ip' => '203.0.113.3', 'username' => $username, 'timestamp' => time() - 80],
            ['ip' => '203.0.113.4', 'username' => $username, 'timestamp' => time() - 70],
            ['ip' => '203.0.113.5', 'username' => $username, 'timestamp' => time() - 60],
            ['ip' => '203.0.113.6', 'username' => $username, 'timestamp' => time() - 50],
        ];

        $isDistributedAttack = $this->detectDistributedBruteForce(
            attempts: $attempts,
            username: $username,
            windowSeconds: 300,
            uniqueIpThreshold: 5
        );

        if (!$isDistributedAttack) {
            throw new \RuntimeException(
                'Distributed brute force attack not detected (' . count($attempts) . ' attempts from different IPs)'
            );
        }

        echo "✅ Distributed brute force attack detected\n";
        echo "   " . count($attempts) . " attempts from " . count(array_unique(array_column($attempts, 'ip'))) . " unique IPs\n";
    }

    /**
     * Test password spray attack detection
     */
    public function testDetectsPasswordSprayAttack(): void
    {
        // Simulate password spray: same password tried against multiple accounts
        $attempts = [
            ['username' => 'user1@example.com', 'password' => 'Password123!', 'timestamp' => time() - 100],
            ['username' => 'user2@example.com', 'password' => 'Password123!', 'timestamp' => time() - 90],
            ['username' => 'user3@example.com', 'password' => 'Password123!', 'timestamp' => time() - 80],
            ['username' => 'user4@example.com', 'password' => 'Password123!', 'timestamp' => time() - 70],
            ['username' => 'user5@example.com', 'password' => 'Password123!', 'timestamp' => time() - 60],
        ];

        $isPasswordSpray = $this->detectPasswordSpray(
            attempts: $attempts,
            windowSeconds: 300,
            uniqueUsernameThreshold: 5
        );

        if (!$isPasswordSpray) {
            throw new \RuntimeException(
                'Password spray attack not detected (' . count($attempts) . ' attempts with same password)'
            );
        }

        echo "✅ Password spray attack detected\n";
        echo "   Same password tried against " . count($attempts) . " different accounts\n";
    }

    /**
     * Test successful login resets attempt counter
     */
    public function testSuccessfulLoginResetsAttemptCounter(): void
    {
        $username = 'testuser@example.com';

        // Failed attempts
        $attempts = [
            ['username' => $username, 'success' => false, 'timestamp' => time() - 200],
            ['username' => $username, 'success' => false, 'timestamp' => time() - 150],
            ['username' => $username, 'success' => false, 'timestamp' => time() - 100],
            // Successful login
            ['username' => $username, 'success' => true, 'timestamp' => time() - 50],
        ];

        $failedCount = $this->getFailedAttemptCount($username, $attempts);

        if ($failedCount !== 0) {
            throw new \RuntimeException(
                'Failed attempt counter not reset after successful login (count: ' . $failedCount . ')'
            );
        }

        echo "✅ Failed attempt counter reset after successful login\n";
    }

    /**
     * Run all brute force protection tests
     */
    public function runAllTests(): array
    {
        $results = [];

        try {
            $this->testEnforcesRateLimitOnLoginAttempts();
            $results['rate_limiting'] = 'PASS';
        } catch (\Exception $e) {
            $results['rate_limiting'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testLocksAccountAfterFailedAttempts();
            $results['account_lockout'] = 'PASS';
        } catch (\Exception $e) {
            $results['account_lockout'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testImplementsProgressiveDelay();
            $results['progressive_delay'] = 'PASS';
        } catch (\Exception $e) {
            $results['progressive_delay'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testRequiresCaptchaAfterSuspiciousActivity();
            $results['captcha_requirement'] = 'PASS';
        } catch (\Exception $e) {
            $results['captcha_requirement'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testDetectsDistributedBruteForce();
            $results['distributed_attack'] = 'PASS';
        } catch (\Exception $e) {
            $results['distributed_attack'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testDetectsPasswordSprayAttack();
            $results['password_spray'] = 'PASS';
        } catch (\Exception $e) {
            $results['password_spray'] = 'FAIL: ' . $e->getMessage();
        }

        try {
            $this->testSuccessfulLoginResetsAttemptCounter();
            $results['counter_reset'] = 'PASS';
        } catch (\Exception $e) {
            $results['counter_reset'] = 'FAIL: ' . $e->getMessage();
        }

        return $results;
    }

    private function checkRateLimit(
        string $ipAddress,
        array $attempts,
        int $maxAttempts,
        int $windowSeconds
    ): bool {
        $recentAttempts = array_filter($attempts, function ($attempt) use ($ipAddress, $windowSeconds) {
            return $attempt['ip'] === $ipAddress
                && (time() - $attempt['timestamp']) <= $windowSeconds
                && !$attempt['success'];
        });

        return count($recentAttempts) >= $maxAttempts;
    }

    private function checkAccountLockout(
        string $username,
        array $attempts,
        int $maxFailedAttempts,
        int $lockoutDuration
    ): array {
        $failedAttempts = array_filter($attempts, function ($attempt) use ($username) {
            return $attempt['username'] === $username && !$attempt['success'];
        });

        $isLocked = count($failedAttempts) >= $maxFailedAttempts;

        return [
            'is_locked' => $isLocked,
            'failed_attempts' => count($failedAttempts),
            'unlock_at' => $isLocked ? time() + $lockoutDuration : null
        ];
    }

    private function calculateProgressiveDelay(int $attemptNumber): int
    {
        // Exponential backoff: 1s, 2s, 4s, 8s, 16s
        return min(2 ** ($attemptNumber - 1), 16);
    }

    private function shouldRequireCaptcha(
        string $ipAddress,
        int $failedAttempts,
        int $captchaThreshold
    ): bool {
        return $failedAttempts >= $captchaThreshold;
    }

    private function detectDistributedBruteForce(
        array $attempts,
        string $username,
        int $windowSeconds,
        int $uniqueIpThreshold
    ): bool {
        $recentAttempts = array_filter($attempts, function ($attempt) use ($username, $windowSeconds) {
            return $attempt['username'] === $username
                && (time() - $attempt['timestamp']) <= $windowSeconds;
        });

        $uniqueIps = array_unique(array_column($recentAttempts, 'ip'));

        return count($uniqueIps) >= $uniqueIpThreshold;
    }

    private function detectPasswordSpray(
        array $attempts,
        int $windowSeconds,
        int $uniqueUsernameThreshold
    ): bool {
        $recentAttempts = array_filter($attempts, function ($attempt) use ($windowSeconds) {
            return (time() - $attempt['timestamp']) <= $windowSeconds;
        });

        // Group by password
        $passwordGroups = [];
        foreach ($recentAttempts as $attempt) {
            $password = $attempt['password'];
            if (!isset($passwordGroups[$password])) {
                $passwordGroups[$password] = [];
            }
            $passwordGroups[$password][] = $attempt['username'];
        }

        // Check if any password was tried against multiple usernames
        foreach ($passwordGroups as $password => $usernames) {
            $uniqueUsernames = array_unique($usernames);
            if (count($uniqueUsernames) >= $uniqueUsernameThreshold) {
                return true;
            }
        }

        return false;
    }

    private function getFailedAttemptCount(string $username, array $attempts): int
    {
        // Find last successful login
        $lastSuccessIndex = null;
        foreach ($attempts as $index => $attempt) {
            if ($attempt['username'] === $username && $attempt['success']) {
                $lastSuccessIndex = $index;
            }
        }

        // Count failed attempts after last successful login
        if ($lastSuccessIndex === null) {
            // No successful login, count all failed attempts
            return count(array_filter($attempts, fn($a) => $a['username'] === $username && !$a['success']));
        }

        // Count failed attempts after last successful login
        $failedCount = 0;
        for ($i = $lastSuccessIndex + 1; $i < count($attempts); $i++) {
            if ($attempts[$i]['username'] === $username && !$attempts[$i]['success']) {
                $failedCount++;
            }
        }

        return $failedCount;
    }
}
