<?php

namespace Seo\Extractor;

use AutoAdmin\Bundle\Content\Configuration\ConfigManager;
use AutoAdmin\Bundle\Content\Entity\Domain;
use AutoAdmin\Bundle\Content\Entity\Site;
use AutoAdmin\Bundle\Content\Model\LocaleInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Content\Model\TranslationInterface;
use AutoAdmin\Bundle\Content\Routing\ContentRouter;
use AutoAdmin\Bundle\Content\Site\SiteContextHelper;
use AutoAdmin\Bundle\Seo\Entity\SeoMetadata;
use AutoAdmin\Bundle\Seo\Extractor\SeoContentExtractor;
use AutoAdmin\Bundle\Seo\Model\Alternate;
use AutoAdmin\Bundle\Seo\Model\Header;
use AutoAdmin\Bundle\Seo\Model\Meta;
use AutoAdmin\Bundle\Seo\Model\SeoContentTranslationInterface;
use AutoAdmin\Bundle\Seo\Model\SeoPage;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Component\HttpFoundation\Request;

class SeoContentExtractorTest extends TestCase
{
    /**
     * @dataProvider supportsDataProvider
     */
    public function testSupports(
        bool $isTranslatable,
        bool $isTranslation,
        bool $isUntranslatedContent,
        bool $expected
    ): void
    {
        $extractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['isTranslatable', 'isTranslation', 'isUntranslatedContent'])
            ->getMock();

        $extractor->method('isTranslatable')
            ->willReturn($isTranslatable);

        $extractor->method('isTranslation')
            ->willReturn($isTranslation);

        $extractor->method('isUntranslatedContent')
            ->willReturn($isUntranslatedContent);

