<?php

declare(strict_types=1);

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

use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogHubConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogEnrollmentPayload;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\CatalogHubRegistrationException;
use CpCreation\VitiCore\Commerce\Catalog\Enrollment\HttpCatalogHubRegistrar;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

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

    private static string $publicKeyPem;

    public static function setUpBeforeClass(): void
    {
        $key = openssl_pkey_new([
            'private_key_bits' => 2048,
            'private_key_type' => \OPENSSL_KEYTYPE_RSA,
        ]);
        self::assertNotFalse($key);
        $details = openssl_pkey_get_details($key);
        self::assertIsArray($details);
        self::assertIsString($details['key']);
        self::$publicKeyPem = $details['key'];
    }

    public function testItUsesTheIdempotentHubContract(): void
    {
        $client = new MockHttpClient(function (string $method, string $url, array $options): MockResponse {
            self::assertSame('PUT', $method);
            self::assertSame('http://127.0.0.1:8085/api/integrations/serenity/' . self::INSTANCE_ID, $url);
            self::assertSame('Authorization: Bearer long-lived-enrollment-token', $options['normalized_headers']['authorization'][0]);
            self::assertSame('Idempotency-Key: ' . self::INSTANCE_ID, $options['normalized_headers']['idempotency-key'][0]);
            self::assertSame(self::INSTANCE_ID, json_decode($options['body'], true, flags: \JSON_THROW_ON_ERROR)['instance_id']);

            return new MockResponse(json_encode([
                'instance_id' => self::INSTANCE_ID,
                'source_id' => 'source-42',
                'webhook_url' => 'http://127.0.0.1:8085/webhooks/serenity/source-42',
                'status' => 'active',
                'jwt_issuer' => 'serenity-commerce-hub',
                'jwt_audience' => 'viticore',
                'jwt_key_id' => 'hub-main',
                'jwt_algorithm' => 'RS256',
                'jwt_public_key' => self::$publicKeyPem,
            ], \JSON_THROW_ON_ERROR), ['http_code' => 201]);
        });

        $response = new HttpCatalogHubRegistrar($this->configuration(), $client)->register($this->payload());

        self::assertSame('source-42', $response->sourceId);
        self::assertSame('active', $response->status);
        self::assertSame('hub-main', $response->jwtKeyId);
        self::assertSame(mb_trim(self::$publicKeyPem), $response->jwtPublicKeyPem);
        self::assertSame(1, $client->getRequestsCount());
    }

    public function testItRejectsVerificationMetadataForAnotherAudience(): void
    {
        $registrar = new HttpCatalogHubRegistrar(
            $this->configuration(),
            new MockHttpClient(new MockResponse(json_encode([
                'instance_id' => self::INSTANCE_ID,
                'source_id' => 'source-42',
                'webhook_url' => 'https://hub.example/webhooks/serenity/source-42',
                'status' => 'active',
                'jwt_issuer' => 'serenity-commerce-hub',
                'jwt_audience' => 'another-audience',
                'jwt_key_id' => 'hub-main',
                'jwt_algorithm' => 'RS256',
                'jwt_public_key' => self::$publicKeyPem,
            ], \JSON_THROW_ON_ERROR), ['http_code' => 200])),
        );

        $this->expectException(CatalogHubRegistrationException::class);
        $registrar->register($this->payload());
    }

    public function testHttp429IsRetryableAndHttp400IsPermanent(): void
    {
        foreach ([429 => true, 400 => false] as $statusCode => $retryable) {
            $registrar = new HttpCatalogHubRegistrar(
                $this->configuration(),
                new MockHttpClient(new MockResponse('{"error":"nope"}', ['http_code' => $statusCode])),
            );

            try {
                $registrar->register($this->payload());
                self::fail('An HTTP error was expected.');
            } catch (CatalogHubRegistrationException $exception) {
                self::assertSame($retryable, $exception->retryable);
                self::assertSame($statusCode, $exception->statusCode);
                self::assertSame(['error' => 'nope'], $exception->response);
            }
        }
    }

    public function testItRejectsAnOversizedHubResponse(): void
    {
        $registrar = new HttpCatalogHubRegistrar(
            $this->configuration(),
            new MockHttpClient(new MockResponse(str_repeat('x', 32_769), ['http_code' => 200])),
        );

        $this->expectException(CatalogHubRegistrationException::class);
        $this->expectExceptionMessage('trop volumineuse');
        $registrar->register($this->payload());
    }

    public function testItRejectsAnHttpWebhookOutsideDebugMode(): void
    {
        $registrar = new HttpCatalogHubRegistrar(
            $this->configuration(false),
            new MockHttpClient(new MockResponse(json_encode([
                'instance_id' => self::INSTANCE_ID,
                'source_id' => 'source-42',
                'webhook_url' => 'http://hub.example/webhooks/serenity/source-42',
                'status' => 'active',
                'jwt_issuer' => 'serenity-commerce-hub',
                'jwt_audience' => 'viticore',
                'jwt_key_id' => 'hub-main',
                'jwt_algorithm' => 'RS256',
                'jwt_public_key' => self::$publicKeyPem,
            ], \JSON_THROW_ON_ERROR), ['http_code' => 200])),
        );

        $this->expectException(CatalogHubRegistrationException::class);
        $this->expectExceptionMessage('URL de webhook invalide');
        $registrar->register($this->payload());
    }

    #[DataProvider('overlongResponseFieldProvider')]
    public function testItRejectsOverlongResponseFields(string $field, string $value): void
    {
        $response = [
            'instance_id' => self::INSTANCE_ID,
            'source_id' => 'source-42',
            'webhook_url' => 'https://hub.example/webhooks/serenity/source-42',
            'status' => 'active',
            'jwt_issuer' => 'serenity-commerce-hub',
            'jwt_audience' => 'viticore',
            'jwt_key_id' => 'hub-main',
            'jwt_algorithm' => 'RS256',
            'jwt_public_key' => self::$publicKeyPem,
        ];
        $response[$field] = $value;
        $registrar = new HttpCatalogHubRegistrar(
            $this->configuration(),
            new MockHttpClient(new MockResponse(json_encode($response, \JSON_THROW_ON_ERROR), ['http_code' => 200])),
        );

        $this->expectException(CatalogHubRegistrationException::class);
        $registrar->register($this->payload());
    }

    /** @return iterable<string, array{string, string}> */
    public static function overlongResponseFieldProvider(): iterable
    {
        yield 'source_id' => ['source_id', str_repeat('s', 256)];
        yield 'webhook_url' => ['webhook_url', 'https://hub.example/'.str_repeat('w', 2048)];
        yield 'status' => ['status', str_repeat('s', 65)];
    }

    private function configuration(bool $allowInsecure = true): CatalogHubConfiguration
    {
        return new CatalogHubConfiguration(
            hubUrl: $allowInsecure ? 'http://127.0.0.1:8085' : 'https://hub.example',
            enrollmentToken: 'long-lived-enrollment-token',
            publicKeyPath: '/unused/public.pem',
            jwtIssuer: 'hub',
            jwtAudience: 'viticore',
            jwtKeyId: 'key-1',
            allowInsecure: $allowInsecure,
        );
    }

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