<?php declare(strict_types=1);

namespace Newland\Toubiz\Poi\Neos\Tests\Integration\Detail;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Neos\ContentRepository\Domain\Model\Node;
use Neos\Flow\Http\Response;
use Newland\NeosCommon\Service\ObjectPathMappingService;
use Newland\Toubiz\Api\ObjectAdapter\ArticleAdapterInterface;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ArticleConstants;
use Newland\Toubiz\Api\ObjectAdapter\Concern\ExternalIdType;
use Newland\Toubiz\Api\Service\ServiceFactory;
use Newland\Toubiz\Api\Service\Weather\AbstractWeatherService;
use Newland\Toubiz\Poi\Neos\Tests\Integration\IntegrationTestCase;
use Newland\Toubiz\Sync\Neos\Command\IdentifierCommandController;
use Newland\Toubiz\Sync\Neos\Domain\Model\Article;
use Newland\Toubiz\Sync\Neos\Domain\Repository\ArticleRepository;
use Newland\Toubiz\Sync\Neos\Service\UrlIdentifierRedirectService;
use Newland\Toubiz\Sync\Neos\Service\WeatherService;
use Newland\Toubiz\Sync\Neos\Tests\Factory\ArticleFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\MediaFactory;
use Newland\Toubiz\Sync\Neos\Tests\Factory\UriFactory;
use Ramsey\Uuid\Uuid;

abstract class AbstractDetailTestCase extends IntegrationTestCase
{

    public function setUp(): void
    {
        parent::setUp();

        // Do not fetch live weather data during tests
        foreach (WeatherService::CONFIGURATION_MAP as $serviceName) {
            ServiceFactory::mock(
                $serviceName,
                $this->createMock(AbstractWeatherService::class)
            );
        }
    }

    public function tearDown(): void
    {
        ServiceFactory::resetMocks();
        parent::tearDown();
    }

    protected abstract function articleType(): int;

    protected function article(array $attributes = []): Article
    {
        $attributes['mainType'] = $this->articleType();
        $attributes['urlIdentifier'] = $attributes['urlIdentifier'] ?? Uuid::uuid4()->toString();
        return $this->articleFactory->create($attributes);
    }

    protected function articleUrl(Article $article): string
    {
        return $this->articleUrlService->generateUrlByCurrentNode($article, $this->node);
    }

    protected function requestArticleDetailPage(Article $article): Response
    {
        return $this->browser->request(
            $this->articleUrl($article)
        );
    }

    public function testDoesNotThrowError(): void
    {
        $response = $this->requestArticleDetailPage($this->article());
        $this->assertResponseOk($response);
        $this->assertResponseNotContains('Exception.*', $response);
    }

