<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

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

use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogSecretCipher;
use CpCreation\VitiCore\Commerce\Catalog\Model\CatalogConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Model\CatalogConfigurationStatus;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;

final class CatalogConfigurationTest extends TestCase
{
    private CatalogSecretCipher $cipher;

    protected function setUp(): void
    {
        $this->cipher = new CatalogSecretCipher('test-app-secret-with-sufficient-entropy');
    }

    public function testItStartsAsAStableDisabledSingleton(): void
    {
        $instanceId = Uuid::uuid7();
        $configuration = new CatalogConfiguration($instanceId);

        self::assertSame(CatalogConfiguration::SINGLETON_ID, $configuration->getId());
        self::assertSame($instanceId, $configuration->getInstanceId());
        self::assertFalse($configuration->isEnabled());
        self::assertFalse($configuration->isMonitoringEnabled());
        self::assertFalse($configuration->isHubConnectionEnabled());
        self::assertSame('EUR', $configuration->getCurrency());
        self::assertSame('serenity:catalog', $configuration->getAuthAudience());
        self::assertSame(CatalogConfigurationStatus::DISABLED, $configuration->getStatus());
    }

    public function testActivationGeneratesAndKeepsAWebhookSecret(): void
    {
        $configuration = new CatalogConfiguration();
        $configuration->setHubUrl('https://hub.example.test/');
        $configuration->prepareActivation($this->cipher);
        $secret = $configuration->revealWebhookSecret($this->cipher);

        self::assertTrue($configuration->isEnabled());
        self::assertSame('https://hub.example.test', $configuration->getHubUrl());
        self::assertSame(CatalogConfigurationStatus::PENDING_AUTHORIZATION, $configuration->getStatus());
        self::assertIsString($secret);
        self::assertGreaterThanOrEqual(43, strlen($secret));

        $configuration->prepareActivation($this->cipher);
        self::assertSame($secret, $configuration->revealWebhookSecret($this->cipher));
    }

    public function testMonitoringCanKeepTheHubConnectionActiveWithoutTheCatalog(): void
    {
        $configuration = new CatalogConfiguration();
        $configuration->setHubUrl('https://hub.example.test');
        $configuration->setMonitoringEnabled(true);
        $configuration->prepareHubConnection($this->cipher);

        self::assertFalse($configuration->isEnabled());
        self::assertTrue($configuration->isMonitoringEnabled());
        self::assertTrue($configuration->isHubConnectionEnabled());
        self::assertNotNull($configuration->revealWebhookSecret($this->cipher));
        self::assertSame(CatalogConfigurationStatus::PENDING_AUTHORIZATION, $configuration->getStatus());

        $configuration->disable();
        self::assertTrue($configuration->isHubConnectionEnabled());

        $configuration->disableHubConnection();
        self::assertFalse($configuration->isHubConnectionEnabled());
        self::assertSame(CatalogConfigurationStatus::DISABLED, $configuration->getStatus());
    }

    public function testItStoresOnlyAHashedStateAndAnEncryptedPkceVerifier(): void
    {
        $configuration = new CatalogConfiguration();
        $now = new \DateTimeImmutable('2030-01-01 12:00:00 UTC');
        $state = str_repeat('s', 64);
        $verifier = str_repeat('v', 64);

        $configuration->beginAuthorization($state, $verifier, $now->modify('+10 minutes'), $this->cipher);

        self::assertTrue($configuration->matchesAuthorizationState($state, $now));
        self::assertFalse($configuration->matchesAuthorizationState(str_repeat('x', 64), $now));
        self::assertFalse($configuration->matchesAuthorizationState($state, $now->modify('+11 minutes')));
        self::assertSame($verifier, $configuration->revealAuthorizationPkceVerifier($this->cipher, $now));
        self::assertNull($configuration->revealAuthorizationPkceVerifier($this->cipher, $now->modify('+11 minutes')));
    }