        $this->assertEquals($expected, $extractor->supports(new stdClass()));
    }

    /**
     * @dataProvider extractDataProvider
     */
    public function testExtract(
        TranslatableInterface|SeoContentTranslationInterface|null $translatable,
        TranslationInterface|SeoContentTranslationInterface|null $translation,
        InvokedCount $handleTitle,
        InvokedCount $handleDescription,
        InvokedCount $handleMetas,
        InvokedCount $handleCanonical,
        InvokedCount $handleRobotsTag,
        InvokedCount $handleAlternates
    ): void {
        $extractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->onlyMethods([
                'getTranslatableTranslationObjects',
                'handleTitle',
                'handleDescription',
                'handleMetas',
                'handleCanonical',
                'handleRobotsTag',
                'handleAlternates',
            ])
            ->getMock();

        $extractor->method('getTranslatableTranslationObjects')
            ->willReturn([$translatable, $translation]);

        $extractor->expects($handleTitle)
            ->method('handleTitle');

        $extractor->expects($handleDescription)
            ->method('handleDescription');

        $extractor->expects($handleMetas)
            ->method('handleMetas');

        $extractor->expects($handleCanonical)
            ->method('handleCanonical');

        $extractor->expects($handleRobotsTag)
            ->method('handleRobotsTag');

        $extractor->expects($handleAlternates)
            ->method('handleAlternates');

        $result = $extractor->extract(new Request(), new stdClass(), new SeoPage());

        $this->assertInstanceOf(SeoPage::class, $result);
    }

    /**
     * @dataProvider handleTitleDataProvider
     */
    public function testHandleTitle(?string $title = null): void
    {
        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['sanitize'])
            ->getMock();

        $seoContentExtractor->method('sanitize')
            ->willReturn($title ?? '');

        $seoMetadata = $this->getMockBuilder(SeoMetadata::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata->method('getTitle')
            ->willReturn($title);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoPage->expects(!empty($title) ? $this->once() : $this->never())
            ->method('setTitle')
            ->with($title);

        $reflection = new \ReflectionClass(SeoContentExtractor::class);

        $method = $reflection->getMethod('handleTitle');

        $method->invokeArgs($seoContentExtractor, [$seoMetadata, $seoPage]);
    }

    /**
     * @dataProvider handleDescriptionDataProvider
     */
    public function testHandleDescription(?string $description = null): void
    {
        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['sanitize'])
            ->getMock();

        $seoContentExtractor->method('sanitize')
            ->willReturn($description ?? '');

        $seoMetadata = $this->getMockBuilder(SeoMetadata::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata->method('getDescription')
            ->willReturn($description);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoPage->expects(!empty($title) ? $this->once() : $this->never())
            ->method('addMeta')
            ->willReturnCallback(function ($meta) use ($description) {
                $this->assertInstanceOf(Meta::class, $meta);
                $this->assertEquals('description', $meta->getName());
                $this->assertEquals($description, $meta->getContent());
            });

        $reflection = new \ReflectionClass(SeoContentExtractor::class);

        $method = $reflection->getMethod('handleTitle');

        $method->invokeArgs($seoContentExtractor, [$seoMetadata, $seoPage]);
    }

    /**
     * @dataProvider handleMetasDataProvider
     */
    public function testHandleMetas(?array $metas, InvokedCount $addMeta): void
    {
        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata = $this->getMockBuilder(SeoMetadata::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata->method('getMetas')
            ->willReturn($metas);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $callCount = 0;
        $seoPage->expects($addMeta)
            ->method('addMeta')
            ->willReturnCallback(function ($meta) use ($metas, &$callCount) {
                $split = array_chunk($metas, 1, true);

                $this->assertInstanceOf(Meta::class, $meta);
                foreach ($split[$callCount] as $key => $value) {
                    $this->assertEquals($key, $meta->getName());
                    $this->assertEquals($value, $meta->getValue());
                }

                $callCount++;
            });

        $reflection = new \ReflectionClass(SeoContentExtractor::class);

        $method = $reflection->getMethod('handleMetas');

        $method->invokeArgs($seoContentExtractor, [$seoMetadata, $seoPage]);
    }

    /**
     * @dataProvider handleCanonicalDataProvider
     */
    public function testHandleCanonical(?string $canonicalOverride, string $uri, string $expected): void
    {
        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata = $this->getMockBuilder(SeoMetadata::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata->method('getCanonicalOverride')
            ->willReturn($canonicalOverride);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoPage->expects($this->once())
            ->method('setCanonical')
            ->with($expected);

        $request = $this->getMockBuilder(Request::class)
            ->disableOriginalConstructor()
            ->getMock();

        $request->method('getUri')
            ->willReturn($uri);

        $reflection = new \ReflectionClass(SeoContentExtractor::class);

        $method = $reflection->getMethod('handleCanonical');

        $method->invokeArgs($seoContentExtractor, [$seoMetadata, $seoPage, $request]);
    }

    /**
     * @dataProvider handleRobotsTagDataProvider
     */
    public function testHandleRobotsTag(bool $noIndex, bool $noFollow, ?string $expected): void
    {
        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata = $this->getMockBuilder(SeoMetadata::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoMetadata->method('isNoIndex')
            ->willReturn($noIndex);

        $seoMetadata->method('isNoFollow')
            ->willReturn($noFollow);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoPage->expects($expected !== null ? $this->once() : $this->never())
            ->method('addResponseHeader')
            ->willReturnCallback(function ($header) use ($expected) {
                $this->assertInstanceOf(Header::class, $header);
                $this->assertEquals('X-Robots-Tag', $header->getName());
                $this->assertEquals($expected, $header->getValue());
            });

        $reflection = new \ReflectionClass(SeoContentExtractor::class);

        $method = $reflection->getMethod('handleRobotsTag');

        $method->invokeArgs($seoContentExtractor, [$seoMetadata, $seoPage]);
    }

    /**
     * @dataProvider handleAlternatesDataProvider
     */
    public function testHandleAlternates(
        string $route,
        ?string $action,
        Collection $translations,
        ?string $siteLocaleForCode,
        ?Site $site,
        ?Domain $domain,
        ?string $generatedUrl,
        ?string $exception,
        array $expected
    ): void {
        if ($exception !== null) {
            $this->expectExceptionMessage($exception);
        }

        $seoContentExtractor = $this->getMockBuilder(SeoContentExtractor::class)
            ->disableOriginalConstructor()
            ->getMock();

        $translatable = $this->getMockBuilder(TranslatableInterface::class)
            ->disableOriginalConstructor()
            ->getMock();

        $translatable->method('getTranslations')
            ->willReturn($translations);

        $translatable->method('getSite')
            ->willReturn($site);

        $seoPage = $this->getMockBuilder(SeoPage::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoPage->expects($exception === null ? $this->once() : $this->never())
            ->method('setAlternates')
            ->willReturnCallback(function ($alternates) use ($expected) {
                if ($expected === []) {
                    $this->assertEmpty($alternates);

                    return;
                } else {
                    $index = 0;
                    foreach ($expected as $locale => $url) {
                        $this->assertInstanceOf(Alternate::class, $alternates[$index]);
                        $this->assertEquals($locale, $alternates[$index]->getHrefLang()->getCode());
                        $this->assertEquals($url, $alternates[$index]->getHref());
                        $index++;
                    }
                }
            });

        $request = $this->getMockBuilder(Request::class)
            ->disableOriginalConstructor()
            ->getMock();

        $request->method('get')
            ->with('_route')
            ->willReturn($route);

        $configManager = $this->getMockBuilder(ConfigManager::class)
            ->disableOriginalConstructor()
            ->getMock();

        $configManager->method('getRouteAction')
            ->with($route)
            ->willReturn($action);

        $siteContextHelper = $this->getMockBuilder(SiteContextHelper::class)
            ->disableOriginalConstructor()
            ->getMock();

        $siteContextHelper->method('getDomainByLocale')
            ->with($site, $this->isInstanceOf(LocaleInterface::class))
            ->willReturn($domain);

        if ($siteLocaleForCode !== null) {
            $locale = $this->getMockBuilder(LocaleInterface::class)
                ->disableOriginalConstructor()
                ->getMock();

            $locale->method('getCode')
                ->willReturn($siteLocaleForCode);

            $siteContextHelper->method('getSiteLocaleForCode')
                ->with($siteLocaleForCode)
                ->willReturn($locale);
        }

        $contentRouter = $this->getMockBuilder(ContentRouter::class)
            ->disableOriginalConstructor()
            ->getMock();

        $contentRouter->method('generate')
            ->willReturn($generatedUrl);

        $reflection = new \ReflectionClass(SeoContentExtractor::class);
        $reflection->getProperty('config')
            ->setValue($seoContentExtractor, $configManager);
        $reflection->getProperty('contextHelper')
            ->setValue($seoContentExtractor, $siteContextHelper);
        $reflection->getProperty('contentRouter')
            ->setValue($seoContentExtractor, $contentRouter);

        $method = $reflection->getMethod('handleAlternates');

        $method->invokeArgs($seoContentExtractor, [$translatable, $seoPage, $request]);
    }

    private function supportsDataProvider(): array
    {
        return [
            'is translatable' => [
                true,
                false,
                false,
                true,
            ],
            'is translation' => [
                false,
                true,
                false,
                true,
            ],
            'is untranslated content' => [
                false,
                false,
                true,
                true,
            ],
            'is not translatable content' => [
                false,
                true,
                true,
                true,
            ],
            'is not translation content' => [
                true,
                false,
                true,
                true,
            ],
            'is not untranslated content' => [
                true,
                true,
                false,
                true,
            ],
            'is all content' => [
                true,
                true,
                true,
                true,
            ],
            'is not content' => [
                false,
                false,
                false,
                false,
            ],
        ];
    }

    private function extractDataProvider(): array
    {
        return [
            'translatable/translation' => [
                'translatable' => $this->createMock(TranslatableInterface::class),
                'translation' => $this->createMock(TranslationInterface::class),
                'handleTitle' => $this->never(),
                'handleDescription' => $this->never(),
                'handleMetas' => $this->never(),
                'handleCanonical' => $this->never(),
                'handleRobotsTag' => $this->never(),
                'handleAlternates' => $this->once(),
            ],
            'untranslated content no SeoMetadata' => [
                'translatable' => $this->createSeoContentTranslation(),
                'translation' => $this->createSeoContentTranslation(),
                'handleTitle' => $this->never(),
                'handleDescription' => $this->never(),
                'handleMetas' => $this->never(),
                'handleCanonical' => $this->never(),
                'handleRobotsTag' => $this->never(),
                'handleAlternates' => $this->never(),
            ],
            'untranslated content' => [
                'translatable' => $this->createSeoContentTranslation($this->createMock(SeoMetadata::class)),
                'translation' => $this->createSeoContentTranslation($this->createMock(SeoMetadata::class)),
                'handleTitle' => $this->once(),
                'handleDescription' => $this->once(),
                'handleMetas' => $this->once(),
                'handleCanonical' => $this->once(),
                'handleRobotsTag' => $this->once(),
                'handleAlternates' => $this->never(),
            ],
        ];
    }

    private function createSeoContentTranslation(?MockObject $seoMetadata = null): MockObject
    {
        $seoContentTranslation = $this->getMockBuilder(SeoContentTranslationInterface::class)
            ->disableOriginalConstructor()
            ->getMock();

        $seoContentTranslation->method('getSeoMetadata')
            ->willReturn($seoMetadata);

        return $seoContentTranslation;
    }

    private function handleTitleDataProvider(): array
    {
        return [
            'null title' => [
                'title' => null,
            ],
            'empty title' => [
                'title' => '',
            ],
            'title' => [
                'title' => 'title',
            ],
        ];
    }

    private function handleDescriptionDataProvider(): array
    {
        return [
            'null description' => [
                'description' => null,
            ],
            'empty description' => [
                'description' => '',
            ],
            'description' => [
                'description' => 'description',
            ],
        ];
    }

    private function handleMetasDataProvider(): array
    {
        return [
            'null metas' => [
                'metas' => null,
                'addMeta' => $this->never(),
            ],
            'empty metas' => [
                'metas' => [],
                'addMeta' => $this->never(),
            ],
            'metas' => [
                'metas' => ['meta' => 'value'],
                'addMeta' => $this->once(),
            ],
            'multiple metas' => [
                'metas' => [
                    'meta1' => 'value1',
                    'meta2' => 'value2',
                ],
                'addMeta' => $this->exactly(2),
            ],
        ];
    }

    private function handleCanonicalDataProvider(): array
    {
        return [
            'canonical override' => [
                'canonicalOverride' => 'override',
                'uri' => 'uri',
                'expected' => 'override',
            ],
            'no canonical override' => [
                'canonicalOverride' => null,
                'uri' => 'uri',
                'expected' => 'uri',
            ],
            'uri with ?' => [
                'canonicalOverride' => null,
                'uri' => 'uri?query',
                'expected' => 'uri',
            ],
        ];
    }

    private function handleRobotsTagDataProvider(): array
    {
        return [
            'no index' => [
                'noIndex' => true,
                'noFollow' => false,
                'expected' => 'noindex',
            ],
            'no follow' => [
                'noIndex' => false,
                'noFollow' => true,
                'expected' => 'nofollow',
            ],
            'no index and no follow' => [
                'noIndex' => true,
                'noFollow' => true,
                'expected' => 'noindex, nofollow',
            ],
            'index and follow' => [
                'noIndex' => false,
                'noFollow' => false,
                'expected' => null,
            ],
        ];
    }

    private function handleAlternatesDataProvider(): array
    {
        return [
            'not Page, no translations' => [
                'route' => 'route',
                'action' => null,
                'translations' => new ArrayCollection(),
                'siteLocaleForCode' => null,
                'site' => null,
                'domaine' => null,
                'generatedUrl' => null,
                'exception' => null,
                'expected' => [],
            ],
            'not Page, translations but no Locale' => [
                'route' => 'route',
                'action' => null,
                'translations' => new ArrayCollection([
                    $this->createTranslation(),
                ]),
                'siteLocaleForCode' => null,
                'site' => null,
                'domaine' => null,
                'generatedUrl' => null,
                'exception' => "Locale is required for translatable content",
                'expected' => [],
            ],
            'not Page, translations but no Site' => [
                'route' => 'route',
                'action' => null,
                'translations' => new ArrayCollection([
                    $this->createTranslation('fr'),
                ]),
                'siteLocaleForCode' => null,
                'site' => null,
                'domaine' => null,
                'generatedUrl' => null,
                'exception' => "Site is required for translatable content",
                'expected' => [],
            ],
            'not Page, translations but no Domaine' => [
                'route' => 'route',
                'action' => null,
                'translations' => new ArrayCollection([
                    $this->createTranslation('fr'),
                ]),
                'siteLocaleForCode' => null,
                'site' => $this->createMock(Site::class),
                'domaine' => null,
                'generatedUrl' => null,
                'exception' => "Domain is required for translatable content",
                'expected' => [],
            ],
            'Page' => [
                'route' => 'route',
                'action' => null,
                'translations' => new ArrayCollection([
                    $this->createTranslation('fr'),
                ]),
                'siteLocaleForCode' => null,
                'site' => $this->createMock(Site::class),
                'domaine' => $this->createDomaine('domain'),
                'generatedUrl' => '/generated/url',
                'exception' => null,
                'expected' => [
                    'fr' => '/generated/url',
                ],
            ],
            'Page translated route' => [
                'route' => 'route.en',
                'action' => null,
                'translations' => new ArrayCollection([
                    $this->createTranslation('fr'),
                ]),
                'siteLocaleForCode' => 'en',
                'site' => $this->createMock(Site::class),
                'domaine' => $this->createDomaine('domain'),
                'generatedUrl' => '/generated/url',
                'exception' => null,
                'expected' => [
                    'en' => '/generated/url',
                ],
            ],
        ];
    }

    private function createTranslation(?string $code = null): MockObject
    {
        $translation = $this->getMockBuilder(TranslationInterface::class)
            ->disableOriginalConstructor()
            ->getMock();

        if ($code !== null) {
            $locale = $this->getMockBuilder(LocaleInterface::class)
                ->disableOriginalConstructor()
                ->getMock();

            $locale->method('getCode')
                ->willReturn($code);

            $translation->method('getLocale')
                ->willReturn($locale);
        }

        return $translation;
    }

    private function createDomaine(string $domain): MockObject
    {
        $domaine = $this->getMockBuilder(Domain::class)
            ->disableOriginalConstructor()
            ->getMock();

        $domaine->method('getDomain')
            ->willReturn($domain);

        return $domaine;
    }
}
