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

use CpCreation\VitiCore\Bridge\Symfony\Bundle\LocalizedRouting\LocalizedPageRoute;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\LocalizedRouting\LocalizedPageRoutes;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Yaml\Yaml;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
use Twig\Loader\ChainLoader;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;
use Twig\TwigFunction;

final class LocalizedViewRoutingTest extends TestCase
{
    private const string REQUEST_FALLBACK_URL = 'https://request.example.test/url-not-canonical';

    public function testSeoPartialRendersLocalizedCanonicalAlternatesAndOpenGraphLocales(): void
    {
        $routes = new LocalizedPageRoutes('en', 'fr', [
            'fr' => $this->route('fr', '/fr/page', 'https://localized.example.test/fr/page'),
            'en' => $this->route('en', '/en/page', 'https://localized.example.test/en/page'),
            'de' => $this->route('de', '/de/seite', 'https://localized.example.test/de/seite'),
        ]);

        $html = $this->twig($routes)->render('partials/seo.html.twig', [
            'title_key' => 'page.title',
            'description_key' => 'page.description',
        ]);
        $crawler = $this->headCrawler($html);

        self::assertSame(
            'https://localized.example.test/en/page',
            $crawler->filter('link[rel="canonical"]')->attr('href'),
        );
        self::assertSame(
            'https://localized.example.test/fr/page',
            $crawler->filter('link[rel="alternate"][hreflang="fr"]')->attr('href'),
        );
        self::assertSame(
            'https://localized.example.test/en/page',
            $crawler->filter('link[rel="alternate"][hreflang="en"]')->attr('href'),
        );
        self::assertSame(
            'https://localized.example.test/de/seite',
            $crawler->filter('link[rel="alternate"][hreflang="de"]')->attr('href'),
        );
        self::assertSame(
            'https://localized.example.test/fr/page',
            $crawler->filter('link[rel="alternate"][hreflang="x-default"]')->attr('href'),
        );
        self::assertSame(
            'https://localized.example.test/en/page',
            $crawler->filter('meta[property="og:url"]')->attr('content'),
        );
        self::assertSame('en_GB', $crawler->filter('meta[property="og:locale"]')->attr('content'));
        self::assertSame(
            ['fr_FR', 'de_DE'],
            $crawler->filter('meta[property="og:locale:alternate"]')->each(
                static fn (Crawler $node): ?string => $node->attr('content'),
            ),
        );
    }

    public function testSeoPartialFallsBackToTheRequestUrlWithoutACurrentLocalizedRoute(): void
    {
        $routes = new LocalizedPageRoutes('es', 'fr', [
            'fr' => $this->route('fr', '/fr/page', 'https://localized.example.test/fr/page'),
        ]);

        $html = $this->twig($routes, locale: 'es')->render('partials/seo.html.twig', [
            'title_key' => 'page.title',
            'description_key' => 'page.description',
        ]);
        $crawler = $this->headCrawler($html);

        self::assertSame(
            self::REQUEST_FALLBACK_URL,
            $crawler->filter('link[rel="canonical"]')->attr('href'),
        );
        self::assertSame(
            self::REQUEST_FALLBACK_URL,
            $crawler->filter('meta[property="og:url"]')->attr('content'),
        );
        self::assertCount(0, $crawler->filter('meta[property="og:locale"]'));
    }

    public function testLanguageSwitcherLinksEveryAvailableLocaleExceptTheCurrentOne(): void
    {
        $routes = new LocalizedPageRoutes('en', 'fr', [
            'fr' => $this->route('fr', '/fr/page', 'https://localized.example.test/fr/page'),
            'en' => $this->route('en', '/en/page', 'https://localized.example.test/en/page'),
            'de' => $this->route('de', '/de/seite', 'https://localized.example.test/de/seite'),
        ]);

        $html = $this->twig($routes)->render('partials/_language_switcher.html.twig');
        $crawler = new Crawler('<html><body>'.$html.'</body></html>');

        self::assertSame('EN', $crawler->filter('span[lang="en"][aria-current="page"]')->text());
        self::assertCount(0, $crawler->filter('a[lang="en"]'));
        self::assertSame('/fr/page', $crawler->filter('a[lang="fr"]')->attr('href'));
        self::assertSame('fr', $crawler->filter('a[lang="fr"]')->attr('hreflang'));
        self::assertSame('français', $crawler->filter('a[lang="fr"]')->attr('aria-label'));
        self::assertSame('/de/seite', $crawler->filter('a[lang="de"]')->attr('href'));
        self::assertSame('de', $crawler->filter('a[lang="de"]')->attr('hreflang'));
        self::assertSame('Deutsch', $crawler->filter('a[lang="de"]')->attr('aria-label'));
        self::assertCount(2, $crawler->filter('a[hreflang]'));
        self::assertCount(0, $crawler->filter('[lang="nl"]'));
    }

