<?php

/*
 * 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\Seed;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Seed\CoreSeed;
use CpCreation\VitiCore\Contact\Model\Contact;
use CpCreation\VitiCore\Marketing\Model\ProductReview;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductCollection;
use CpCreation\VitiCore\Product\Model\ProductCollectionItem;
use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Seed\SeedPayload;
use CpCreation\VitiCore\Shop\Model\ShippingConfig;
use CpCreation\VitiCore\Shop\Model\ShopConfig;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Nelmio\Alice\Loader\NativeLoader;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Yaml\Yaml;

final class CoreSeedTest extends TestCase
{
    #[DataProvider('pendingSeedVersionProvider')]
    public function testPendingVersionFillsEmptyLegalContents(int $fromVersion): void
    {
        $contact = new Contact();
        $contact->setCompany('Maison Exemple');
        $contact->setEditorCompany('Éditeur Exemple');
        $contact->setEditorAddress('1 rue des Vignes');
        $contact->setEditorPostal('51200');
        $contact->setEditorCity('Épernay');
        $contact->setEditorRcs('123 456 789');
        $contact->setEditorTva('FR00123456789');

        $seed = new CoreSeed($this->createEntityManagerForContact($contact));
        $seed->seed(new SeedPayload('run-id', null, 'core'), $fromVersion);

        self::assertSame(5, $seed->version());
        self::assertStringContainsString("Article 1 : Champ d'application", (string) $contact->getCgv());
        self::assertStringContainsString('directive 2011/83/UE', (string) $contact->getCgv());
        self::assertStringContainsString('directive (UE) 2023/2673', (string) $contact->getCgv());
        self::assertStringContainsString('href="/droit-de-retractation"', (string) $contact->getCgv());
        self::assertStringContainsString('Édition du site internet', (string) $contact->getLegalNoticeContent());
        self::assertStringContainsString('{{ contact.companyUpper }}', (string) $contact->getLegalNoticeContent());
        self::assertStringContainsString('Introduction', (string) $contact->getPrivacyPolicyContent());
    }

    public function testPendingVersionDoesNotOverwriteManagedLegalContents(): void
    {
        $contact = new Contact();
        $contact->setCgv('<p>CGV administrées</p>');
        $contact->setLegalNoticeContent('<p>Mentions administrées</p>');
        $contact->setPrivacyPolicyContent('<p>Politique administrée</p>');

        $seed = new CoreSeed($this->createEntityManagerForContact($contact));
        $seed->seed(new SeedPayload('run-id', null, 'core'), 2);

        self::assertSame('<p>CGV administrées</p>', $contact->getCgv());
        self::assertSame('<p>Mentions administrées</p>', $contact->getLegalNoticeContent());
        self::assertSame('<p>Politique administrée</p>', $contact->getPrivacyPolicyContent());
    }

    public function testDevelopmentFixtureContainsLegalContents(): void
    {
        $fixture = Yaml::parseFile(\dirname(__DIR__, 2) . '/src/fixtures/15_Contact.yaml');
        $contact = $fixture[Contact::class]['contact_contact'];

        self::assertSame('@image__logo_email', $contact['image']);
        self::assertStringContainsString("Article 1 : Champ d'application", $contact['cgv']);
        self::assertStringContainsString('directive 2011/83/UE', $contact['cgv']);
        self::assertStringContainsString('directive (UE) 2023/2673', $contact['cgv']);
        self::assertStringContainsString('href="/droit-de-retractation"', $contact['cgv']);
        self::assertStringContainsString('Édition du site internet', $contact['legalNoticeContent']);
        self::assertStringContainsString('{{ contact.companyUpper }}', $contact['legalNoticeContent']);
        self::assertStringContainsString('Introduction', $contact['privacyPolicyContent']);
    }

    public function testDevelopmentFixtureContainsChampagnesCollectionWithAllProducts(): void
    {
        $fixtureDirectory = \dirname(__DIR__, 2) . '/src/fixtures';
        $productFixture = Yaml::parseFile($fixtureDirectory . '/10_Product.yaml');
        $collectionFixture = Yaml::parseFile($fixtureDirectory . '/11_ProductCollection.yaml');
        $collection = $collectionFixture[ProductCollection::class]['product_collection__champagnes'];
        $items = $collectionFixture[ProductCollectionItem::class];

        self::assertSame('Nos champagnes', $collection['name']);
        self::assertSame('champagnes', $collection['code']);
        self::assertSame(ProductCollection::MODE_MANUAL, $collection['mode']);
        self::assertSame(array_keys($items), array_map(
            static fn(string $reference): string => mb_ltrim($reference, '@'),
            $collection['items'],
        ));
        self::assertSame(
            array_map(static fn(string $reference): string => '@' . $reference, array_keys($productFixture[Product::class])),
            array_column($items, 'product'),
        );
        self::assertSame(array_fill(0, \count($items), '@product_collection__champagnes'), array_column($items, 'productCollection'));
        self::assertSame(range(0, \count($items) - 1), array_column($items, 'position'));

        $products = [];
        foreach (array_keys($productFixture[Product::class]) as $productReference) {
            $products[$productReference] = new Product();
        }
        $objects = new NativeLoader()->loadFile(
            $fixtureDirectory . '/11_ProductCollection.yaml',
            objects: $products,
        )->getObjects();

        $collectionObject = $objects['product_collection__champagnes'];
        self::assertInstanceOf(ProductCollection::class, $collectionObject);
        self::assertSame('Nos champagnes', $collectionObject->getName());
        self::assertSame('champagnes', $collectionObject->getCode());
        self::assertCount(\count($products), $collectionObject->getItems());
        self::assertSame(array_values($products), $collectionObject->getProducts()->toArray());
    }

    public function testDevelopmentFixtureContainsFiveStarReviewForEveryCuvee(): void
    {
        $fixtureDirectory = \dirname(__DIR__, 2) . '/src/fixtures';
        $productFixture = Yaml::parseFile($fixtureDirectory . '/10_Product.yaml');
        $reviewFixture = Yaml::parseFile($fixtureDirectory . '/70_ProductReview.yaml');
        $productReferences = array_keys($productFixture[Product::class]);
        $reviews = $reviewFixture[ProductReview::class];
        $reviewProductReferences = array_values(array_map(
            static fn(array $review): string => mb_ltrim($review['__construct'][1], '@'),
            $reviews,
        ));
        $reviewCounts = array_count_values($reviewProductReferences);

        self::assertSame(
            $productReferences,
            array_keys($reviewCounts),
        );
        self::assertSame(
            array_fill(0, \count($reviews), 5),
            array_values(array_map(static fn(array $review): int => $review['__construct'][3], $reviews)),
        );
        self::assertContains(1, $reviewCounts);
        self::assertContains(2, $reviewCounts);
        self::assertContains(3, $reviewCounts);
        foreach ($reviewCounts as $reviewCount) {
            self::assertGreaterThanOrEqual(1, $reviewCount);
            self::assertLessThanOrEqual(3, $reviewCount);
        }

        $products = [];
        foreach ($productReferences as $productReference) {
            $products[$productReference] = new Product();
        }

        $objects = new NativeLoader()->loadFile(
            $fixtureDirectory . '/70_ProductReview.yaml',
            objects: $products,
        )->getObjects();
        $reviewObjects = array_values(array_filter(
            $objects,
            static fn(object $object): bool => $object instanceof ProductReview,
        ));

        self::assertCount(\count($reviews), $reviewObjects);
        foreach ($reviewObjects as $review) {
            self::assertSame(5, $review->getRating());
            self::assertSame(ProductReview::STATUS_PUBLISHED, $review->getStatus());
        }
    }

    public function testDevelopmentFixtureContainsStableProductTypeCodes(): void
    {
        $fixture = Yaml::parseFile(\dirname(__DIR__, 2) . '/src/fixtures/05_ProductType.yaml');

        self::assertSame([
            'DEMI-BOUTEILLE',
            'BOUTEILLE',
            'MAGNUM',
            'JEROBOAM',
            'REHOBOAM',
            'MATHUSALEM',
            'COFFRET-PRESTIGE',
            'COFFRET-DEGUSTATION',
            'BOUTEILLE-ETUI',
        ], array_column($fixture[ProductType::class], 'code'));
    }

    public function testDevelopmentFixtureEnablesProductPagesByDefault(): void
    {
        $fixture = Yaml::parseFile(\dirname(__DIR__, 2) . '/src/fixtures/50_ShopConfig.yaml');

        self::assertTrue($fixture[ShopConfig::class]['shop_config']['enableProductPageFront']);
    }

    public function testPendingVersionCreatesEmptyChampagnesCollection(): void
    {
        $repository = $this->createProductCollectionRepository(null);
        $persistedCollection = null;
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->with(ProductCollection::class)
            ->willReturn($repository);
        $entityManager->expects(self::once())
            ->method('persist')
            ->willReturnCallback(static function (object $entity) use (&$persistedCollection): void {
                $persistedCollection = $entity;
            });

        $seed = new CoreSeed($entityManager);
        $seed->seed(new SeedPayload('run-id', null, 'core'), 4);

        self::assertInstanceOf(ProductCollection::class, $persistedCollection);
        self::assertSame('Nos champagnes', $persistedCollection->getName());
        self::assertSame('champagnes', $persistedCollection->getCode());
        self::assertTrue($persistedCollection->isManual());
        self::assertCount(0, $persistedCollection->getItems());
    }

    public function testPendingVersionKeepsExistingChampagnesCollectionUntouched(): void
    {
        $collection = new ProductCollection();
        $collection->setName('Collection administrée');
        $collection->setCode('champagnes');
        $collection->addProduct(new Product());

        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->with(ProductCollection::class)
            ->willReturn($this->createProductCollectionRepository($collection));
        $entityManager->expects(self::never())->method('persist');

        $seed = new CoreSeed($entityManager);
        $seed->seed(new SeedPayload('run-id', null, 'core'), 4);

        self::assertSame('Collection administrée', $collection->getName());
        self::assertCount(1, $collection->getItems());
    }

    public function testPendingVersionCreatesDefaultShippingConfig(): void
    {
        $repository = $this->createMock(EntityRepository::class);
        $repository->method('findOneBy')
            ->with([])
            ->willReturn(null);

        $persistedConfig = null;
        $collectionRepository = $this->createProductCollectionRepository(new ProductCollection());
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->willReturnMap([
                [ShippingConfig::class, $repository],
                [ProductCollection::class, $collectionRepository],
            ]);
        $entityManager->expects(self::once())
            ->method('persist')
            ->willReturnCallback(static function (object $entity) use (&$persistedConfig): void {
                $persistedConfig = $entity;
            });

        $seed = new CoreSeed($entityManager);
        $seed->seed(new SeedPayload('run-id', null, 'core'), 3);

        self::assertInstanceOf(ShippingConfig::class, $persistedConfig);
        self::assertSame(1000, $persistedConfig->getAmountMinVDManu());
        self::assertSame(0.02, $persistedConfig->getMarginManu());
        self::assertSame(0.2, $persistedConfig->getTvaManu());
        self::assertSame(
            'Livraison dans un délai de 15 jours maximum.',
            $persistedConfig->translate('fr')->getShippingText(),
        );
        self::assertSame(
            'Delivery within 15 days maximum.',
            $persistedConfig->translate('en')->getShippingText(),
        );
    }

    public function testPendingVersionDoesNotOverwriteManagedShippingConfig(): void
    {
        $shippingConfig = new ShippingConfig();
        $shippingConfig->translate('fr')->setShippingText('Texte administré');

        $repository = $this->createMock(EntityRepository::class);
        $repository->method('findOneBy')
            ->with([])
            ->willReturn($shippingConfig);

        $collectionRepository = $this->createProductCollectionRepository(new ProductCollection());
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->willReturnMap([
                [ShippingConfig::class, $repository],
                [ProductCollection::class, $collectionRepository],
            ]);
        $entityManager->expects(self::never())->method('persist');

        $seed = new CoreSeed($entityManager);
        $seed->seed(new SeedPayload('run-id', null, 'core'), 3);

        self::assertSame('Texte administré', $shippingConfig->translate('fr')->getShippingText());
    }

    public function testPendingVersionFillsOnlyEmptyShippingTexts(): void
    {
        $shippingConfig = new ShippingConfig();
        $shippingConfig->translate('fr')->setShippingText('   ');
        $shippingConfig->translate('en')->setShippingText('Managed text');

        $repository = $this->createMock(EntityRepository::class);
        $repository->method('findOneBy')
            ->with([])
            ->willReturn($shippingConfig);

        $collectionRepository = $this->createProductCollectionRepository(new ProductCollection());
        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->willReturnMap([
                [ShippingConfig::class, $repository],
                [ProductCollection::class, $collectionRepository],
            ]);
        $entityManager->expects(self::never())->method('persist');

        $seed = new CoreSeed($entityManager);
        $seed->seed(new SeedPayload('run-id', null, 'core'), 3);

        self::assertSame(
            'Livraison dans un délai de 15 jours maximum.',
            $shippingConfig->translate('fr')->getShippingText(),
        );
        self::assertSame('Managed text', $shippingConfig->translate('en')->getShippingText());
    }

    public function testDevelopmentFixtureContainsDefaultShippingText(): void
    {
        $fixture = Yaml::parseFile(\dirname(__DIR__, 2) . '/src/fixtures/26_ShippingConfig.yaml');
        $translations = $fixture['CpCreation\\VitiCore\\Shop\\Model\\Translation\\ShippingConfigTranslation'];

        self::assertSame(
            'Livraison dans un délai de 15 jours maximum.',
            $translations['shipping_config_fr']['shippingText'],
        );
        self::assertSame(
            'Delivery within 15 days maximum.',
            $translations['shipping_config_en']['shippingText'],
        );
    }

    /**
     * @return iterable<string, array{int}>
     */
    public static function pendingSeedVersionProvider(): iterable
    {
        yield 'base actuellement en version 1' => [1];
        yield 'base historique déjà en version 2' => [2];
    }

    private function createEntityManagerForContact(Contact $contact): EntityManagerInterface
    {
        $contactRepository = $this->createMock(EntityRepository::class);
        $contactRepository->method('findOneBy')
            ->with([])
            ->willReturn($contact);

        $shippingConfigRepository = $this->createMock(EntityRepository::class);
        $shippingConfigRepository->method('findOneBy')
            ->with([])
            ->willReturn(new ShippingConfig());
        $productCollectionRepository = $this->createProductCollectionRepository(null);

        $entityManager = $this->createMock(EntityManagerInterface::class);
        $entityManager->method('getRepository')
            ->willReturnMap([
                [Contact::class, $contactRepository],
                [ShippingConfig::class, $shippingConfigRepository],
                [ProductCollection::class, $productCollectionRepository],
            ]);

        return $entityManager;
    }

    private function createProductCollectionRepository(?ProductCollection $collection): EntityRepository
    {
        $repository = $this->createMock(EntityRepository::class);
        $repository->method('findOneBy')
            ->with(['code' => 'champagnes'])
            ->willReturn($collection);

        return $repository;
    }
}
