<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CoreBundle\Tests\Routing;

use Ferienpass\CoreBundle\Entity\Tenant;
use Ferienpass\CoreBundle\Entity\TenantDomain;
use Ferienpass\CoreBundle\Repository\TenantDomainRepository;
use Ferienpass\CoreBundle\Routing\SurfaceAwareUrlGenerator;
use Ferienpass\CoreBundle\Tenant\HostResolver;
use Ferienpass\CoreBundle\Tenant\ResolvedHost;
use Ferienpass\CoreBundle\Tenant\Surface;
use Ferienpass\CoreBundle\Tenant\SurfaceContext;
use Ferienpass\CoreBundle\Tenant\TenantContext;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouterInterface;

class SurfaceAwareUrlGeneratorTest extends TestCase
{
    private const string PUBLIC_HOST = 'ferienpass-nordhorn.de';
    private const string ADMIN_HOST = 'nordhorn.my-ferienpass.de';

    public function testLeavesEverythingAloneOnASingleTenantBuild(): void
    {
        $generator = $this->generator('/admin/angebote', enabled: false);

        self::assertSame('/admin/angebote', $generator->generate('admin_offers'));
    }

    public function testKeepsTheAdminPathWhereThePublicSiteAlsoServesTheAdmin(): void
    {
        $tenant = self::tenant(withAdminDomain: false);
        $generator = $this->generator('/admin/angebote', tenant: $tenant, on: ResolvedHost::frontend(self::PUBLIC_HOST, $tenant, true));

        self::assertSame('/admin/angebote', $generator->generate('admin_offers'));
    }

    public function testDropsTheAdminSegmentOnTheAdminDomain(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/admin/angebote', tenant: $tenant, on: ResolvedHost::admin(self::ADMIN_HOST, $tenant));

        self::assertSame('/angebote', $generator->generate('admin_offers'));
    }

    public function testKeepsTheLocalePrefixInFront(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/nl/admin/angebote?page=2', tenant: $tenant, on: ResolvedHost::admin(self::ADMIN_HOST, $tenant));

        self::assertSame('/nl/angebote?page=2', $generator->generate('admin_offers'));
    }

    public function testDropsTheAdminSegmentFromAnAbsoluteUrlToo(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('https://'.self::ADMIN_HOST.'/admin/angebote', tenant: $tenant, on: ResolvedHost::admin(self::ADMIN_HOST, $tenant));

        self::assertSame('https://'.self::ADMIN_HOST.'/angebote', $generator->generate('admin_offers', referenceType: UrlGeneratorInterface::ABSOLUTE_URL));
    }

    /**
     * "Angebot ansehen" in the admin, on a tenant whose public site is a
     * different domain.
     */
    public function testSendsAFrontendLinkToThePublicSite(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/o/sommer/4Z9k', tenant: $tenant, on: ResolvedHost::admin(self::ADMIN_HOST, $tenant));

        self::assertSame('https://'.self::PUBLIC_HOST.'/o/sommer/4Z9k', $generator->generate('offer_reader'));
    }

    public function testSendsAnAdminLinkToTheAdminDomain(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/admin/angebote/4Z9k/bearbeiten', tenant: $tenant, on: ResolvedHost::frontend(self::PUBLIC_HOST, $tenant, false));

        self::assertSame('https://'.self::ADMIN_HOST.'/angebote/4Z9k/bearbeiten', $generator->generate('admin_offer_edit'));
    }

    /**
     * A cron has no request. The router points at the tenant's public site, so
     * a link into the admin still has to move to the admin domain.
     */
    public function testMovesAdminLinksToTheAdminDomainWithoutARequest(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/admin/angebote', tenant: $tenant, on: null);

        self::assertSame('https://'.self::ADMIN_HOST.'/angebote', $generator->generate('admin_offers'));
    }

    /**
     * A municipality that runs no CMS has nowhere to send a public link — the
     * caller should not have offered one, and swallowing it would only hide
     * that.
     */
    public function testLeavesAFrontendLinkAloneWhenTheTenantHasNoPublicSite(): void
    {
        $tenant = self::tenant(withFrontendDomain: false);
        $generator = $this->generator('/o/sommer/4Z9k', tenant: $tenant, on: ResolvedHost::admin(self::ADMIN_HOST, $tenant));

        self::assertSame('/o/sommer/4Z9k', $generator->generate('offer_reader'));
    }

    public function testThePlatformConsoleServesTheAdminAtItsRoot(): void
    {
        $tenant = self::tenant();
        $generator = $this->generator('/admin/mandanten', tenant: $tenant, on: ResolvedHost::platform('platform.my-ferienpass.de'));

        self::assertSame('/mandanten', $generator->generate('admin_tenants'));
    }

    private function generator(string $url, bool $enabled = true, ?Tenant $tenant = null, ?ResolvedHost $on = null): SurfaceAwareUrlGenerator
    {
        $inner = $this->createMock(RouterInterface::class);
        $inner->method('generate')->willReturn($url);
        $inner->method('getContext')->willReturn(new RequestContext(scheme: 'https', httpsPort: 443));

        $tenantContext = $this->createMock(TenantContext::class);
        $tenantContext->method('getTenant')->willReturn($tenant);

        $surfaces = new SurfaceContext(new HostResolver($this->createMock(TenantDomainRepository::class), $enabled), $tenantContext);
        $surfaces->setCurrent($on);

        return new SurfaceAwareUrlGenerator($inner, $surfaces);
    }

    private static function tenant(bool $withFrontendDomain = true, bool $withAdminDomain = true): Tenant
    {
        $tenant = new Tenant();

        if ($withFrontendDomain) {
            $tenant->addDomain(new TenantDomain($tenant, self::PUBLIC_HOST, Surface::Frontend));
        }

        if ($withAdminDomain) {
            $tenant->addDomain(new TenantDomain($tenant, self::ADMIN_HOST, Surface::Admin));
        }

        return $tenant;
    }
}
