<?php

declare(strict_types=1);

namespace Grav\Plugin\Api\Tests\Unit\Middleware;

use Grav\Plugin\Api\Middleware\CorsMiddleware;
use Grav\Plugin\Api\Tests\Unit\TestHelper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;

#[CoversClass(CorsMiddleware::class)]
class CorsMiddlewareTest extends TestCase
{
    private function buildMiddleware(array $corsConfig): CorsMiddleware
    {
        $config = TestHelper::createMockConfig([
            'plugins' => ['api' => ['cors' => $corsConfig]],
        ]);

        return new CorsMiddleware($config);
    }

    #[Test]
    public function adds_cors_headers_to_response(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
            'credentials' => false,
            'expose_headers' => [],
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://example.com']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('*', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function wildcard_origin_allows_all(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://any-domain.test']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('*', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function specific_origin_matching(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['http://allowed.test', 'http://also-allowed.test'],
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://allowed.test']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('http://allowed.test', $result->getHeaderLine('Access-Control-Allow-Origin'));
        self::assertSame('Origin', $result->getHeaderLine('Vary'));
    }

    #[Test]
    public function non_matching_origin_no_cors_headers(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['http://allowed.test'],
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://evil.test']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function credentials_header_when_enabled(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
            'credentials' => true,
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://example.com']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('true', $result->getHeaderLine('Access-Control-Allow-Credentials'));
    }

    #[Test]
    public function cors_disabled_no_headers(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => false,
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://example.com']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function no_origin_header_no_cors_headers(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
        ]);

        $request = TestHelper::createMockRequest();
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function expose_headers_are_set(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
            'expose_headers' => ['X-Request-Id', 'X-Rate-Limit-Remaining'],
        ]);

        $request = TestHelper::createMockRequest(headers: ['Origin' => 'http://example.com']);
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        // CorsMiddleware always appends X-Invalidates (cache-invalidation tags)
        // and ETag (optimistic concurrency) so cross-origin clients can read
        // them, regardless of the configured expose_headers.
        self::assertSame(
            'X-Request-Id, X-Rate-Limit-Remaining, X-Invalidates, ETag',
            $result->getHeaderLine('Access-Control-Expose-Headers'),
        );
    }

    #[Test]
    public function preflight_response_reflects_allowlisted_origin(): void
    {
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['http://example.com'],
            'methods' => ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
            'headers' => ['Authorization', 'Content-Type'],
            'max_age' => 86400,
            'credentials' => false,
        ]);

        $request = TestHelper::createMockRequest(method: 'OPTIONS', headers: ['Origin' => 'http://example.com']);
        $response = $middleware->createPreflightResponse($request);

        self::assertInstanceOf(ResponseInterface::class, $response);
        self::assertSame(204, $response->getStatusCode());
        self::assertSame('http://example.com', $response->getHeaderLine('Access-Control-Allow-Origin'));
        self::assertSame('Origin', $response->getHeaderLine('Vary'));
        self::assertStringContainsString('GET', $response->getHeaderLine('Access-Control-Allow-Methods'));
        self::assertStringContainsString('Authorization', $response->getHeaderLine('Access-Control-Allow-Headers'));
        self::assertSame('86400', $response->getHeaderLine('Access-Control-Max-Age'));
        self::assertSame('0', $response->getHeaderLine('Content-Length'));
    }

    #[Test]
    public function preflight_does_not_grant_wildcard_origin(): void
    {
        // Security guard (GHSA-hqm9-5xxw-4qxp): a `*` config must never let an
        // arbitrary origin pass preflight, otherwise the browser would execute
        // the cross-origin POST/DELETE that follows.
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
            'methods' => ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
        ]);

        $request = TestHelper::createMockRequest(method: 'OPTIONS', headers: ['Origin' => 'http://evil.test']);
        $response = $middleware->createPreflightResponse($request);

        self::assertSame(204, $response->getStatusCode());
        self::assertSame('', $response->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function wildcard_not_applied_to_authenticated_response(): void
    {
        // Security guard (GHSA-hqm9-5xxw-4qxp): `*` is honored for guests but
        // never for an authenticated response, so a stolen token can't be used
        // to read API data cross-origin from an attacker page.
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['*'],
        ]);

        $request = TestHelper::createMockRequest(
            headers: ['Origin' => 'http://evil.test'],
            attributes: ['api_user' => (object) ['username' => 'admin']],
        );
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    #[Test]
    public function allowlisted_origin_applied_to_authenticated_response(): void
    {
        // An explicitly trusted origin is still reflected for authenticated
        // responses — that is the supported way to allow a browser SPA.
        $middleware = $this->buildMiddleware([
            'enabled' => true,
            'origins' => ['http://app.example.com'],
        ]);

        $request = TestHelper::createMockRequest(
            headers: ['Origin' => 'http://app.example.com'],
            attributes: ['api_user' => (object) ['username' => 'admin']],
        );
        $response = $this->createStubResponse();

        $result = $middleware->addHeaders($request, $response);

        self::assertSame('http://app.example.com', $result->getHeaderLine('Access-Control-Allow-Origin'));
    }

    /**
     * Lightweight PSR-7 ResponseInterface stub with withHeader() support.
     */
    private function createStubResponse(): ResponseInterface
    {
        return new \Grav\Framework\Psr7\Response();
    }
}
