<?php

declare(strict_types=1);

namespace Tests\Security\AuthenticationTests;

use Tests\Security\SecurityTestCase;

/**
 * Token Validation tests
 *
 * Tests JWT/Bearer token validation, expiration, and signature verification
 */
final readonly class TokenValidationTest extends SecurityTestCase
{
    /**
     * Test JWT token structure validation
     */
    public function testValidatesJwtStructure(): void
    {
        $validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

        if (!$this->hasValidJwtStructure($validToken)) {
            throw new \RuntimeException('Valid JWT structure rejected');
        }

        echo "✅ Valid JWT structure accepted\n";

        // Test invalid structures
        $invalidTokens = [
            'invalid',  // Not base64
            'header.payload',  // Missing signature
            'header.payload.signature.extra',  // Too many parts
            '',  // Empty
        ];

        foreach ($invalidTokens as $token) {
            if ($this->hasValidJwtStructure($token)) {
                throw new \RuntimeException("Invalid JWT structure accepted: {$token}");
            }
        }

        echo "✅ Invalid JWT structures rejected\n";
    }

    /**
     * Test token expiration validation
     */
    public function testValidatesTokenExpiration(): void
    {
        // Expired token (exp claim in the past)
        $expiredToken = [
            'sub' => '1234567890',
            'name' => 'John Doe',
            'exp' => time() - 3600  // Expired 1 hour ago
        ];

        if (!$this->isTokenExpired($expiredToken)) {
            throw new \RuntimeException('Expired token not detected');
        }

        echo "✅ Expired token detected\n";

        // Valid token (exp claim in the future)
        $validToken = [
            'sub' => '1234567890',
            'name' => 'John Doe',
            'exp' => time() + 3600  // Expires in 1 hour
        ];

        if ($this->isTokenExpired($validToken)) {
            throw new \RuntimeException('Valid token marked as expired');
        }

        echo "✅ Valid token accepted\n";
    }

    /**
     * Test token signature verification
     */
    public function testVerifiesTokenSignature(): void
    {
        $secret = 'test-secret-key';
        $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
        $payload = base64_encode(json_encode(['sub' => '123', 'exp' => time() + 3600]));

        // Valid signature
        $validSignature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));
        $validToken = "$header.$payload.$validSignature";

        if (!$this->verifyTokenSignature($validToken, $secret)) {
            throw new \RuntimeException('Valid token signature rejected');
        }

        echo "✅ Valid token signature verified\n";

        // Invalid signature
        $invalidSignature = base64_encode('invalid-signature');
        $invalidToken = "$header.$payload.$invalidSignature";

        if ($this->verifyTokenSignature($invalidToken, $secret)) {
            throw new \RuntimeException('Invalid token signature accepted');
        }

        echo "✅ Invalid token signature rejected\n";
    }

    /**
     * Test Bearer token format
     */
    public function testValidatesBearerTokenFormat(): void
    {
        $validBearerTokens = [
            'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature',
            'Bearer valid-api-key-12345',
        ];

        foreach ($validBearerTokens as $token) {
            if (!$this->hasValidBearerFormat($token)) {
                throw new \RuntimeException("Valid Bearer token rejected: {$token}");
            }
        }

        echo "✅ Valid Bearer token formats accepted\n";

        $invalidBearerTokens = [
            'bearer token',  // Lowercase 'bearer'
            'Token xyz',  // Wrong prefix
            'eyJhbGci...',  // Missing 'Bearer' prefix
            '',  // Empty
        ];

        foreach ($invalidBearerTokens as $token) {
            if ($this->hasValidBearerFormat($token)) {
                throw new \RuntimeException("Invalid Bearer token accepted: {$token}");
            }
        }

        echo "✅ Invalid Bearer token formats rejected\n";
    }

    /**
     * Test token claims validation
     */
    public function testValidatesTokenClaims(): void
    {
        // Valid claims
        $validClaims = [
            'sub' => '1234567890',
            'name' => 'John Doe',
            'iat' => time(),
            'exp' => time() + 3600,
            'iss' => 'https://example.com',
            'aud' => 'https://api.example.com'
        ];

        $validationErrors = $this->validateTokenClaims($validClaims);

        if (!empty($validationErrors)) {
            throw new \RuntimeException(
                "Valid claims rejected: " . implode(', ', $validationErrors)
            );
        }

        echo "✅ Valid token claims accepted\n";

        // Missing required claims
        $invalidClaims = [
            'name' => 'John Doe',
            // Missing 'sub' and 'exp'
        ];

        $validationErrors = $this->validateTokenClaims($invalidClaims);

        if (empty($validationErrors)) {
            throw new \RuntimeException('Missing required claims not detected');
        }

        echo "✅ Missing required claims detected\n";
    }

    /**
     * Test token issued at (iat) validation
     */
    public function testValidatesIssuedAtClaim(): void
    {
        // Future iat (token issued in the future)
        $futureIat = [
            'sub' => '123',
            'iat' => time() + 3600,  // Issued 1 hour in the future
            'exp' => time() + 7200
        ];

        if (!$this->isIssuedInFuture($futureIat)) {
            throw new \RuntimeException('Future iat not detected');
        }

        echo "✅ Future iat claim detected\n";

        // Valid iat (issued in the past)
        $validIat = [
            'sub' => '123',
            'iat' => time() - 60,  // Issued 1 minute ago
            'exp' => time() + 3600
        ];

        if ($this->isIssuedInFuture($validIat)) {
            throw new \RuntimeException('Valid iat marked as future');
        }

        echo "✅ Valid iat claim accepted\n";
    }

    /**
     * Test token not before (nbf) validation
     */
    public function testValidatesNotBeforeClaim(): void
    {
        // Token not yet valid (nbf in the future)
        $notYetValid = [
            'sub' => '123',
            'nbf' => time() + 3600,  // Valid in 1 hour
            'exp' => time() + 7200
        ];

        if (!$this->isNotYetValid($notYetValid)) {
            throw new \RuntimeException('Not-yet-valid token not detected');
        }

        echo "✅ Not-yet-valid token detected\n";

        // Token already valid (nbf in the past)
        $alreadyValid = [
            'sub' => '123',
            'nbf' => time() - 60,  // Valid since 1 minute ago
            'exp' => time() + 3600
        ];

        if ($this->isNotYetValid($alreadyValid)) {
            throw new \RuntimeException('Valid token marked as not-yet-valid');
        }

        echo "✅ Valid token accepted (nbf check)\n";
    }

    /**
     * Run all token validation tests
     */
    public function runAllTests(): array
    {
        $results = [];

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

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

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

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

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

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

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

        return $results;
    }

    private function hasValidJwtStructure(string $token): bool
    {
        $parts = explode('.', $token);
        return count($parts) === 3 && !empty($parts[0]) && !empty($parts[1]) && !empty($parts[2]);
    }

    private function isTokenExpired(array $claims): bool
    {
        if (!isset($claims['exp'])) {
            return true;  // No expiration claim = treat as expired
        }

        return time() >= $claims['exp'];
    }

    private function verifyTokenSignature(string $token, string $secret): bool
    {
        $parts = explode('.', $token);
        if (count($parts) !== 3) {
            return false;
        }

        [$header, $payload, $signature] = $parts;

        $validSignature = base64_encode(hash_hmac('sha256', "$header.$payload", $secret, true));

        return hash_equals($validSignature, $signature);
    }

    private function hasValidBearerFormat(string $authHeader): bool
    {
        return str_starts_with($authHeader, 'Bearer ') && strlen($authHeader) > 7;
    }

    private function validateTokenClaims(array $claims): array
    {
        $errors = [];

        // Required claims
        $requiredClaims = ['sub', 'exp'];

        foreach ($requiredClaims as $claim) {
            if (!isset($claims[$claim])) {
                $errors[] = "Missing required claim: {$claim}";
            }
        }

        return $errors;
    }

    private function isIssuedInFuture(array $claims): bool
    {
        if (!isset($claims['iat'])) {
            return false;
        }

        return $claims['iat'] > time();
    }

    private function isNotYetValid(array $claims): bool
    {
        if (!isset($claims['nbf'])) {
            return false;
        }

        return time() < $claims['nbf'];
    }
}
