<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Tests\Commerce\Catalog\Enrollment;

use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogEnrollmentPayload;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogEnrollmentPayloadProvider;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogEnrollmentResponse;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogEnrollmentSupersededException;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogHubRegistrar;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogHubRegistrationException;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\Message\DeliverCatalogEnrollmentMessage;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\Message\DeliverCatalogEnrollmentMessageHandler;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\Model\CatalogEnrollmentRequest;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\Model\CatalogEnrollmentStatus;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\Repository\CatalogEnrollmentRequestStore;
use CpCreation\VitiCore\Commerce\Catalog\Model\CatalogConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Repository\CatalogConfigurationStore;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Ramsey\Uuid\Uuid;

final class DeliverCatalogEnrollmentMessageHandlerTest extends TestCase
{
    private const string INSTANCE_ID = '7ad9e145-910f-4de4-9cec-5f832494adef';
    private const string FINGERPRINT = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

    public function testItPersistsTheHubRegistrationResponse(): void
    {
        $request = CatalogEnrollmentRequest::prepared(self::INSTANCE_ID, self::FINGERPRINT);
        $payload = $this->payload();
        $provider = $this->createStub(CatalogEnrollmentPayloadProvider::class);
        $provider->method('create')->willReturn($payload);
        $registrar = $this->createStub(CatalogHubRegistrar::class);
        $registrar->method('register')->willReturn(new CatalogEnrollmentResponse(
            instanceId: self::INSTANCE_ID,
            sourceId: 'source-42',
            webhookUrl: 'https://hub.example/webhooks/source-42',
            status: 'active',
            jwtIssuer: 'serenity-commerce-hub',
            jwtAudience: 'serenity:catalog',
            jwtKeyId: 'hub-main',
            jwtPublicKeyPem: 'public-key',
            raw: ['instance_id' => self::INSTANCE_ID, 'source_id' => 'source-42'],
        ));
        $store = $this->storeReturning($request);
        $configuration = $this->configuration();
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::once())->method('flush');

        (new DeliverCatalogEnrollmentMessageHandler(
            $store,
            $provider,
            $registrar,
            $this->configurationStoreReturning($configuration),
            $entityManager,
        ))(
            new DeliverCatalogEnrollmentMessage($request->getRequestId()->toString()),
        );

