<?php

namespace App\Tests\Shared;

use App\Auth\Entity\User;
use App\Auth\Entity\UserActiveContext;
use App\Clinic\Entity\Clinic;
use App\Clinic\Entity\ClinicDoctorPermission;
use App\Discount\Entity\DiscountRule;
use App\Doctor\Entity\Doctor;
use App\Shared\Context\EntityContext;
use App\Shared\Tenant\TenantFilter;
use App\Tests\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;

/**
 * فیلتر باید **به‌تنهایی** جلوی cross-tenant را بگیرد — یعنی حتی کوئری‌ای که هیچ
 * شرط دستی روی محیط ندارد. بقیهٔ تست‌ها رفتار endpointها را می‌سنجند؛ اینجا خودِ
 * تور ایمنی سنجیده می‌شود.
 */
class TenantFilterLeakTest extends ApiTestCase
{
    private function em(): EntityManagerInterface
    {
        return static::getContainer()->get(EntityManagerInterface::class);
    }

    private function makeDoctor(?User $user = null): Doctor
    {
        $doctor = new Doctor($user ?? $this->createUser(['ROLE_DOCTOR']), 'دکتر نشتی');
        $this->em->persist($doctor);
        $this->em->flush();

        return $doctor;
    }

    private function makeClinic(?User $owner = null): Clinic
    {
        $clinic = new Clinic($owner ?? $this->createUser(['ROLE_CLINIC']));
        $clinic->setName('کلینیک نشتی');
        $this->em->persist($clinic);
        $this->em->flush();

        return $clinic;
    }

    private function rule(string $type, int $id, string $name): DiscountRule
    {
        $rule = new DiscountRule($type, $id, $name, DiscountRule::TYPE_INVOICE_AMOUNT);
        $this->em->persist($rule);
        $this->em->flush();

        return $rule;
    }

    private function enableFilterFor(string $type, int $id): void
    {
        $this->em()->getFilters()
            ->enable(TenantFilter::NAME)
            ->setParameter(TenantFilter::PARAM_TYPE, $type, 'string')
            ->setParameter(TenantFilter::PARAM_ID, $id, 'integer');
    }

    protected function tearDown(): void
    {
        $filters = $this->em()->getFilters();
        if ($filters->isEnabled(TenantFilter::NAME)) {
            $filters->disable(TenantFilter::NAME);
        }

        parent::tearDown();
    }

    /** ✅ کوئری بدون هیچ WHERE دستی، فقط ردیف‌های محیط جاری را می‌دهد. */
    public function testFilterAloneBlocksCrossTenantRowsWithoutAnyManualCondition(): void
    {
        $mine  = $this->makeDoctor();
        $other = $this->makeDoctor();

        $this->rule('doctor', $mine->getId(), 'قانون من');
        $this->rule('doctor', $other->getId(), 'قانون دیگری');
        $this->em->clear();

        $this->enableFilterFor('doctor', $mine->getId());

        $names = array_map(
            static fn(DiscountRule $r) => $r->getName(),
            $this->em()->createQuery('SELECT r FROM ' . DiscountRule::class . ' r')->getResult(),
        );

        self::assertContains('قانون من', $names);
        self::assertNotContains('قانون دیگری', $names);
    }

    /** findOneBy هم فیلتر می‌خورد — رکورد محیط دیگر «وجود ندارد». */
    public function testFindOneByCannotReachAnotherTenantsRow(): void
    {
        $mine    = $this->makeDoctor();
        $other   = $this->makeDoctor();
        $foreign = $this->rule('doctor', $other->getId(), 'قانون بیگانه');
        $this->em->clear();

        $this->enableFilterFor('doctor', $mine->getId());

        self::assertNull(
            $this->em()->getRepository(DiscountRule::class)->findOneBy(['uuid' => $foreign->getUuid()]),
        );
    }