    public function testDisplaysBookingLink(): void
    {
        if ($this->articleType() === ArticleConstants::TYPE_CITY) {
            $this->assertTrue(true);
            return;
        }

        $response = $this->requestArticleDetailPage($this->article([
            'bookingUris' => [
                (new UriFactory($this->objectManager))->create([ 'label' => 'Jetzt buchen', 'uri' => 'https://google.com', 'originalId' => 'foo' ]),
                (new UriFactory($this->objectManager))->create([ 'label' => 'Jetzt informieren', 'uri' => 'https://yahoo.com', 'originalId' => 'bar' ]),
            ]
        ]));

        $this->assertResponseOk($response);
        $this->assertResponseContains('Jetzt buchen', $response);
        $this->assertResponseContains('href="https:\/\/google.com"', $response);
        $this->assertResponseContains('Jetzt informieren', $response);
        $this->assertResponseContains('href="https:\/\/yahoo.com"', $response);
    }
    public function testUrlsWithoutUrlIdentifiersRedirectCorrectly(): void
    {
        $article = $this->article([ 'language' => 'de', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $url = $this->articleUrl($article);
        $urlWithoutHash = str_replace('-' . $article->getUrlIdentifier(), '', $url);

        $this->browser->setFollowRedirects(false);
        $response = $this->browser->request($urlWithoutHash);

        $this->assertEquals(301, $response->getStatusCode());
        $this->assertEquals($url, $response->getHeader('Location'));
    }

    public function testUrlsWithIncorrectPathRedirectToCorrectUrlIdentifier(): void
    {
        $article = $this->article([ 'language' => 'de', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $url = $this->articleUrl($article);

        $urlParts = explode('/', $url);
        $urlParts[count($urlParts) - 1] = 'foo-bar-' . $article->getUrlIdentifier();
        $urlWithIncorrectNamePart = implode('/', $urlParts);

        $this->browser->setFollowRedirects(false);
        $response = $this->browser->request($urlWithIncorrectNamePart);

        $this->assertEquals(301, $response->getStatusCode());
        $this->assertEquals($url, $response->getHeader('Location'));
    }
    public function testDoesNotShowHiddenArticles(): void
    {
        $hidden = $this->article([ 'hidden' => true ]);
        $response = $this->requestArticleDetailPage($hidden);
        $this->assertEquals(404, $response->getStatusCode());
    }

    public function testContainsLanguageMenuLinkToAlternativeLanguage(): void
    {
        $this->createNodeLanguageVariants($this->siteNode, [ 'en', 'fr' ]);

        $id = (string) Uuid::uuid4();
        $german = $this->article([ 'languageGrouping' => $id, 'name' => 'the-german-one', 'language' => 'de', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $english = $this->article([ 'languageGrouping' => $id, 'name' => 'the-english-one', 'language' => 'en', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $french = $this->article([ 'languageGrouping' => $id, 'name' => 'the-french-one', 'language' => 'fr', 'urlIdentifier' => md5(random_bytes(32)) ]);

        $response = $this->requestArticleDetailPage($german);
        $this->assertResponseOk($response);

        $this->assertResponseContains(
            sprintf('<a href=".*?%s"', $german->getUrlIdentifier()),
            $response,
            'Should contain language navigation link to german version'
        );
        $this->assertResponseContains(
            sprintf('<a href=".*?%s"', $english->getUrlIdentifier()),
            $response,
            'Should contain language navigation link to english version'
        );
        $this->assertResponseContains(
            sprintf('<a href=".*?%s"', $french->getUrlIdentifier()),
            $response,
            'Should contain language navigation link to french version'
        );
    }

    public function testContainsHrefLangLinkToAlternativeLanguage(): void
    {
        $this->createNodeLanguageVariants($this->siteNode, [ 'en', 'fr' ]);

        $id = (string) Uuid::uuid4();
        $german = $this->article([ 'originalId' => $id, 'name' => 'the-german-one', 'language' => 'de', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $english = $this->article([ 'originalId' => $id, 'name' => 'the-english-one', 'language' => 'en', 'urlIdentifier' => md5(random_bytes(32)) ]);
        $french = $this->article([ 'originalId' => $id, 'name' => 'the-french-one', 'language' => 'fr', 'urlIdentifier' => md5(random_bytes(32)) ]);

        $response = $this->requestArticleDetailPage($german);
        $this->assertResponseOk($response);

        $this->assertResponseContains(
            sprintf('<link rel="alternate" hreflang="%s" href=".*?%s"', 'de', $german->getUrlIdentifier()),
            $response,
            'Should contain hreflang link to german version'
        );
        $this->assertResponseContains(
            sprintf('<link rel="alternate" hreflang="%s" href=".*?%s"', 'en', $english->getUrlIdentifier()),
            $response,
            'Should contain hreflang link to english version'
        );
        $this->assertResponseContains(
            sprintf('<link rel="alternate" hreflang="%s" href=".*?%s"', 'fr', $french->getUrlIdentifier()),
            $response,
            'Should contain hreflang link to french version'
        );
    }


    public function testRendersSlots(): void
    {
        $article = $this->article();

        $response = $this->browser->request($this->articleUrl($article));
        $this->assertResponseOk($response);

        foreach ($this->provideSlotsThatShouldExist() as [ $slot ]) {
            $this->assertResponseContains(sprintf('Slot(%s)', $slot), $response, null, false);
        }
    }

    public function provideSlotsThatShouldExist(): array
    {
        return [
            [ 'aboveAll' ],
            [ 'belowAll' ],
            [ 'aboveTitle' ],
            [ 'belowTitle' ],
            [ 'aboveDetail' ],
            [ 'belowDetail' ],
            [ 'aboveDetailGallery' ],
            [ 'belowDetailGallery' ],
            [ 'aboveDetailMainContent' ],
            [ 'belowDetailMainContent' ],
            [ 'aboveDetailAside' ],
            [ 'belowDetailAside' ],
        ];
    }

    public function testRespectsUrlIdentifierRedirects(): void
    {
        $before = $this->article([ 'language' => 'de', 'name' => '__BEFORE_NAME__' ]);
        $after = $this->article([ 'language' => 'de', 'name' => '__AFTER_NAME__' ]);
        $beforeUrl = $this->articleUrl($before);
        $afterUrl = $this->articleUrl($after);

        $this->objectManager->get(UrlIdentifierRedirectService::class)
            ->addRedirect($before->getUrlIdentifier(), $after->getUrlIdentifier());
        $this->persistenceManager->persistAll();

        // While the old one still exists, calling it will display it's detail page
        $this->browser->setFollowRedirects(false);
        $response = $this->browser->request($beforeUrl);
        $this->assertResponseOk($response);
        $this->assertResponseContains('__BEFORE_NAME__', $response);

        // When it is deleted, the it'll redirect to the new one.
        $this->persistenceManager->remove($this->persistenceManager->getObjectByIdentifier($before->getPersistenceObjectIdentifier(), Article::class));
        $this->persistenceManager->persistAll();
        $this->objectManager->get(ObjectPathMappingService::class)->flushMappings(Article::class);

        $this->browser->setFollowRedirects(false);
        $response = $this->browser->request($beforeUrl);
        $this->assertEquals(301, $response->getStatusCode());
        $this->assertEquals($afterUrl, $response->getHeader('Location'));
    }

    public function testRespectsRedirectsCreatedByMigratingToToubizNew(): void
    {
        $legacyId = Uuid::uuid4()->toString();
        $after = $this->article([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ,
            'externalIds' => [ [ 'type' => ExternalIdType::TOUBIZ_LEGACY_REMOTE_ID, 'id' => $legacyId ] ],
            'urlIdentifier' => Uuid::uuid4()->toString(),
        ]);
        $before = $this->article([
            'language' => 'de',
            'sourceSystem' => ArticleAdapterInterface::SOURCE_TOUBIZ_LEGACY,
            'originalId' => $legacyId,
            'urlIdentifier' => Uuid::uuid4()->toString(),
        ]);
        $beforeUrl = $this->articleUrl($before);
        $afterUrl = $this->articleUrl($after);

        // Migrate & delete old
        $this->objectManager->get(IdentifierCommandController::class)->migrateArticlesToToubizNewCommand(true, true);
        $this->persistenceManager->remove($this->fresh($before));
        $this->persistenceManager->persistAll();
        $this->persistenceManager->clearState();
        $this->objectManager->get(ObjectPathMappingService::class)->flushMappings(Article::class);

        // Now old URL should redirect to new one
        $this->browser->setFollowRedirects(false);
        $response = $this->browser->request($beforeUrl);
        $this->assertEquals(301, $response->getStatusCode());
        $this->assertEquals($afterUrl, $response->getHeader('Location'));
    }

    private function fresh(Article $article): Article
    {
        return $this->persistenceManager->getObjectByIdentifier(
            $article->getPersistenceObjectIdentifier(),
            Article::class
        );
    }
}
