<?php

declare(strict_types=1);

namespace Tests\Framework\Security;

use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Middlewares\SecurityHeaderConfig;
use App\Framework\Http\Middlewares\SecurityHeaderMiddleware;
use App\Framework\Http\Next;
use App\Framework\Http\Request;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Status;
use PHPUnit\Framework\TestCase;

/**
 * Critical Security Tests for HTTP Security Headers
 * Tests gegen OWASP A05:2021 – Security Misconfiguration
 */
final class SecurityHeadersTest extends TestCase
{
    private SecurityHeaderConfig $config;

    private SecurityHeaderMiddleware $middleware;

    private RequestStateManager $stateManager;

    protected function setUp(): void
    {
        $this->config = new SecurityHeaderConfig(
            hsts: true,
            hstsMaxAge: 31536000,
            hstsIncludeSubdomains: true,
            hstsPreload: true,
            csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
            xFrameOptions: 'DENY',
            xContentTypeOptions: true,
            referrerPolicy: 'strict-origin-when-cross-origin',
            permissionsPolicy: "camera=(), microphone=(), geolocation=()"
        );
        $this->middleware = new SecurityHeaderMiddleware($this->config);
        $this->stateManager = $this->createMock(RequestStateManager::class);
    }

    /**
     * Test: HSTS Header wird korrekt gesetzt
     * Verhindert SSL-Stripping Attacks
     */
    public function test_hsts_header_set_correctly(): void
    {
        // Arrange
        $request = $this->createHttpsRequest();
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $this->assertEquals(
            'max-age=31536000; includeSubDomains; preload',
            $headers->getFirst('Strict-Transport-Security')
        );
    }

    /**
     * Test: CSP Header verhindert XSS
     * OWASP A03:2021 – Injection
     */
    public function test_csp_header_prevents_xss(): void
    {
        // Arrange
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $cspHeader = $headers->getFirst('Content-Security-Policy');

        $this->assertStringContains("default-src 'self'", $cspHeader);
        $this->assertStringContains("script-src 'self'", $cspHeader);
        $this->assertStringNotContains("'unsafe-eval'", $cspHeader);
    }

    /**
     * Test: X-Frame-Options verhindert Clickjacking
     * OWASP A05:2021 – Security Misconfiguration
     */
    public function test_x_frame_options_prevents_clickjacking(): void
    {
        // Arrange
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $this->assertEquals('DENY', $headers->getFirst('X-Frame-Options'));
    }

    /**
     * Test: X-Content-Type-Options verhindert MIME-Type Confusion
     */
    public function test_x_content_type_options_prevents_mime_sniffing(): void
    {
        // Arrange
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $this->assertEquals('nosniff', $headers->getFirst('X-Content-Type-Options'));
    }

    /**
     * Test: Referrer Policy schützt vor Information Leakage
     */
    public function test_referrer_policy_prevents_info_leakage(): void
    {
        // Arrange
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $this->assertEquals(
            'strict-origin-when-cross-origin',
            $headers->getFirst('Referrer-Policy')
        );
    }

    /**
     * Test: Permissions Policy beschränkt Browser-Features
     */
    public function test_permissions_policy_restricts_features(): void
    {
        // Arrange
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert
        $headers = $result->response->headers;
        $permissionsPolicy = $headers->getFirst('Permissions-Policy');

        $this->assertStringContains('camera=()', $permissionsPolicy);
        $this->assertStringContains('microphone=()', $permissionsPolicy);
        $this->assertStringContains('geolocation=()', $permissionsPolicy);
    }

    /**
     * Test: HSTS wird nur über HTTPS gesetzt
     * Verhindert Mixed Content Probleme
     */
    public function test_hsts_only_over_https(): void
    {
        // Arrange: HTTP Request
        $request = $this->createHttpRequest();
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert: Kein HSTS Header über HTTP
        $headers = $result->response->headers;
        $this->assertNull($headers->getFirst('Strict-Transport-Security'));
    }

    /**
     * Test: Security Headers werden nicht überschrieben
     * Respektiert explizit gesetzte Headers
     */
    public function test_existing_security_headers_not_overridden(): void
    {
        // Arrange: Response mit bereits gesetztem CSP Header
        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $existingHeaders = ['Content-Security-Policy' => ["default-src 'none'"]];
        $response = new HttpResponse(Status::OK, $existingHeaders, 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $this->middleware->__invoke($context, $next, $this->stateManager);

        // Assert: Ursprünglicher CSP Header bleibt erhalten
        $headers = $result->response->headers;
        $this->assertEquals("default-src 'none'", $headers->getFirst('Content-Security-Policy'));
    }

    /**
     * Test: Development-spezifische CSP Policy
     * Erlaubt eval() und inline styles in Development
     */
    public function test_development_csp_policy(): void
    {
        // Arrange: Development Config
        $devConfig = new SecurityHeaderConfig(
            csp: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
        );
        $middleware = new SecurityHeaderMiddleware($devConfig);

        $request = $this->createMock(Request::class);
        $context = new MiddlewareContext($request);
        $response = new HttpResponse(Status::OK, [], 'content');

        $next = $this->createMock(Next::class);
        $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response));

        // Act
        $result = $middleware->__invoke($context, $next, $this->stateManager);

        // Assert: Development CSP erlaubt unsafe-eval
        $headers = $result->response->headers;
        $cspHeader = $headers->getFirst('Content-Security-Policy');

        $this->assertStringContains("'unsafe-eval'", $cspHeader);
        $this->assertStringContains("'unsafe-inline'", $cspHeader);
    }

    // Helper Methods

    private function createHttpsRequest(): Request
    {
        $request = $this->createMock(Request::class);
        $request->method('isSecure')->willReturn(true);

        return $request;
    }

    private function createHttpRequest(): Request
    {
        $request = $this->createMock(Request::class);
        $request->method('isSecure')->willReturn(false);

        return $request;
    }
}