    public function testDefaultNotFoundViewKeepsTheSharedErrorContract(): void
    {
        $routes = new LocalizedPageRoutes('fr', 'fr', []);

        $html = $this->twig($routes, locale: 'fr')->render('bundles/TwigBundle/Exception/error404.html.twig');
        $crawler = new Crawler($html);

        self::assertSame('fr', $crawler->filter('html')->attr('lang'));
        self::assertSame('noindex,follow', $crawler->filter('meta[name="robots"]')->attr('content'));
        self::assertSame('error_page.not_found.title', $crawler->filter('h1#error-title')->text());
        self::assertSame('/homepage', $crawler->filter('a')->attr('href'));
        self::assertSame('error_page.not_found.back_home', $crawler->filter('a')->text());
        self::assertStringNotContainsString('http-equiv="refresh"', $html);
        self::assertStringNotContainsString('window.location', $html);
    }

    public function testDefaultNotFoundCopyExistsInEveryCoreLocale(): void
    {
        $translationsDirectory = \dirname(__DIR__, 2).'/src/Bridge/Symfony/Bundle/Resources/translations';

        foreach (['fr', 'en', 'nl', 'de', 'es', 'it'] as $locale) {
            $messages = Yaml::parseFile($translationsDirectory.'/messages.'.$locale.'.yaml');
            self::assertIsArray($messages);

            foreach (['title', 'description', 'back_home'] as $key) {
                self::assertNotSame(
                    '',
                    mb_trim((string) ($messages['error_page']['not_found'][$key] ?? '')),
                    \sprintf('La traduction error_page.not_found.%s manque pour la locale %s.', $key, $locale),
                );
            }
        }
    }

    public function testProductPageUsesTheCurrentLocalizedUrlInMetadataAndStructuredData(): void
    {
        $localizedProductUrl = 'https://localized.example.test/en/shop/champagnes/cuvee-brut';
        $routes = new LocalizedPageRoutes('en', 'fr', [
            'fr' => $this->route(
                'fr',
                '/boutique/champagnes/cuvee-brut',
                'https://localized.example.test/boutique/champagnes/cuvee-brut',
            ),
            'en' => $this->route('en', '/en/shop/champagnes/cuvee-brut', $localizedProductUrl),
        ]);
        $variant = new class {
            public bool $unPublished = false;
            public bool $soldOut = false;
            public bool $available = true;
            public string $fullName = 'Cuvée Brut – 75 cl';
            public string $label = '75 cl';
            public int $stock = 12;

            public function getPriceByGroupId(mixed $groupId): object
            {
                return (object) ['finalPrice' => 4250];
            }
        };
        $product = (object) [
            'backgroundImage' => null,
            'fullName' => 'Cuvée Brut',
            'name' => 'Brut',
            'description' => '<p>Champagne brut.</p>',
            'secondImage' => null,
            'image' => (object) ['path' => 'products/cuvee-brut.jpg'],
            'soldOut' => false,
            'variants' => [$variant],
            'sku' => 'CUVEE-BRUT',
        ];
        $category = (object) ['name' => 'Champagnes', 'slug' => 'champagnes'];
        $config = new class {
            public function shouldShowPrices(mixed $partnerCode): bool
            {
                return true;
            }
        };

        $html = $this->twig($routes)->render('shop/product.html.twig', [
            'product' => $product,
            'category' => $category,
            'config' => $config,
            'activePriceGroupId' => 'public-price-group',
        ]);
        $crawler = $this->headCrawler($html);
        $schema = $this->jsonLdSchema($crawler);

        self::assertSame($localizedProductUrl, $crawler->filter('link[rel="canonical"]')->attr('href'));
        self::assertSame($localizedProductUrl, $crawler->filter('meta[property="og:url"]')->attr('content'));
        self::assertSame('product', $crawler->filter('meta[property="og:type"]')->attr('content'));
        self::assertSame($localizedProductUrl, $schema['url'] ?? null);
        self::assertSame($localizedProductUrl, $schema['offers'][0]['url'] ?? null);
    }

