<?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;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Provider\LocaleProvider;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Commerce\Catalog\CatalogIdentity;
use CpCreation\VitiCore\Commerce\Catalog\CatalogInstanceProjector;
use CpCreation\VitiCore\Commerce\Catalog\CatalogInventoryResolver;
use CpCreation\VitiCore\Commerce\Catalog\CatalogMediaProjector;
use CpCreation\VitiCore\Commerce\Catalog\CatalogProductProjector;
use CpCreation\VitiCore\Commerce\Catalog\CatalogProductReviewsProjector;
use CpCreation\VitiCore\Commerce\Catalog\CatalogPublishabilityResolver;
use CpCreation\VitiCore\Commerce\Catalog\CatalogSellerProjector;
use CpCreation\VitiCore\Commerce\Catalog\CatalogSnapshotHasher;
use CpCreation\VitiCore\Commerce\Catalog\CatalogStorefrontUrlGenerator;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogHubConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogIntegrationConfiguration;
use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogWebhookEndpointResolver;
use CpCreation\VitiCore\Cart\CartService;
use CpCreation\VitiCore\Commerce\Catalog\Api\Resource\CatalogPrice;
use CpCreation\VitiCore\Commerce\Catalog\Api\Resource\CatalogSellerAddress;
use CpCreation\VitiCore\Contact\ContactProvider;
use CpCreation\VitiCore\Contact\Model\Contact;
use CpCreation\VitiCore\Contact\Repository\ContactRepository;
use CpCreation\VitiCore\Marketing\ProductReviewConfigurationProvider;
use CpCreation\VitiCore\Marketing\PublishedProductReviewsProvider;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\CommerceProductType;
use CpCreation\VitiCore\Product\Model\ProductCategory;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Product\Repository\ProductCategoryRepository;
use CpCreation\VitiCore\Product\Repository\ProductCollectionRepository;
use CpCreation\VitiCore\Product\Repository\ProductPriceGroupRepository;
use CpCreation\VitiCore\Product\Repository\ProductRepository;
use CpCreation\VitiCore\Product\ProductVariantSkuResolver;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use CpCreation\VitiCore\Shop\ShopCatalogResolver;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\RouterInterface;

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

    protected function tearDown(): void
    {
        GroupContext::$groupId = null;
    }

    public function testProjectsOneLegalSellerWithLocalizedPolicies(): void
    {
        $projection = $this->instanceProjector()->project(
            new \DateTimeImmutable('2026-07-22T10:30:00+02:00'),
        );

        self::assertSame('2026-07-22T08:30:00+00:00', $projection->generated_at);
        self::assertSame(self::INSTANCE_ID, $projection->instance_id);
        self::assertSame(['fr', 'en'], $projection->locales);
        self::assertSame('Maison Exemple', $projection->seller->display_name);
        self::assertSame('Maison Exemple SAS', $projection->seller->legal_name);
        self::assertSame('FR', $projection->seller->store_country);
        self::assertSame('https://vigneron.example', $projection->seller->website_url);
        self::assertSame('vc_7ad9e145910f4de49cec5f832494adef_seller', $projection->seller->id);
        self::assertEquals(
            new CatalogSellerAddress('1 rue des Vignes', '51100', 'Reims', 'FR'),
            $projection->seller->address,
        );
        self::assertCount(3, $projection->seller->links);
        self::assertSame([
            'fr' => 'https://vigneron.example/fr/private-policy',
            'en' => 'https://vigneron.example/en/private-policy',
        ], $projection->seller->links[0]->urls);
        self::assertNotContains('webhook.notifications', $projection->capabilities);
    }

    public function testNonStockRestrictionsDoNotReportAFalseStockOut(): void
    {
        $cartService = new \ReflectionClass(CartService::class)->newInstanceWithoutConstructor();
        self::assertInstanceOf(CartService::class, $cartService);
        $resolver = new CatalogInventoryResolver($cartService);
        $product = new Product();
        $variant = new ProductVariant();
        $variant->setStock(5);

        $availability = $resolver->resolve(
            $product,
            $variant,
            null,
            false,
            false,
            false,
            new \DateTimeImmutable('2026-07-22T08:30:00+00:00'),
        );

        self::assertSame('in_stock', $availability->status);
        self::assertFalse($availability->orderable);
        self::assertSame(5, $availability->quantity);
        self::assertContains('public_price_missing', $availability->reasons);
        self::assertContains('product_not_published', $availability->reasons);
        self::assertContains('purchase_url_missing', $availability->reasons);

        $variant->setSoldOut(true);
        self::assertSame(
            'out_of_stock',
            $resolver->resolve(
                $product,
                $variant,
                null,
                true,
                true,
                true,
                new \DateTimeImmutable('2026-07-22T08:30:00+00:00'),
            )->status,
        );
    }

    public function testPriceContractExplicitlyUsesMinorUnits(): void
    {
        $price = new CatalogPrice(
            amount_minor: 3250,
            regular_amount_minor: 3500,
            discount_amount_minor: 3250,
            currency: 'EUR',
            minor_unit_exponent: 2,
            tax_included: true,
            publicly_visible: true,
        );

        self::assertSame([
            'amount_minor' => 3250,
            'regular_amount_minor' => 3500,
            'discount_amount_minor' => 3250,
            'currency' => 'EUR',
            'minor_unit_exponent' => 2,
            'tax_included' => true,
            'publicly_visible' => true,
        ], json_decode(json_encode($price, \JSON_THROW_ON_ERROR), true, flags: \JSON_THROW_ON_ERROR));
    }

    public function testAlcoholComplianceRequiresAnAgeRestriction(): void
    {
        GroupContext::$groupId = null;
        $category = new ProductCategory();
        $category->setCommerceExportEnabled(true);
        $category->setCommerceProductType(CommerceProductType::ALCOHOL);
        $product = new Product();
        $product->setCategory($category);
        $product->setSecondImage(null);
        $product->generateTimestampsAtCreation();

        $withoutAge = $this->productProjector()->project($product);

        self::assertTrue($withoutAge->compliance->catalog_export_enabled);
        self::assertSame('alcohol', $withoutAge->compliance->product_type);
        self::assertNull($withoutAge->compliance->age_restriction);
        self::assertTrue($withoutAge->compliance->regulated);
        self::assertContains(
            'age_restriction_missing',
            array_map(static fn($issue): string => $issue->code, $withoutAge->validation->issues),
        );

        $category->setMinimumPurchaseAge(18);
        $withAge = $this->productProjector()->project($product);

        self::assertSame(18, $withAge->compliance->age_restriction);
        self::assertNotContains(
            'age_restriction_missing',
            array_map(static fn($issue): string => $issue->code, $withAge->validation->issues),
        );
    }

    public function testEnabledExportRequiresAnExplicitProductClassification(): void
    {
        GroupContext::$groupId = null;
        $category = new ProductCategory();
        $category->setCommerceExportEnabled(true);
        $product = new Product();
        $product->setCategory($category);
        $product->setSecondImage(null);
        $product->generateTimestampsAtCreation();

        $projection = $this->productProjector()->project($product);

        self::assertSame('unknown', $projection->compliance->product_type);
        self::assertContains(
            'commerce_product_type_missing',
            array_map(static fn($issue): string => $issue->code, $projection->validation->issues),
        );
    }

    public function testProjectionSeparatesLifecycleFromCommercePublishability(): void
    {
        GroupContext::$groupId = null;
        $category = new ProductCategory();
        $product = new Product();
        $product->setCategory($category);
        $product->generateTimestampsAtCreation();

        $projection = $this->productProjector()->project($product);

        self::assertSame('active', $projection->lifecycle->status);
        self::assertSame('not_publishable', $projection->publishability->status);
        self::assertContains(
            'category_export_disabled',
            array_map(static fn($reason): string => $reason->code, $projection->publishability->reasons),
        );
        self::assertIsBool($projection->status->published);
    }

    private function instanceProjector(): CatalogInstanceProjector
    {
        $configuration = new CatalogIntegrationConfiguration(
            enabled: true,
            instanceId: self::INSTANCE_ID,
            apiTokenHash: hash('sha256', 'central-token'),
            currency: 'EUR',
        );
        $identity = new CatalogIdentity($configuration);
        $contactRepository = $this->createMock(ContactRepository::class);
        $contactRepository->method('findOne')->willReturn($this->contact());
        $localeProvider = $this->createStub(LocaleProvider::class);
        $localeProvider->method('getLocales')->willReturn(['fr', 'en']);
        $localeProvider->method('getDefaultLocale')->willReturn('fr');
        $router = $this->createStub(RouterInterface::class);
        $router->method('generate')->willReturnCallback(
            static function (string $route, array $parameters): string {
                $locale = $parameters['_locale'] ?? 'fr';

                return 'shop' === $route
                    ? 'https://vigneron.example/' . $locale . '/shop'
                    : 'https://vigneron.example/' . $locale . '/' . $route;
            },
        );
        $sellerProjector = new CatalogSellerProjector(
            $identity,
            new ContactProvider($contactRepository),
            $localeProvider,
            new CatalogStorefrontUrlGenerator($router),
        );

        return new CatalogInstanceProjector(
            $identity,
            $configuration,
            new CatalogWebhookEndpointResolver(
                $configuration,
                new CatalogHubConfiguration('', '', '', '', '', '', false),
                '',
                '',
            ),
            $sellerProjector,
            $localeProvider,
        );
    }

    private function productProjector(): CatalogProductProjector
    {
        $configuration = new CatalogIntegrationConfiguration(
            enabled: true,
            instanceId: self::INSTANCE_ID,
            apiTokenHash: hash('sha256', 'central-token'),
            currency: 'EUR',
        );
        $identity = new CatalogIdentity($configuration);
        $contactRepository = $this->createMock(ContactRepository::class);
        $contactRepository->method('findOne')->willReturn($this->contact());
        $localeProvider = $this->createStub(LocaleProvider::class);
        $localeProvider->method('getLocales')->willReturn(['fr', 'en']);
        $localeProvider->method('getDefaultLocale')->willReturn('fr');
        $router = $this->createStub(RouterInterface::class);
        $router->method('generate')->willReturnCallback(
            static function (string $route, array $parameters): string {
                $locale = $parameters['_locale'] ?? 'fr';

                return 'https://vigneron.example/' . $locale . '/' . $route;
            },
        );
        $urlGenerator = new CatalogStorefrontUrlGenerator($router);
        $sellerProjector = new CatalogSellerProjector(
            $identity,
            new ContactProvider($contactRepository),
            $localeProvider,
            $urlGenerator,
        );
        $mediaProjector = new \ReflectionClass(CatalogMediaProjector::class)->newInstanceWithoutConstructor();
        self::assertInstanceOf(CatalogMediaProjector::class, $mediaProjector);
        $cartService = new \ReflectionClass(CartService::class)->newInstanceWithoutConstructor();
        self::assertInstanceOf(CartService::class, $cartService);
        $shopConfigRepository = $this->createStub(ShopConfigRepository::class);
        $shopConfigRepository->method('findOne')->willReturn(null);
        $shopCatalogResolver = new ShopCatalogResolver(
            $this->createStub(ProductCategoryRepository::class),
            $this->createStub(ProductRepository::class),
            $this->createStub(ProductCollectionRepository::class),
            $this->createStub(ProductPriceGroupRepository::class),
            $shopConfigRepository,
        );

        return new CatalogProductProjector(
            $identity,
            $configuration,
            $sellerProjector,
            $mediaProjector,
            new CatalogInventoryResolver($cartService),
            $urlGenerator,
            new CatalogSnapshotHasher(),
            new CatalogPublishabilityResolver(),
            $localeProvider,
            $shopConfigRepository,
            $shopCatalogResolver,
            new ProductVariantSkuResolver(),
            $this->disabledReviewsProjector(),
        );
    }

    private function disabledReviewsProjector(): CatalogProductReviewsProjector
    {
        $configuration = new class implements ProductReviewConfigurationProvider {
            public function isEnabled(): bool
            {
                return false;
            }

            public function isModerationRequired(): bool
            {
                return true;
            }
        };
        $reviews = new class implements PublishedProductReviewsProvider {
            public function findPublishedByProduct(
                Product $product,
                ?\DateTimeImmutable $now = null,
            ): array {
                return [];
            }

            public function findPublishedByProducts(
                iterable $products,
                ?\DateTimeImmutable $now = null,
            ): array {
                return [];
            }
        };

        return new CatalogProductReviewsProjector($configuration, $reviews);
    }

    private function contact(): Contact
    {
        $contact = new Contact();
        $contact->setCompany('Maison Exemple');
        $contact->setEditorCompany('Maison Exemple SAS');
        $contact->setAddress('1 rue des Vignes');
        $contact->setPostal('51100');
        $contact->setCity('Reims');
        $contact->setCountry('FR');

        return $contact;
    }
}
