<?php

declare(strict_types=1);

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

use App\Shared\Infrastructure\Messenger\MessengerMetricsMiddleware;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Prometheus\CollectorRegistry;
use Prometheus\Counter;
use RuntimeException;
use stdClass;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;

final class MessengerMetricsMiddlewareTest extends TestCase
{
    #[Test]
    public function itIncrementsHandledCounterOnSuccess(): void
    {
        $handledCounter = $this->createMock(Counter::class);
        $failedCounter = $this->createMock(Counter::class);

        // Label is now the message class name, not the handler name
        $handledCounter->expects($this->once())
            ->method('inc')
            ->with(['stdClass']);

        $failedCounter->expects($this->never())->method('inc');

        $registry = $this->createRegistryMock($handledCounter, $failedCounter);
        $middleware = new MessengerMetricsMiddleware($registry);

        $envelope = new Envelope(new stdClass());
        $returnedEnvelope = $envelope->with(new HandledStamp('result', 'App\\Test\\TestHandler'));

        $stack = $this->createStackReturning($returnedEnvelope);

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

    #[Test]
    public function itIncrementsFailedCounterOnException(): void
    {
        $handledCounter = $this->createMock(Counter::class);
        $failedCounter = $this->createMock(Counter::class);

        $handledCounter->expects($this->never())->method('inc');
        $failedCounter->expects($this->once())
            ->method('inc')
            ->with(['stdClass']);

        $registry = $this->createRegistryMock($handledCounter, $failedCounter);
        $middleware = new MessengerMetricsMiddleware($registry);

        $envelope = new Envelope(new stdClass());
        $stack = $this->createStackThrowing(new RuntimeException('fail'));

        $this->expectException(RuntimeException::class);
        $middleware->handle($envelope, $stack);
    }

    #[Test]
    public function itDoesNotIncrementHandledWhenNoHandledStamps(): void
    {
        $handledCounter = $this->createMock(Counter::class);
        $failedCounter = $this->createMock(Counter::class);

        $handledCounter->expects($this->never())->method('inc');
        $failedCounter->expects($this->never())->method('inc');

        $registry = $this->createRegistryMock($handledCounter, $failedCounter);
        $middleware = new MessengerMetricsMiddleware($registry);

        // No HandledStamp = async dispatch, no handler executed yet
        $envelope = new Envelope(new stdClass());
        $stack = $this->createStackReturning($envelope);

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

    #[Test]
    public function itSurvivesPrometheusStorageFailure(): void
    {
        $handledCounter = $this->createMock(Counter::class);
        $failedCounter = $this->createMock(Counter::class);

        // Simulate Redis/Prometheus failure
        $handledCounter->method('inc')->willThrowException(new RuntimeException('Redis connection refused'));

        $registry = $this->createRegistryMock($handledCounter, $failedCounter);
        $middleware = new MessengerMetricsMiddleware($registry);

        $envelope = new Envelope(new stdClass());
        $returnedEnvelope = $envelope->with(new HandledStamp('result', 'App\\Test\\TestHandler'));
        $stack = $this->createStackReturning($returnedEnvelope);

        // Should NOT throw - metrics failure is swallowed
        $result = $middleware->handle($envelope, $stack);
        self::assertSame($returnedEnvelope, $result);
    }

    private function createRegistryMock(Counter $handledCounter, Counter $failedCounter): CollectorRegistry
    {
        $registry = $this->createMock(CollectorRegistry::class);
        $registry->method('getOrRegisterCounter')
            ->willReturnCallback(
                static fn (string $ns, string $name): Counter => match (true) {
                    str_contains($name, 'handled') => $handledCounter,
                    str_contains($name, 'failed') => $failedCounter,
                },
            );

        return $registry;
    }

    private function createStackReturning(Envelope $envelope): StackInterface
    {
        return new class($envelope) implements StackInterface {
            public function __construct(private readonly Envelope $envelope)
            {
            }

            public function next(): MiddlewareInterface
            {
                return new class($this->envelope) implements MiddlewareInterface {
                    public function __construct(private readonly Envelope $envelope)
                    {
                    }

                    public function handle(Envelope $envelope, StackInterface $stack): Envelope
                    {
                        return $this->envelope;
                    }
                };
            }
        };
    }

    private function createStackThrowing(RuntimeException $exception): StackInterface
    {
        return new class($exception) implements StackInterface {
            public function __construct(private readonly RuntimeException $exception)
            {
            }

            public function next(): MiddlewareInterface
            {
                return new class($this->exception) implements MiddlewareInterface {
                    public function __construct(private readonly RuntimeException $exception)
                    {
                    }

                    public function handle(Envelope $envelope, StackInterface $stack): Envelope
                    {
                        throw $this->exception;
                    }
                };
            }
        };
    }
}