    public function testCatalogStructuredDataUsesTheCurrentLocalizedPageUrl(): void
    {
        $localizedCatalogUrl = 'https://localized.example.test/en/shop/champagnes';
        $routes = new LocalizedPageRoutes('en', 'fr', [
            'fr' => $this->route(
                'fr',
                '/boutique/champagnes',
                'https://localized.example.test/boutique/champagnes',
            ),
            'en' => $this->route('en', '/en/shop/champagnes', $localizedCatalogUrl),
        ]);

        $schema = $this->renderCatalogSchema($routes);

        self::assertSame($localizedCatalogUrl, $schema['url'] ?? null);
        self::assertSame(
            $localizedCatalogUrl.'#cuvee-brut',
            $schema['mainEntity']['itemListElement'][0]['url'] ?? null,
        );
    }

    public function testCatalogStructuredDataResolvesTheRenderedCatalogInsteadOfTheCurrentPage(): void
    {
        $currentPageRoutes = new LocalizedPageRoutes('en', 'fr', [
            'en' => $this->route('en', '/en/about', 'https://localized.example.test/en/about'),
        ]);
        $catalogRoutes = new LocalizedPageRoutes('en', 'fr', [
            'en' => $this->route(
                'en',
                '/en/shop/champagnes',
                'https://localized.example.test/en/shop/champagnes',
            ),
        ]);
        $resolvedRouteName = null;
        $resolvedParameters = [];
        $localizedRoutesResolver = static function (
            ?string $routeName = null,
            array $parameters = [],
        ) use (
            $currentPageRoutes,
            $catalogRoutes,
            &$resolvedRouteName,
            &$resolvedParameters,
        ): LocalizedPageRoutes {
            if (null === $routeName) {
                return $currentPageRoutes;
            }

            $resolvedRouteName = $routeName;
            $resolvedParameters = $parameters;

            return $catalogRoutes;
        };

        $schema = $this->renderCatalogSchema($currentPageRoutes, $localizedRoutesResolver);

        self::assertSame('shop', $resolvedRouteName);
        self::assertSame(['slug' => 'champagnes'], $resolvedParameters);
        self::assertSame('https://localized.example.test/en/shop/champagnes', $schema['url'] ?? null);
        self::assertNotSame('https://localized.example.test/en/about', $schema['url'] ?? null);
    }

    public function testCatalogStructuredDataKeepsTheGeneratedRouteFallback(): void
    {
        $routes = new LocalizedPageRoutes('en', 'fr', []);

        $schema = $this->renderCatalogSchema($routes);

        self::assertSame('https://fallback.example.test/fallback-shop/champagnes', $schema['url'] ?? null);
        self::assertSame(
            'https://fallback.example.test/fallback-shop/champagnes#cuvee-brut',
            $schema['mainEntity']['itemListElement'][0]['url'] ?? null,
        );
    }

    private function route(string $locale, string $path, string $absoluteUrl): LocalizedPageRoute
    {
        $languageNames = [
            'fr' => 'français',
            'en' => 'English',
            'de' => 'Deutsch',
        ];
        $openGraphLocales = [
            'fr' => 'fr_FR',
            'en' => 'en_GB',
            'de' => 'de_DE',
        ];

        return new LocalizedPageRoute(
            locale: $locale,
            languageName: $languageNames[$locale],
            hreflang: $locale,
            openGraphLocale: $openGraphLocales[$locale],
            path: $path,
            absoluteUrl: $absoluteUrl,
        );
    }