    /**
     * find() با کلید اصلی هم فیلتر می‌خورد — در Doctrine ORM 3 برخلاف نسخه‌های قدیمی
     * که این مسیر را استثنا می‌کردند. تضمین قوی‌تری است، ولی چون رفتارِ کتابخانه است
     * نه قرارداد ما، اینجا تثبیتش می‌کنیم: اگر روزی برگردد، این تست خبر می‌دهد و سند
     * tenancy باید به‌روز شود.
     */
    public function testFindByPrimaryKeyIsAlsoFiltered(): void
    {
        $mine    = $this->makeDoctor();
        $other   = $this->makeDoctor();
        $foreign = $this->rule('doctor', $other->getId(), 'قانون بیگانه');
        $id      = $foreign->getId();
        $this->em->clear();

        $this->enableFilterFor('doctor', $mine->getId());

        self::assertNull($this->em()->find(DiscountRule::class, $id));
    }

    /**
     * ⚠️ محدودیت واقعی: entity که از قبل در identity map است دوباره کوئری نمی‌شود،
     * پس فیلتر آن را نمی‌بیند. به همین دلیل فیلتر جایگزین authorization نیست.
     */
    public function testAnEntityAlreadyInMemoryIsNotHiddenByTheFilter(): void
    {
        $mine    = $this->makeDoctor();
        $other   = $this->makeDoctor();
        $foreign = $this->rule('doctor', $other->getId(), 'قانون بیگانه');
        $id      = $foreign->getId();

        $this->enableFilterFor('doctor', $mine->getId());

        self::assertNotNull($this->em()->find(DiscountRule::class, $id), 'از identity map برمی‌گردد، نه از دیتابیس');
    }

    /** ⚠️ مسیر عمومی مارکت‌پلیس کاربر پنل ندارد؛ فیلتر نباید سایت را خالی کند. */
    public function testPublicDoctorSearchStaysCrossTenant(): void
    {
        $this->makeDoctor();
        $this->makeDoctor();

        $this->client->request('GET', '/api/v1/doctors?limit=5');

        self::assertSame(200, $this->client->getResponse()->getStatusCode());
    }

    /** ادمین سراسری است و فیلتر نمی‌خورد. */
    public function testAdminIsNotFiltered(): void
    {
        $admin  = $this->createUser(['ROLE_ADMIN']);
        $doctor = $this->makeDoctor();
        $clinic = $this->makeClinic();

        $this->rule('doctor', $doctor->getId(), 'قانون پزشک');
        $this->rule('clinic', $clinic->getId(), 'قانون کلینیک');

        $this->authJson('GET', '/api/v1/admin/discount-rules', $admin);

        self::assertNotSame(500, $this->responseCode(), 'ادمین نباید با فیلتر بشکند');
    }

    /**
     * ⚠️ کاربری که هنوز محیطی انتخاب نکرده فیلتر نمی‌خورد — در هیچ محیطی «نیست» و
     * دسترسی‌اش را همان checkerهای دامنه تعیین می‌کنند.
     */
    public function testUserWithoutAChosenContextIsNotFiltered(): void
    {
        $doctor = $this->makeDoctor();
        $clinic = $this->makeClinic();
        $clinic->getDoctors()->add($doctor);
        $this->em->persist(new ClinicDoctorPermission($clinic, $doctor));
        $this->em->flush();

        $resolver = static::getContainer()->get(\App\Shared\Context\EntityContextResolver::class);

        self::assertFalse(
            $resolver->resolve($doctor->getUser())->chosen,
            'محیطِ برآمده از نقش، انتخاب کاربر نیست',
        );
    }

    /** محیطی که کاربر صریحاً انتخاب کرده، «انتخاب‌شده» علامت می‌خورد. */
    public function testStoredActiveContextCountsAsChosen(): void
    {
        $doctor = $this->makeDoctor();
        $clinic = $this->makeClinic();
        $clinic->getDoctors()->add($doctor);
        $this->em->persist(new ClinicDoctorPermission($clinic, $doctor));
        $this->em->persist(new UserActiveContext(
            $doctor->getUser(),
            $clinic->getUuid(),
            EntityContext::TYPE_CLINIC,
        ));
        $this->em->flush();

        $resolver = static::getContainer()->get(\App\Shared\Context\EntityContextResolver::class);
        $context  = $resolver->resolve($doctor->getUser());

        self::assertTrue($context->chosen);
        self::assertSame(['clinic', $clinic->getId()], $context->toEntityPair());
    }
}
