<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Tests\Monitoring\Error;

use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogConfigurationProvider;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogConfigurationSnapshot;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogHubConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogIntegrationConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Outbox\CatalogWebhookSigner;
use CpCreation\VitiCore\Monitoring\Error\ErrorMonitoringConfiguration;
use CpCreation\VitiCore\Monitoring\Error\ErrorReportFactory;
use CpCreation\VitiCore\Monitoring\Error\HttpErrorSubscriber;
use CpCreation\VitiCore\Monitoring\Error\Message\DeliverErrorReportMessage;
use CpCreation\VitiCore\Monitoring\Error\Message\DeliverErrorReportMessageHandler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

final class HttpErrorMonitoringTest extends TestCase
{
    private const string INSTANCE_ID = '7ad9e145-910f-4de4-9cec-5f832494adef';
    private const string SECRET = '0123456789abcdef0123456789abcdef';

    public function testItQueuesOnlyMainRequestServerErrorsWithoutRequestSecrets(): void
    {
        $configuration = $this->configuration();
        $factory = new ErrorReportFactory($configuration, '/srv/app', 'prod');
        $messageBus = $this->createMock(MessageBusInterface::class);
        $captured = null;
        $messageBus->expects(self::once())
            ->method('dispatch')
            ->willReturnCallback(static function (object $message) use (&$captured): Envelope {
                $captured = $message;

                return new Envelope($message);
            });
        $subscriber = new HttpErrorSubscriber($configuration, $factory, $messageBus);
        $request = Request::create(
            'https://shop.example/reset/secret?token=do-not-send',
            'POST',
            ['password' => 'do-not-send'],
            ['session' => 'do-not-send'],
            server: ['HTTP_AUTHORIZATION' => 'Bearer do-not-send'],
        );
        $request->attributes->set('_route', 'account_reset');
        $kernel = $this->createStub(HttpKernelInterface::class);

        $subscriber->onKernelException(new ExceptionEvent(
            $kernel,
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new \RuntimeException('Échec applicatif'),
        ));
        $subscriber->onKernelResponse(new ResponseEvent(
            $kernel,
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new Response(status: 500),
        ));

        self::assertInstanceOf(DeliverErrorReportMessage::class, $captured);
        self::assertSame(self::INSTANCE_ID, $captured->payload['instance_id']);
        self::assertSame(500, $captured->payload['status_code']);
        self::assertSame('account_reset', $captured->payload['route']);
        self::assertSame('POST', $captured->payload['method']);
        self::assertArrayNotHasKey('url', $captured->payload);
        self::assertArrayNotHasKey('query', $captured->payload);
        self::assertArrayNotHasKey('headers', $captured->payload);
        self::assertArrayNotHasKey('cookies', $captured->payload);
        self::assertArrayNotHasKey('body', $captured->payload);
        self::assertStringNotContainsString('do-not-send', json_encode($captured->payload, \JSON_THROW_ON_ERROR));
    }

    public function testItIgnoresClientErrors(): void
    {
        $configuration = $this->configuration();
        $messageBus = $this->createMock(MessageBusInterface::class);
        $messageBus->expects(self::never())->method('dispatch');
        $subscriber = new HttpErrorSubscriber(
            $configuration,
            new ErrorReportFactory($configuration, '/srv/app', 'prod'),
            $messageBus,
        );

        $subscriber->onKernelException(new ExceptionEvent(
            $kernel = $this->createStub(HttpKernelInterface::class),
            $request = Request::create('/missing'),
            HttpKernelInterface::MAIN_REQUEST,
            new NotFoundHttpException(),
        ));
        $subscriber->onKernelResponse(new ResponseEvent(
            $kernel,
            $request,
            HttpKernelInterface::MAIN_REQUEST,
            new Response(status: 404),
        ));
    }

    public function testItAlsoReportsAnExplicitServerErrorResponse(): void
    {
        $configuration = $this->configuration();
        $messageBus = $this->createMock(MessageBusInterface::class);
        $messageBus->expects(self::once())
            ->method('dispatch')
            ->with(self::callback(static fn(object $message): bool => $message instanceof DeliverErrorReportMessage
                && 503 === $message->payload['status_code']))
            ->willReturnCallback(static fn(object $message): Envelope => new Envelope($message));
        $subscriber = new HttpErrorSubscriber(
            $configuration,
            new ErrorReportFactory($configuration, '/srv/app', 'prod'),
            $messageBus,
        );

        $subscriber->onKernelResponse(new ResponseEvent(
            $this->createStub(HttpKernelInterface::class),
            Request::create('/maintenance'),
            HttpKernelInterface::MAIN_REQUEST,
            new Response(status: 503),
        ));
    }

    public function testTheAsyncHandlerSignsAndSendsTheReport(): void
    {
        $configuration = $this->configuration();
        $payload = new ErrorReportFactory($configuration, '/srv/app', 'prod')->create(
            new \RuntimeException('Échec applicatif'),
            Request::create('/'),
            500,
        );
        $called = false;
        $httpClient = new MockHttpClient(function (string $method, string $url, array $options) use (&$called, $payload): MockResponse {
            $called = true;
            self::assertSame('POST', $method);
            self::assertSame(
                'https://hub.example/api/integrations/serenity/' . self::INSTANCE_ID . '/errors',
                $url,
            );
            self::assertSame($payload, json_decode($options['body'], true, flags: \JSON_THROW_ON_ERROR));
            self::assertSame(
                'X-Serenity-Instance: ' . self::INSTANCE_ID,
                $options['normalized_headers']['x-serenity-instance'][0],
            );
            self::assertMatchesRegularExpression(
                '/^X-Serenity-Signature: v1=[a-f0-9]{64}$/',
                $options['normalized_headers']['x-serenity-signature'][0],
            );

            return new MockResponse('{"status":"accepted"}', ['http_code' => 202]);
        });

        (new DeliverErrorReportMessageHandler(
            $configuration,
            new CatalogWebhookSigner(),
            $httpClient,
        ))(new DeliverErrorReportMessage($payload));

        self::assertTrue($called);
    }

    private function configuration(): ErrorMonitoringConfiguration
    {
        $provider = $this->createStub(CatalogConfigurationProvider::class);
        $provider->method('current')->willReturn(new CatalogConfigurationSnapshot(
            enabled: false,
            hubUrl: 'https://hub.example',
            instanceId: self::INSTANCE_ID,
            currency: 'EUR',
            authAudience: 'serenity:catalog',
            enrollmentToken: 'enrollment-token',
            webhookSecret: self::SECRET,
            webhookUrl: 'https://hub.example/webhooks/serenity/by-instance/' . self::INSTANCE_ID,
            jwtIssuer: 'serenity-commerce-hub',
            jwtKeyId: 'hub-main',
            jwtPublicKeyPem: 'public-key',
            monitoringEnabled: true,
        ));
        $integration = new CatalogIntegrationConfiguration(false, '', '', 'EUR', $provider);
        $hub = new CatalogHubConfiguration('', '', '', '', '', '', false, $provider);

        return new ErrorMonitoringConfiguration($integration, $hub, $provider);
    }
}
