<?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\Edition;
use Ferienpass\CoreBundle\Entity\Offer;
use Ferienpass\CoreBundle\Entity\Tenant;
use Ferienpass\CoreBundle\Entity\TenantDomain;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Fixtures\Doctrine\OrmTestCase;
use Ferienpass\CoreBundle\Tenant\TenantContext;

/**
 * The filter is the whole security model of the shared database, so these run
 * against a real EntityManager and real SQL rather than a mocked one.
 */
class TenantFilterTest extends OrmTestCase
{
    public function testHidesRowsOfOtherTenants(): void
    {
        [$a, $b] = $this->createTenants();

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

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

        $offers = $this->em->getRepository(Offer::class)->findAll();

        $this->assertCount(1, $offers);
        $this->assertSame('a', $offers[0]->getTenant()?->getSlug());
    }

    public function testHidesARowOfAnotherTenantEvenWhenFetchedByItsId(): void
    {
        [$a, $b] = $this->createTenants();

        $offer = $this->createOffer();
        $offer->setTenant($b);
        $this->em->flush();
        $id = $offer->getId();
        $this->em->clear();

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

        $this->assertNull($this->em->getRepository(Offer::class)->find($id));
    }

    public function testHidesAccountsOfOtherTenants(): void
    {
        [$a, $b] = $this->createTenants();

        $this->createUser('parent@example.com')->setTenant($a);
        $this->createUser('other@example.com')->setTenant($b);
        $this->em->flush();
        $this->em->clear();

        $this->context()->setTenant($this->reload($b));

        $this->assertNull($this->em->getRepository(User::class)->findOneBy(['email' => 'parent@example.com']));
        $this->assertNotNull($this->em->getRepository(User::class)->findOneBy(['email' => 'other@example.com']));
    }

    /**
     * The same address may hold an account in two municipalities; only the
     * tenant-scoped index stands in the way, and it must not.
     */
    public function testTheSameEmailMayExistInTwoTenants(): void
    {
        [$a, $b] = $this->createTenants();

        $this->createUser('parent@example.com')->setTenant($a);
        $this->createUser('parent@example.com')->setTenant($b);

        $this->em->flush();

        $this->context()->setTenant($a);
        $this->assertCount(1, $this->em->getRepository(User::class)->findBy(['email' => 'parent@example.com']));
    }

    public function testAssociationsAreFilteredToo(): void
    {
        [$a, $b] = $this->createTenants();

        $edition = $this->createEdition();
        $edition->setTenant($a);

        $mine = $this->createOffer(edition: $edition);
        $mine->setTenant($a);
        $theirs = $this->createOffer(edition: $edition);
        $theirs->setTenant($b);

        $this->em->flush();
        $editionId = $edition->getId();
        $this->em->clear();

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

        $edition = $this->em->getRepository(Edition::class)->find($editionId);

        $this->assertNotNull($edition);
        $this->assertCount(1, $edition->getOffers());
    }

    public function testTheTenantListItselfIsNotFiltered(): void
    {
        [$a] = $this->createTenants();

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

        $this->assertCount(2, $this->em->getRepository(Tenant::class)->findAll());
    }

    public function testNoTenantMeansNoFiltering(): void
    {
        [$a, $b] = $this->createTenants();

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

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

        $this->assertCount(2, $this->em->getRepository(Offer::class)->findAll());
    }

    /**
     * A single-tenant build must behave exactly as it did before multi-tenancy
     * existed, even if tenant rows happen to be present.
     */
    public function testDisabledMultiTenancyNeverFilters(): void
    {
        [$a, $b] = $this->createTenants();

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

        $context = new TenantContext($this->registry, enabled: false);
        $context->setTenant($this->reload($a));

        $this->assertCount(2, $this->em->getRepository(Offer::class)->findAll());
        $this->assertFalse($this->em->getFilters()->isEnabled('tenant'));
    }

    /**
     * @return array{Tenant, Tenant}
     */
    private function createTenants(): array
    {
        $tenants = [];

        foreach (['a', 'b'] as $slug) {
            $tenant = new Tenant();
            $tenant->setSlug($slug);
            $tenant->setName(strtoupper($slug));
            $tenant->addDomain(new TenantDomain($tenant, $slug.'.example.com'));

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

            $tenants[] = $tenant;
        }

        $this->em->flush();

        return $tenants;
    }

    private function reload(Tenant $tenant): Tenant
    {
        return $this->em->getRepository(Tenant::class)->find($tenant->getId());
    }

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