<?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\CmsBundle\Tests\EventListener\DataContainer;

use Contao\DataContainer;
use Ferienpass\CmsBundle\EventListener\DataContainer\TenantRootPageListener;
use Ferienpass\CoreBundle\Entity\Tenant;
use Ferienpass\CoreBundle\Entity\TenantDomain;
use Ferienpass\CoreBundle\Repository\TenantRepository;
use Ferienpass\CoreBundle\Tenant\TenantContext;
use Ferienpass\CoreBundle\Tenant\TenantRootPage;
use PHPUnit\Framework\TestCase;

class TenantRootPageListenerTest extends TestCase
{
    public function testAcceptsARootPageWhoseDomainBelongsToItsTenant(): void
    {
        $values = $this->validate(['tenant' => '1', 'dns' => 'ferienpass-nordhorn.de'], ['id' => 5, 'type' => 'root']);

        $this->assertSame(['tenant' => '1', 'dns' => 'ferienpass-nordhorn.de'], $values);
    }

    /**
     * A root page without a tenant answers a host on a shared installation
     * without anything saying whose Ferienpass it serves.
     */
    public function testRejectsARootPageWithoutATenant(): void
    {
        $this->expectExceptionMessageMatches('/einem Mandanten zu/');

        $this->validate(['dns' => 'ferienpass-nordhorn.de'], ['id' => 5, 'type' => 'root', 'tenant' => 0]);
    }

    /**
     * One page root, one tenant: two root pages for one municipality would put
     * the same data under two page trees, and nothing could say which of them
     * "the" root page is.
     */
    public function testRejectsATenantThatAnotherRootPageAlreadyClaims(): void
    {
        $rootPages = $this->createMock(TenantRootPage::class);
        $rootPages->method('rootPagesOfTenant')->with(1, 5)->willReturn([['id' => 9, 'title' => 'Nordhorn', 'dns' => 'ferienpass-nordhorn.de']]);

        $this->expectExceptionMessageMatches('/gehört bereits zum Startpunkt „Nordhorn“ \(ID 9\)/');

        $this->validate(['tenant' => '1'], ['id' => 5, 'type' => 'root', 'dns' => 'ferienpass-nordhorn.de'], $rootPages);
    }

    /**
     * The two host mappings decide different halves of one request — whose data
     * (TenantDomain) and whose pages (tl_page.dns). Letting them disagree is
     * how one municipality's site ends up rendering another's offers.
     */
    public function testRejectsADomainTheTenantDoesNotHold(): void
    {
        $this->expectExceptionMessageMatches('/gehört nicht zum Mandanten/');

        $this->validate(['tenant' => '1', 'dns' => 'ferienpass-lingen.de'], ['id' => 5, 'type' => 'root']);
    }

    /**
     * An empty dns makes the root page answer every host the installation has,
     * including the ones another tenant claims.
     */
    public function testRejectsARootPageWithoutADomain(): void
    {
        $this->expectExceptionMessageMatches('/Domain ein/');

        $this->validate(['tenant' => '1', 'dns' => ''], ['id' => 5, 'type' => 'root']);
    }

    /**
     * Contao stores `dns` IDNA-encoded; the tenant's domains are stored as
     * typed. The umlaut domain is the same domain.
     */
    public function testComparesInternationalisedDomainsInTheirEncodedForm(): void
    {
        $tenant = $this->tenant(1, 'ferienpass-münster.de');

        $values = $this->validate(['tenant' => '1', 'dns' => 'xn--ferienpass-mnster-e3b.de'], ['id' => 5, 'type' => 'root'], tenant: $tenant);

        $this->assertSame('xn--ferienpass-mnster-e3b.de', $values['dns']);
    }

    /**
     * Leaving the tenant on a page that stopped being a root page would keep
     * the tenant claimed forever.
     */
    public function testClearsTheTenantWhenThePageStopsBeingARootPage(): void
    {
        $values = $this->validate(['type' => 'regular'], ['id' => 5, 'type' => 'root', 'tenant' => 1]);

        $this->assertSame(0, $values['tenant']);
    }

    /**
     * "Duplizieren" copies every column and never opens the edit form, so
     * without this the copy would be a second root page for the same tenant.
     */
    public function testUnassignsTheTenantOfACopiedRootPage(): void
    {
        $rootPages = $this->createMock(TenantRootPage::class);
        $rootPages->expects($this->once())->method('releaseRootPage')->with(12);

        $context = $this->createMock(TenantContext::class);
        $context->method('isEnabled')->willReturn(true);

        new TenantRootPageListener($context, $this->createMock(TenantRepository::class), $rootPages)->clearTenantOnCopy(12, $this->dc(['id' => 12]));
    }

    public function testDoesNothingOnASingleTenantBuild(): void
    {
        $context = $this->createMock(TenantContext::class);
        $context->method('isEnabled')->willReturn(false);

        $tenants = $this->createMock(TenantRepository::class);
        $tenants->expects($this->never())->method('find');

        $listener = new TenantRootPageListener($context, $tenants, $this->createMock(TenantRootPage::class));

        $this->assertSame(['dns' => 'ferienpass-nordhorn.de'], $listener->validate(['dns' => 'ferienpass-nordhorn.de'], $this->dc(['id' => 5, 'type' => 'root'])));
    }

    /**
     * @param array<string, mixed> $values
     * @param array<string, mixed> $record
     *
     * @return array<string, mixed>
     */
    private function validate(array $values, array $record, ?TenantRootPage $rootPages = null, ?Tenant $tenant = null): array
    {
        $context = $this->createMock(TenantContext::class);
        $context->method('isEnabled')->willReturn(true);

        $tenants = $this->createMock(TenantRepository::class);
        $tenants->method('find')->willReturn($tenant ?? $this->tenant(1, 'ferienpass-nordhorn.de'));

        $rootPages ??= $this->createMock(TenantRootPage::class);

        return new TenantRootPageListener($context, $tenants, $rootPages)->validate($values, $this->dc($record));
    }

    /**
     * @param array<string, mixed> $record
     */
    private function dc(array $record): DataContainer
    {
        $dc = $this->createMock(DataContainer::class);
        $dc->method('getCurrentRecord')->willReturn($record);

        return $dc;
    }

    private function tenant(int $id, string ...$hosts): Tenant
    {
        $tenant = new Tenant();
        $tenant->setName('Ferienpass Nordhorn');
        $tenant->setSlug('nordhorn');

        new \ReflectionProperty(Tenant::class, 'id')->setValue($tenant, $id);

        foreach ($hosts as $host) {
            $tenant->addDomain(new TenantDomain($tenant, $host));
        }

        return $tenant;
    }
}
