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

use Ferienpass\CoreBundle\Entity\Offer;
use Ferienpass\CoreBundle\Entity\Tenant;
use Ferienpass\CoreBundle\Entity\TenantDomain;
use Ferienpass\CoreBundle\Fixtures\Doctrine\OrmTestCase;
use Ferienpass\CoreBundle\Tenant\TenantContext;
use Ferienpass\CoreBundle\Tenant\TenantFilter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RequestContext;

class TenantContextTest extends OrmTestCase
{
    public function testRunForRestoresThePreviousTenant(): void
    {
        $a = $this->createTenant('a');
        $b = $this->createTenant('b');

        $context = $this->context();
        $context->setTenant($a);

        $seen = $context->runFor($b, static fn (): ?Tenant => $context->getTenant());

        $this->assertSame($b, $seen);
        $this->assertSame($a, $context->getTenant());
    }

    public function testRunForRestoresThePreviousTenantWhenTheCallbackThrows(): void
    {
        $a = $this->createTenant('a');
        $b = $this->createTenant('b');

        $context = $this->context();
        $context->setTenant($a);

        try {
            $context->runFor($b, static function (): void {
                throw new \RuntimeException('handler blew up');
            });
        } catch (\RuntimeException) {
        }

        $this->assertSame($a, $context->getTenant());
        $this->assertTrue($this->em->getFilters()->isEnabled(TenantFilter::NAME));
    }

    public function testRunUnfilteredSeesEveryTenant(): void
    {
        $a = $this->createTenant('a');
        $b = $this->createTenant('b');

        $this->createOffer()->setTenant($a);
        $this->createOffer()->setTenant($b);
        $this->em->flush();
        $this->em->clear();

        $context = $this->context();
        $context->setTenant($this->em->getRepository(Tenant::class)->find($a->getId()));

        $this->assertCount(1, $this->em->getRepository(Offer::class)->findAll());
        $this->assertCount(2, $context->runUnfiltered(fn (): array => $this->em->getRepository(Offer::class)->findAll()));
        $this->assertCount(1, $this->em->getRepository(Offer::class)->findAll());
    }

    public function testCacheKeysAreNamespacedByTenant(): void
    {
        $context = $this->context();

        $this->assertSame('page_config', $context->cacheKey('page_config'));

        $context->setTenant($this->createTenant('nordhorn'));

        $this->assertSame('page_config.tenant.nordhorn', $context->cacheKey('page_config'));
    }

    public function testGetTenantOrFailComplainsWhenNoTenantIsActive(): void
    {
        $this->expectException(\LogicException::class);

        $this->context()->getTenantOrFail();
    }

    /**
     * A cron has no request to take a host from, so without this every tenant's
     * mail would link into whichever host `router.request_context` names.
     */
    public function testPointsTheRouterAtTheTenantsHostWhenThereIsNoRequest(): void
    {
        $requestContext = new RequestContext(host: 'default.example.com');
        $context = new TenantContext($this->registry, true, new RequestStack(), $requestContext);

        $context->setTenant($this->createTenantWithHost('nordhorn', 'ferienpass-nordhorn.de'));

        $this->assertSame('ferienpass-nordhorn.de', $requestContext->getHost());
    }

    public function testRestoresTheConfiguredHostWhenTheTenantIsCleared(): void
    {
        $requestContext = new RequestContext(host: 'default.example.com');
        $context = new TenantContext($this->registry, true, new RequestStack(), $requestContext);

        $context->runFor($this->createTenantWithHost('nordhorn', 'ferienpass-nordhorn.de'), static function (): void {});

        $this->assertSame('default.example.com', $requestContext->getHost());
    }

    public function testHopsTheHostFromTenantToTenant(): void
    {
        $requestContext = new RequestContext(host: 'default.example.com');
        $context = new TenantContext($this->registry, true, new RequestStack(), $requestContext);

        $seen = [];
        foreach ([['a', 'a.example.com'], ['b', 'b.example.com']] as [$slug, $host]) {
            $context->runFor($this->createTenantWithHost($slug, $host), static function () use ($requestContext, &$seen): void {
                $seen[] = $requestContext->getHost();
            });
        }

        $this->assertSame(['a.example.com', 'b.example.com'], $seen);
    }

    /**
     * During a request the context already names the host the visitor actually
     * used, which is more specific than the tenant's canonical domain.
     */
    public function testLeavesTheRouterAloneDuringARequest(): void
    {
        $requestStack = new RequestStack();
        $requestStack->push(Request::create('https://www.ferienpass-nordhorn.de/'));

        $requestContext = new RequestContext(host: 'www.ferienpass-nordhorn.de');
        $context = new TenantContext($this->registry, true, $requestStack, $requestContext);

        $context->setTenant($this->createTenantWithHost('nordhorn', 'ferienpass-nordhorn.de'));

        $this->assertSame('www.ferienpass-nordhorn.de', $requestContext->getHost());
    }

    public function testDoesNotTouchTheRouterOnASingleTenantBuild(): void
    {
        $requestContext = new RequestContext(host: 'default.example.com');
        $context = new TenantContext($this->registry, false, new RequestStack(), $requestContext);

        $context->setTenant($this->createTenantWithHost('nordhorn', 'ferienpass-nordhorn.de'));

        $this->assertSame('default.example.com', $requestContext->getHost());
    }

    private function createTenantWithHost(string $slug, string $host): Tenant
    {
        $tenant = $this->createTenant($slug);
        $tenant->addDomain(new TenantDomain($tenant, $host));

        $this->em->flush();

        return $tenant;
    }

    private function createTenant(string $slug): Tenant
    {
        $tenant = new Tenant();
        $tenant->setSlug($slug);
        $tenant->setName(ucfirst($slug));

        $this->em->persist($tenant);
        $this->em->flush();

        return $tenant;
    }

    private function context(): TenantContext
    {
        return new TenantContext($this->registry, enabled: true);
    }
}
