<?php

declare(strict_types=1);

namespace App\Tests\Unit\Shared\Infrastructure\Audit;

use App\Shared\Domain\CorrelationId;
use App\Shared\Infrastructure\Audit\CorrelationIdHolder;
use App\Shared\Infrastructure\Middleware\CorrelationIdMiddleware;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

/**
 * @see Story 1.7 - T3: Correlation ID
 */
final class CorrelationIdMiddlewareTest extends TestCase
{
    private CorrelationIdMiddleware $middleware;

    protected function setUp(): void
    {
        $this->middleware = new CorrelationIdMiddleware();
        CorrelationIdHolder::clear();
    }

    protected function tearDown(): void
    {
        CorrelationIdHolder::clear();
    }

    public function testGeneratesCorrelationIdWhenHeaderMissing(): void
    {
        $request = Request::create('/test');
        $event = $this->createRequestEvent($request, true);

        $this->middleware->onKernelRequest($event);

        $correlationId = CorrelationIdHolder::get();
        $this->assertNotNull($correlationId);
        $this->assertTrue(Uuid::isValid($correlationId->value()));
    }

    public function testUsesExistingCorrelationIdFromHeader(): void
    {
        $existingId = '550e8400-e29b-41d4-a716-446655440000';
        $request = Request::create('/test');
        $request->headers->set('X-Correlation-Id', $existingId);
        $event = $this->createRequestEvent($request, true);

        $this->middleware->onKernelRequest($event);

        $correlationId = CorrelationIdHolder::get();
        $this->assertNotNull($correlationId);
        $this->assertSame($existingId, $correlationId->value());
    }

    public function testGeneratesNewIdWhenHeaderContainsInvalidUuid(): void
    {
        $request = Request::create('/test');
        $request->headers->set('X-Correlation-Id', 'not-a-valid-uuid');
        $event = $this->createRequestEvent($request, true);

        $this->middleware->onKernelRequest($event);

        $correlationId = CorrelationIdHolder::get();
        $this->assertNotNull($correlationId);
        // Should have generated a new valid UUID, not kept the invalid one
        $this->assertTrue(Uuid::isValid($correlationId->value()));
        $this->assertNotSame('not-a-valid-uuid', $correlationId->value());
    }

    public function testStoresCorrelationIdInRequestAttributes(): void
    {
        $request = Request::create('/test');
        $event = $this->createRequestEvent($request, true);

        $this->middleware->onKernelRequest($event);

        $this->assertInstanceOf(CorrelationId::class, $request->attributes->get('correlation_id'));
    }

    public function testIgnoresSubRequests(): void
    {
        $request = Request::create('/test');
        $event = $this->createRequestEvent($request, false);

        $this->middleware->onKernelRequest($event);

        $this->assertNull(CorrelationIdHolder::get());
    }

    public function testAddsCorrelationIdHeaderToResponse(): void
    {
        $correlationId = CorrelationId::generate();
        CorrelationIdHolder::set($correlationId);

        $request = Request::create('/test');
        $response = new Response();
        $event = $this->createResponseEvent($request, $response, true);

        $this->middleware->onKernelResponse($event);

        $this->assertSame(
            $correlationId->value(),
            $response->headers->get('X-Correlation-Id'),
        );
    }

    public function testDoesNotAddHeaderToSubRequestResponse(): void
    {
        $correlationId = CorrelationId::generate();
        CorrelationIdHolder::set($correlationId);

        $request = Request::create('/test');
        $response = new Response();
        $event = $this->createResponseEvent($request, $response, false);

        $this->middleware->onKernelResponse($event);

        $this->assertNull($response->headers->get('X-Correlation-Id'));
    }

    public function testClearsCorrelationIdOnTerminate(): void
    {
        CorrelationIdHolder::set(CorrelationId::generate());

        $request = Request::create('/test');
        $response = new Response();
        $event = $this->createTerminateEvent($request, $response);

        $this->middleware->onKernelTerminate($event);

        $this->assertNull(CorrelationIdHolder::get());
    }

    public function testDefensiveClearOnNewRequest(): void
    {
        // Simulate a stale correlation ID from a previous request that didn't clean up
        $staleId = CorrelationId::generate();
        CorrelationIdHolder::set($staleId);

        $request = Request::create('/test');
        $event = $this->createRequestEvent($request, true);

        $this->middleware->onKernelRequest($event);

        // Should have a NEW correlation ID, not the stale one
        $newId = CorrelationIdHolder::get();
        $this->assertNotNull($newId);
        $this->assertNotSame($staleId->value(), $newId->value());
    }

    private function createRequestEvent(Request $request, bool $isMainRequest): RequestEvent
    {
        $kernel = $this->createMock(HttpKernelInterface::class);

        return new RequestEvent(
            $kernel,
            $request,
            $isMainRequest ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::SUB_REQUEST,
        );
    }

    private function createResponseEvent(Request $request, Response $response, bool $isMainRequest): ResponseEvent
    {
        $kernel = $this->createMock(HttpKernelInterface::class);

        return new ResponseEvent(
            $kernel,
            $request,
            $isMainRequest ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::SUB_REQUEST,
            $response,
        );
    }

    private function createTerminateEvent(Request $request, Response $response): TerminateEvent
    {
        $kernel = $this->createMock(HttpKernelInterface::class);

        return new TerminateEvent($kernel, $request, $response);
    }
}
