<?php

declare(strict_types=1);

namespace Tests\Security;

use App\Framework\Security\CsrfTokenGenerator;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\ServerEnvironment;
use App\Framework\Http\ParsedUri;

/**
 * CSRF Protection tests
 *
 * Tests CSRF token generation, validation, and middleware integration
 */
final readonly class CsrfProtectionTest
{
    public function __construct(
        private CsrfTokenGenerator $csrfTokenGenerator
    ) {}

    /**
     * Test CSRF token generation
     */
    public function testGeneratesValidToken(): void
    {
        $token = $this->csrfTokenGenerator->generate();

        if (strlen($token) < 16) {
            throw new \RuntimeException(
                "CSRF token too short: " . strlen($token) . " characters (minimum 16 required)"
            );
        }

        echo "✅ CSRF token generated successfully (length: " . strlen($token) . ")\n";
    }

    /**
     * Test CSRF tokens are unique
     */
    public function testTokensAreUnique(): void
    {
        $tokens = [];
        for ($i = 0; $i < 100; $i++) {
            $tokens[] = $this->csrfTokenGenerator->generate();
        }

        $uniqueTokens = array_unique($tokens);

        if (count($uniqueTokens) !== count($tokens)) {
            throw new \RuntimeException(
                "CSRF tokens are not unique: " . count($uniqueTokens) . " unique out of " . count($tokens)
            );
        }

        echo "✅ All CSRF tokens are unique (tested 100 tokens)\n";
    }

    /**
     * Test CSRF token validation
     */
    public function testValidatesCorrectToken(): void
    {
        $token = $this->csrfTokenGenerator->generate();

        // Store token in session simulation
        $_SESSION['csrf_token'] = $token;

        // Validate same token
        if ($token !== $_SESSION['csrf_token']) {
            throw new \RuntimeException("CSRF token validation failed");
        }

        echo "✅ CSRF token validation works correctly\n";
    }

    /**
     * Test CSRF token mismatch detection
     */
    public function testDetectsTokenMismatch(): void
    {
        $validToken = $this->csrfTokenGenerator->generate();
        $invalidToken = $this->csrfTokenGenerator->generate();

        $_SESSION['csrf_token'] = $validToken;

        if ($invalidToken === $_SESSION['csrf_token']) {
            throw new \RuntimeException("CSRF token mismatch not detected");
        }

        echo "✅ CSRF token mismatch detected correctly\n";
    }

    /**
     * Test CSRF token missing detection
     */
    public function testDetectsMissingToken(): void
    {
        // Clear session token
        unset($_SESSION['csrf_token']);

        $providedToken = $this->csrfTokenGenerator->generate();

        if (isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $providedToken) {
            throw new \RuntimeException("Missing CSRF token not detected");
        }

        echo "✅ Missing CSRF token detected correctly\n";
    }

    /**
     * Test CSRF protection for POST requests
     */
    public function testRequiresCsrfForPostRequests(): void
    {
        // POST requests without CSRF token should be rejected
        $request = $this->createRequest(Method::POST, [], false);

        if (!$this->shouldRejectRequest($request)) {
            throw new \RuntimeException("POST request without CSRF token was not rejected");
        }

        echo "✅ POST requests without CSRF token are rejected\n";
    }

    /**
     * Test CSRF protection allows GET requests
     */
    public function testAllowsGetRequests(): void
    {
        // GET requests don't require CSRF token
        $request = $this->createRequest(Method::GET, [], false);

        if ($this->shouldRejectRequest($request)) {
            throw new \RuntimeException("GET request was incorrectly rejected");
        }

        echo "✅ GET requests are allowed without CSRF token\n";
    }

    /**
     * Test CSRF token rotation
     */
    public function testTokenRotation(): void
    {
        $token1 = $this->csrfTokenGenerator->generate();
        $_SESSION['csrf_token'] = $token1;

        // Simulate token rotation after successful request
        $token2 = $this->csrfTokenGenerator->generate();
        $_SESSION['csrf_token'] = $token2;

        if ($token1 === $token2) {
            throw new \RuntimeException("CSRF token not rotated");
        }

        if ($_SESSION['csrf_token'] === $token1) {
            throw new \RuntimeException("Old CSRF token still valid after rotation");
        }

        echo "✅ CSRF token rotation works correctly\n";
    }

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

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

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

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

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

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

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

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

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

        return $results;
    }

    private function createRequest(Method $method, array $postData = [], bool $includeCsrf = false): HttpRequest
    {
        if ($includeCsrf && $method !== Method::GET) {
            $postData['_csrf_token'] = $_SESSION['csrf_token'] ?? '';
        }

        $parsedUri = ParsedUri::fromString('https://localhost/api/test');

        $server = new ServerEnvironment([
            'REQUEST_METHOD' => $method->value,
            'REQUEST_URI' => '/api/test',
            'SERVER_NAME' => 'localhost',
            'SERVER_PORT' => '443',
            'HTTPS' => 'on'
        ]);

        return new HttpRequest(
            method: $method,
            uri: $parsedUri,
            server: $server,
            headers: [],
            body: !empty($postData) ? json_encode($postData) : '',
            parsedBody: !empty($postData) ? $postData : null,
            queryParameters: [],
            cookies: [],
            files: []
        );
    }

    private function shouldRejectRequest(HttpRequest $request): bool
    {
        // POST, PUT, DELETE, PATCH require CSRF token
        if (in_array($request->method, [Method::POST, Method::PUT, Method::DELETE, Method::PATCH])) {
            $csrfToken = $request->parsedBody['_csrf_token'] ?? null;
            $sessionToken = $_SESSION['csrf_token'] ?? null;

            return $csrfToken !== $sessionToken || $csrfToken === null;
        }

        return false;
    }
}