    public function testAuthorizationExpirationSurvivesADoctrineRoundTripOutsideUtc(): void
    {
        $previousTimezone = date_default_timezone_get();
        date_default_timezone_set('Europe/Paris');

        try {
            $configuration = new CatalogConfiguration();
            $now = new \DateTimeImmutable('2030-07-01 12:00:00 UTC');
            $state = str_repeat('s', 64);
            $configuration->beginAuthorization(
                $state,
                str_repeat('v', 64),
                $now->modify('+10 minutes'),
                $this->cipher,
            );

            $property = new \ReflectionProperty(CatalogConfiguration::class, 'authorizationExpiresAt');
            $column = $property->getAttributes(Column::class)[0]->newInstance();
            $type = Type::getType($column->type ?? Types::DATETIME_IMMUTABLE);
            $platform = new PostgreSQLPlatform();
            $databaseValue = $type->convertToDatabaseValue($configuration->getAuthorizationExpiresAt(), $platform);
            $property->setValue($configuration, $type->convertToPHPValue($databaseValue, $platform));

            self::assertTrue($configuration->matchesAuthorizationState($state, $now));
        } finally {
            date_default_timezone_set($previousTimezone);
        }
    }

    public function testHubAuthorizationAndEnrollmentExposeOnlyExplicitBusinessData(): void
    {
        $configuration = new CatalogConfiguration();
        $configuration->setHubUrl('https://hub.example.test');
        $configuration->prepareActivation($this->cipher);
        $configuration->beginAuthorization(
            str_repeat('s', 64),
            str_repeat('v', 64),
            new \DateTimeImmutable('2030-01-01 12:10:00 UTC'),
            $this->cipher,
        );
        $tokenExpiresAt = new \DateTimeImmutable('2031-01-01 00:00:00 UTC');
        $configuration->applyHubAuthorization(
            'header.payload.signature',
            $tokenExpiresAt,
            'token-jti',
            "-----BEGIN PUBLIC KEY-----\ntest\n-----END PUBLIC KEY-----",
            'serenity-commerce-hub',
            'hub-main',
            $this->cipher,
        );

        self::assertSame('header.payload.signature', $configuration->revealEnrollmentToken($this->cipher));
        self::assertSame($tokenExpiresAt, $configuration->getEnrollmentTokenExpiresAt());
        self::assertSame('token-jti', $configuration->getEnrollmentTokenJti());
        self::assertSame('serenity-commerce-hub', $configuration->getJwtIssuer());
        self::assertSame('hub-main', $configuration->getJwtKeyId());
        self::assertSame(CatalogConfigurationStatus::PENDING_ENROLLMENT, $configuration->getStatus());
        self::assertNull($configuration->getAuthorizationExpiresAt());

        $enrolledAt = new \DateTimeImmutable('2030-01-02 00:00:00 UTC');
        $configuration->markEnrolled('source-123', 'https://hub.example.test/webhook', $enrolledAt);

        self::assertSame('source-123', $configuration->getSourceId());
        self::assertSame('https://hub.example.test/webhook', $configuration->getWebhookUrl());
        self::assertSame($enrolledAt, $configuration->getEnrolledAt());
        self::assertSame(CatalogConfigurationStatus::ENROLLED, $configuration->getStatus());
    }

    public function testDisablingKeepsTheImmutableInstallationIdentity(): void
    {
        $configuration = new CatalogConfiguration();
        $instanceId = $configuration->getInstanceId();
        $configuration->setHubUrl('https://hub.example.test');
        $configuration->prepareActivation($this->cipher);

        $configuration->disable();

        self::assertFalse($configuration->isEnabled());
        self::assertSame(CatalogConfigurationStatus::DISABLED, $configuration->getStatus());
        self::assertSame($instanceId, $configuration->getInstanceId());
    }

    public function testChangingHubConnectionRotatesTheWebhookSecret(): void
    {
        $configuration = new CatalogConfiguration();
        $configuration->setHubUrl('https://first-hub.example.test');
        $configuration->prepareActivation($this->cipher);
        $firstSecret = $configuration->revealWebhookSecret($this->cipher);

        $configuration->setHubUrl('https://second-hub.example.test');
        $configuration->resetConnection();
        self::assertNull($configuration->revealWebhookSecret($this->cipher));

        $configuration->prepareActivation($this->cipher);
        self::assertNotSame($firstSecret, $configuration->revealWebhookSecret($this->cipher));
        self::assertSame(CatalogConfigurationStatus::PENDING_AUTHORIZATION, $configuration->getStatus());
    }
}
