<?php

declare(strict_types=1);

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

use App\Shared\Infrastructure\Audit\CorrelationIdHolder;
use App\Shared\Infrastructure\Messenger\CorrelationIdMiddleware;
use App\Shared\Infrastructure\Messenger\CorrelationIdStamp;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
use RuntimeException;
use stdClass;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;

/**
 * @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 testUsesCorrelationIdFromStampWhenPresent(): void
    {
        $expectedId = '550e8400-e29b-41d4-a716-446655440000';
        $envelope = new Envelope(new stdClass(), [new CorrelationIdStamp($expectedId)]);
        $stack = $this->createCapturingStack();

        $this->middleware->handle($envelope, $stack);

        // Verify the correlation ID was set during handling
        $this->assertSame($expectedId, $stack->capturedCorrelationId);
    }

    public function testGeneratesNewCorrelationIdWhenNoStampAndNoExistingId(): void
    {
        // Async worker context: no existing ID, no stamp
        $envelope = new Envelope(new stdClass());
        $stack = $this->createCapturingStack();

        $this->middleware->handle($envelope, $stack);

        // Should have generated a valid UUID
        $this->assertNotNull($stack->capturedCorrelationId);
        $this->assertTrue(Uuid::isValid($stack->capturedCorrelationId));
    }

    public function testUsesExistingCorrelationIdDuringSynchronousDispatch(): void
    {
        // HTTP context: existing ID is set by HTTP middleware
        $existingId = '99999999-9999-9999-9999-999999999999';
        CorrelationIdHolder::set(\App\Shared\Domain\CorrelationId::fromString($existingId));

        $envelope = new Envelope(new stdClass());
        $stack = $this->createCapturingStack();

        $this->middleware->handle($envelope, $stack);

        // Should use the existing ID, not generate a new one
        $this->assertSame($existingId, $stack->capturedCorrelationId);
    }

    public function testDoesNotClearCorrelationIdDuringSynchronousDispatch(): void
    {
        // HTTP context: existing ID should be preserved after dispatch
        $existingId = '99999999-9999-9999-9999-999999999999';
        CorrelationIdHolder::set(\App\Shared\Domain\CorrelationId::fromString($existingId));

        $envelope = new Envelope(new stdClass());
        $stack = $this->createPassthroughStack();

        $this->middleware->handle($envelope, $stack);

        // Should NOT be cleared - HTTP middleware handles that
        $this->assertNotNull(CorrelationIdHolder::get());
        $this->assertSame($existingId, CorrelationIdHolder::get()?->value());
    }

    public function testClearsCorrelationIdInAsyncContext(): void
    {
        // Async worker context: no existing ID, no stamp
        $envelope = new Envelope(new stdClass());
        $stack = $this->createPassthroughStack();

        $this->middleware->handle($envelope, $stack);

        // Should be cleared after handling in async context
        $this->assertNull(CorrelationIdHolder::get());
    }

    public function testClearsCorrelationIdWhenStampPresent(): void
    {
        // Async worker receiving message from HTTP: has stamp
        $envelope = new Envelope(new stdClass(), [new CorrelationIdStamp('11111111-1111-1111-1111-111111111111')]);
        $stack = $this->createPassthroughStack();

        $this->middleware->handle($envelope, $stack);

        // Should be cleared after handling (async context)
        $this->assertNull(CorrelationIdHolder::get());
    }

    public function testClearsCorrelationIdEvenOnException(): void
    {
        // Async worker context
        $envelope = new Envelope(new stdClass());
        $stack = $this->createThrowingStack();

        try {
            $this->middleware->handle($envelope, $stack);
            $this->fail('Expected exception to be thrown');
        } catch (RuntimeException) {
            // Expected
        }

        // Should be cleared even after exception in async context
        $this->assertNull(CorrelationIdHolder::get());
    }

    public function testDoesNotLeakBetweenMessages(): void
    {
        $envelope1 = new Envelope(new stdClass(), [new CorrelationIdStamp('11111111-1111-1111-1111-111111111111')]);
        $envelope2 = new Envelope(new stdClass(), [new CorrelationIdStamp('22222222-2222-2222-2222-222222222222')]);

        $stack1 = $this->createCapturingStack();
        $stack2 = $this->createCapturingStack();

        $this->middleware->handle($envelope1, $stack1);
        $this->middleware->handle($envelope2, $stack2);

        $this->assertSame('11111111-1111-1111-1111-111111111111', $stack1->capturedCorrelationId);
        $this->assertSame('22222222-2222-2222-2222-222222222222', $stack2->capturedCorrelationId);
    }

    private function createCapturingStack(): CapturingStack
    {
        return new CapturingStack();
    }

    private function createPassthroughStack(): StackInterface
    {
        return new class implements StackInterface {
            public function next(): MiddlewareInterface
            {
                return new class implements MiddlewareInterface {
                    public function handle(Envelope $envelope, StackInterface $stack): Envelope
                    {
                        return $envelope;
                    }
                };
            }
        };
    }

    private function createThrowingStack(): StackInterface
    {
        return new class implements StackInterface {
            public function next(): MiddlewareInterface
            {
                return new class implements MiddlewareInterface {
                    public function handle(Envelope $envelope, StackInterface $stack): Envelope
                    {
                        throw new RuntimeException('Handler failed');
                    }
                };
            }
        };
    }
}

/**
 * Stack that captures the correlation ID during handling.
 */
final class CapturingStack implements StackInterface
{
    public ?string $capturedCorrelationId = null;

    public function next(): MiddlewareInterface
    {
        return new CapturingMiddleware($this);
    }
}

/**
 * @internal
 */
final class CapturingMiddleware implements MiddlewareInterface
{
    public function __construct(private CapturingStack $stack)
    {
    }

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        $this->stack->capturedCorrelationId = CorrelationIdHolder::get()?->value();

        return $envelope;
    }
}