        self::assertSame(CatalogEnrollmentStatus::REGISTERED, $request->getStatus());
        self::assertSame('source-42', $request->getSourceId());
        self::assertSame('source-42', $configuration->getSourceId());
        self::assertSame('hub-main', $configuration->getJwtKeyId());
    }

    public function testHttp400ClosesTheRequestPermanently(): void
    {
        $request = CatalogEnrollmentRequest::prepared(self::INSTANCE_ID, self::FINGERPRINT);
        $provider = $this->createStub(CatalogEnrollmentPayloadProvider::class);
        $provider->method('create')->willReturn($this->payload());
        $registrar = $this->createStub(CatalogHubRegistrar::class);
        $registrar->method('register')->willThrowException(new CatalogHubRegistrationException(
            'HTTP 400',
            retryable: false,
            statusCode: 400,
            response: ['error' => 'invalid'],
        ));
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::once())->method('flush');

        $this->expectException(UnrecoverableMessageHandlingException::class);
        try {
            (new DeliverCatalogEnrollmentMessageHandler(
                $this->storeReturning($request),
                $provider,
                $registrar,
                $this->configurationStoreReturning($this->configuration()),
                $entityManager,
            ))(new DeliverCatalogEnrollmentMessage($request->getRequestId()->toString()));
        } finally {
            self::assertSame(CatalogEnrollmentStatus::FAILED_PERMANENT, $request->getStatus());
            self::assertSame(['error' => 'invalid'], $request->getLastResponse());
        }
    }

    public function testRetryableFailureRemainsQueuedAndUsesDurableMessengerRetry(): void
    {
        $request = CatalogEnrollmentRequest::prepared(self::INSTANCE_ID, self::FINGERPRINT);
        $provider = $this->createStub(CatalogEnrollmentPayloadProvider::class);
        $provider->method('create')->willReturn($this->payload());
        $registrar = $this->createStub(CatalogHubRegistrar::class);
        $registrar->method('register')->willThrowException(new CatalogHubRegistrationException(
            'network',
            retryable: true,
        ));
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::once())->method('flush');
        $configuration = $this->configuration();

        $this->expectException(RecoverableMessageHandlingException::class);
        try {
            (new DeliverCatalogEnrollmentMessageHandler(
                $this->storeReturning($request),
                $provider,
                $registrar,
                $this->configurationStoreReturning($configuration),
                $entityManager,
            ))(new DeliverCatalogEnrollmentMessage($request->getRequestId()->toString()));
        } finally {
            self::assertSame(CatalogEnrollmentStatus::QUEUED, $request->getStatus());
            self::assertFalse($request->isDeliveryComplete());
            self::assertSame('network', $request->getLastError());
            self::assertSame('network', $configuration->getLastError());
        }
    }

    public function testItDoesNotDeliverAQueuedRequestWhileCommerceIsDisabled(): void
    {
        $request = CatalogEnrollmentRequest::prepared(self::INSTANCE_ID, self::FINGERPRINT);
        $provider = $this->createMock(CatalogEnrollmentPayloadProvider::class);
        $provider->expects(self::never())->method('create');
        $registrar = $this->createMock(CatalogHubRegistrar::class);
        $registrar->expects(self::never())->method('register');
        $configuration = $this->configuration();
        $configuration->disable();
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::never())->method('flush');

        (new DeliverCatalogEnrollmentMessageHandler(
            $this->storeReturning($request),
            $provider,
            $registrar,
            $this->configurationStoreReturning($configuration),
            $entityManager,
        ))(new DeliverCatalogEnrollmentMessage($request->getRequestId()->toString()));

        self::assertFalse($request->isDeliveryComplete());
    }

    public function testASupersededRetryCannotPoisonTheCurrentConnection(): void
    {
        $request = CatalogEnrollmentRequest::prepared(self::INSTANCE_ID, self::FINGERPRINT);
        $provider = $this->createStub(CatalogEnrollmentPayloadProvider::class);
        $provider->method('create')->willThrowException(new CatalogEnrollmentSupersededException('superseded'));
        $registrar = $this->createMock(CatalogHubRegistrar::class);
        $registrar->expects(self::never())->method('register');
        $configuration = $this->configuration();
        $configuration->markEnrolled('current-source', 'https://hub.example/webhooks/current-source');
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->expects(self::once())->method('flush');

        (new DeliverCatalogEnrollmentMessageHandler(
            $this->storeReturning($request),
            $provider,
            $registrar,
            $this->configurationStoreReturning($configuration),
            $entityManager,
        ))(new DeliverCatalogEnrollmentMessage($request->getRequestId()->toString()));

        self::assertSame(CatalogEnrollmentStatus::FAILED_PERMANENT, $request->getStatus());
        self::assertSame('current-source', $configuration->getSourceId());
        self::assertNull($configuration->getLastError());
    }

    private function storeReturning(CatalogEnrollmentRequest $request): CatalogEnrollmentRequestStore
    {
        $store = $this->createStub(CatalogEnrollmentRequestStore::class);
        $store->method('findOneByRequestId')->willReturn($request);

        return $store;
    }

    private function configurationStoreReturning(CatalogConfiguration $configuration): CatalogConfigurationStore
    {
        $store = $this->createStub(CatalogConfigurationStore::class);
        $store->method('get')->willReturn($configuration);

        return $store;
    }

    private function configuration(): CatalogConfiguration
    {
        $configuration = new CatalogConfiguration(Uuid::fromString(self::INSTANCE_ID));
        $configuration->setEnabled(true);

        return $configuration;
    }

    private function payload(): CatalogEnrollmentPayload
    {
        return new CatalogEnrollmentPayload(
            '1.0',
            self::INSTANCE_ID,
            'Champagne Exemple',
            'Champagne Exemple SAS',
            'https://champagne.example',
            'viticore',
            '0123456789abcdef0123456789abcdef',
        );
    }
}