    private function twig(
        LocalizedPageRoutes $localizedRoutes,
        string $requestUri = self::REQUEST_FALLBACK_URL,
        string $locale = 'en',
        ?callable $localizedRoutesResolver = null,
    ): Environment {
        $viewsDirectory = \dirname(__DIR__, 2).'/src/Bridge/Symfony/Bundle/Resources/views';
        $loader = new ChainLoader([
            new ArrayLoader([
                'base.html.twig' => '{% block meta %}{% endblock %}',
            ]),
            new FilesystemLoader($viewsDirectory),
        ]);
        $twig = new Environment($loader, [
            'autoescape' => 'html',
            'strict_variables' => true,
        ]);

        $request = Request::create($requestUri);
        $request->setLocale($locale);
        $app = new \stdClass();
        $app->request = $request;
        $app->environment = 'dev';
        $app->session = new class {
            public function get(string $key): null
            {
                return null;
            }
        };

        $twig->addGlobal('app', $app);
        $twig->addFilter(new TwigFilter(
            'trans',
            static fn (string $key, array $parameters = []): string => strtr($key, $parameters),
        ));
        $twig->addFunction(new TwigFunction(
            'localized_page_routes',
            $localizedRoutesResolver ?? static fn (
                ?string $routeName = null,
                array $parameters = [],
            ): LocalizedPageRoutes => $localizedRoutes,
        ));
        $twig->addFunction(new TwigFunction('contact_business_name', static fn (): string => 'Maison Exemple'));
        $twig->addFunction(new TwigFunction(
            'encore_entry_link_tags',
            static fn (): string => '<link rel="stylesheet" href="/build/app.css">',
            ['is_safe' => ['html']],
        ));
        $twig->addFunction(new TwigFunction('csp_nonce', static fn (): string => 'test-nonce'));
        $twig->addFunction(new TwigFunction(
            'asset',
            static fn (string $path): string => '/assets/'.mb_ltrim($path, '/'),
        ));
        $twig->addFunction(new TwigFunction(
            'absolute_url',
            static fn (string $path): string => str_starts_with($path, 'http://') || str_starts_with($path, 'https://')
                ? $path
                : 'https://fallback.example.test/'.mb_ltrim($path, '/'),
        ));
        $twig->addFunction(new TwigFunction(
            'path',
            static function (string $route, array $parameters = []): string {
                if ('shop' === $route) {
                    return isset($parameters['slug'])
                        ? '/fallback-shop/'.$parameters['slug']
                        : '/fallback-shop';
                }

                if ('shop_product' === $route) {
                    return '/fallback-shop/'.$parameters['categorySlug'].'/'.$parameters['slug'];
                }

                return '/'.$route;
            },
        ));
        $twig->addFunction(new TwigFunction(
            'asset_glide',
            static fn (string $path): string => 'https://fallback.example.test/media/'.mb_ltrim($path, '/'),
        ));
        $twig->addFunction(new TwigFunction(
            'product_review_aggregate_rating',
            static fn (object $product): null => null,
        ));

        return $twig;
    }

    private function headCrawler(string $html): Crawler
    {
        return new Crawler('<html><head>'.$html.'</head><body></body></html>');
    }

    /**
     * @return array<string, mixed>
     */
    private function jsonLdSchema(Crawler $crawler): array
    {
        $scripts = $crawler->filter('script[type="application/ld+json"]');
        self::assertCount(1, $scripts);
        $script = $scripts->getNode(0);
        self::assertNotNull($script);

        $schema = json_decode($script->textContent, true, flags: \JSON_THROW_ON_ERROR);
        self::assertIsArray($schema);

        return $schema;
    }

    /**
     * @return array<string, mixed>
     */
    private function renderCatalogSchema(
        LocalizedPageRoutes $localizedRoutes,
        ?callable $localizedRoutesResolver = null,
    ): array {
        $category = (object) [
            'name' => 'Champagnes',
            'slug' => 'champagnes',
        ];
        $product = (object) [
            'fullName' => 'Cuvée Brut',
            'name' => 'Brut',
            'description' => '<p>Champagne brut.</p>',
            'quality' => '',
            'category' => $category,
            'slug' => 'cuvee-brut',
            'secondImage' => null,
            'image' => null,
            'soldOut' => false,
            'variants' => [],
            'sku' => 'CUVEE-BRUT',
        ];
        $component = (object) [
            'category' => $category,
            'showCategoryMenu' => true,
            'currentCategoryVisible' => true,
            'schemaProducts' => [$product],
            'shouldShowPrices' => false,
            'activePriceGroupId' => null,
            'productPageEnabled' => false,
        ];
        $html = $this->twig(
            $localizedRoutes,
            localizedRoutesResolver: $localizedRoutesResolver,
        )->render('components/shop/CatalogSeo.html.twig', [
            'this' => $component,
        ]);

        return $this->jsonLdSchema($this->headCrawler($html));
    }
}
