<?php

namespace App\Tests\Policy;

use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\DoctorAddress;
use App\Tests\ApiTestCase;

/**
 * موتور قوانین شش‌دسته‌ای — بند ۸ مستند.
 *
 * تأکید تست‌ها روی سه چیز است که خرابیشان بی‌صداست: ترکیب اثرها (max/sum/veto)،
 * ترتیب حل تناقض (اولویت ← اختصاصی‌بودن ← قدمت)، و نسخه‌پذیری (قانون ویرایش
 * نمی‌شود، نسخهٔ تازه می‌گیرد).
 */
class PolicyEngineTest extends ApiTestCase
{
    /** @return array{0: User, 1: ServiceSection, 2: DoctorAddress} */
    private function clinicWithBranch(): array
    {
        $user   = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $clinic = new Clinic($user);
        $clinic->setName('کلینیک قوانین');
        $this->em->persist($clinic);
        $this->em->flush();

        $section = new ServiceSection('clinic', $clinic->getId(), 'زیبایی');
        $this->em->persist($section);

        $address = DoctorAddress::forClinic($clinic->getId());
        $address->setName('شعبهٔ مرکزی');
        $this->em->persist($address);
        $this->em->flush();

        return [$user, $section, $address];
    }

    private function service(ServiceSection $section, string $name, int $solo = 20, int $price = 1_000_000): ServiceItem
    {
        $item = new ServiceItem($section, $name);
        $item->setSoloDurationMinutes($solo);
        $item->setPriceRials($price);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    /**
     * قانون تازه **پیش‌نویس** است؛ تا فعال نشود اجرا نمی‌شود.
     *
     * @param array<string, mixed> $body
     */
    private function policy(User $user, array $body, bool $activate = true): array
    {
        $created = $this->authJson('POST', '/api/v1/policy', $user, $body);
        self::assertSame(201, $this->responseCode(), json_encode($created, JSON_UNESCAPED_UNICODE));

        if (!$activate) {
            return $created['data'];
        }

        // فعال‌سازی از تسک ۱۰ به بعد یک اجرای آزمایشی از **همین نسخه** می‌خواهد.
        $this->authJson('POST', "/api/v1/policy/{$created['data']['uuid']}/simulate", $user);
        self::assertSame(201, $this->responseCode());

        $active = $this->authJson('POST', "/api/v1/policy/{$created['data']['uuid']}/activate", $user);
        self::assertSame(200, $this->responseCode(), json_encode($active, JSON_UNESCAPED_UNICODE));

        return $active['data'];
    }

    /** پیش‌نویس ماندنِ قانون تازه عمدی است: نوشتن قانون نباید یعنی اجرای آن. */
    public function testANewPolicyIsADraftUntilActivated(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'خدمت پیش‌نویس', 20);

        $draft = $this->policy($user, [
            'category' => 'timing',
            'name'     => 'قانون پیش‌نویس',
            'effects'  => [['type' => 'add_duration_minutes', 'value' => 30]],
        ], activate: false);

        self::assertFalse($draft['active']);
        self::assertSame(20, $this->preview($user, $service, $address)['data']['total_minutes']);
    }

    /** @param array<string, mixed> $extra */
    private function preview(User $user, ServiceItem $service, DoctorAddress $address, array $extra = []): array
    {
        return $this->authJson('POST', '/api/v1/appointment-plan/preview', $user, $extra + [
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $address->getUuid(),
        ]);
    }

    /** @param array<string, mixed> $extra */
    private function quote(User $user, ServiceItem $service, DoctorAddress $address, array $extra = []): array
    {
        return $this->authJson('POST', '/api/v1/pricing/quote', $user, $extra + [
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $address->getUuid(),
        ]);
    }

    // ── شِما ────────────────────────────────────────────────────────────────

    public function testSchemaIsAClosedListPerCategory(): void
    {
        $user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $body = $this->authJson('GET', '/api/v1/policy-schema', $user);

        self::assertSame(200, $this->responseCode());

        $schema = $body['data'];

        self::assertArrayHasKey('timing', $schema);
        self::assertContains('equals', array_column($schema['timing']['operators'], 'value'));

        self::assertSame(
            ['min_duration_minutes', 'add_duration_minutes'],
            array_column($schema['timing']['effects'], 'type'),
        );
        self::assertSame(
            ['max', 'sum'],
            array_column($schema['timing']['effects'], 'combination'),
        );

        // فیلد قیمتی در دستهٔ زمان جایی ندارد — همین بسته‌بودن نکتهٔ اصلی شِماست.
        self::assertNotContains('subtotal_rials', $schema['timing']['fields']);

        // فرم باید عملگرها را per فیلد فیلتر کند، وگرنه کاربر «برچسب > ۵» می‌سازد و
        // ۴۲۲ می‌گیرد بی‌آنکه بفهمد چرا.
        $meta = array_column($schema['eligibility']['field_meta'], null, 'key');

        self::assertSame('int', $meta['patient_age']['type']);
        // عدد یازده عملگر ندارد؛ فقط آن‌هایی که روی عدد معنا دارند.
        self::assertSame(
            ['equals', 'not_equals', 'greater_than', 'greater_or_equal', 'less_than', 'less_or_equal', 'between', 'in', 'not_in'],
            $meta['patient_age']['operators'],
        );
        self::assertSame(['contains'], $meta['patient_tags']['operators']);
        self::assertSame('سن بیمار', $meta['patient_age']['label']);
    }

    public function testFieldOutsideTheCategoryIsRejectedAtCreateTime(): void
    {
        [$user] = $this->clinicWithBranch();

        $body = $this->authJson('POST', '/api/v1/policy', $user, [
            'category'  => 'timing',
            'name'      => 'قانون بی‌ربط',
            'condition' => ['match' => 'all', 'conditions' => [['field' => 'subtotal_rials', 'operator' => 'greater_than', 'value' => 10]]],
        ]);

        self::assertSame(422, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
    }

    public function testEffectOutsideTheCategoryIsRejectedAtCreateTime(): void
    {
        [$user] = $this->clinicWithBranch();

        $this->authJson('POST', '/api/v1/policy', $user, [
            'category' => 'timing',
            'name'     => 'تخفیف در دستهٔ زمان',
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        self::assertSame(422, $this->responseCode());
    }

    // ── ترکیب اثرها ─────────────────────────────────────────────────────────

    /** «حداقل مدت» با max ترکیب می‌شود: سخت‌گیرترین قانون برنده است. */
    public function testMinDurationTakesTheLargestNotTheLast(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20);

        $this->policy($user, [
            'category' => 'timing',
            'name'     => 'حداقل ۴۵ دقیقه',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 45]],
        ]);

        $this->policy($user, [
            'category' => 'timing',
            'name'     => 'حداقل ۶۰ دقیقه',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 60]],
        ]);

        $plan = $this->preview($user, $service, $address);

        self::assertSame(200, $this->responseCode(), json_encode($plan, JSON_UNESCAPED_UNICODE));
        self::assertSame(60, $plan['data']['total_minutes']);
    }

    /** «افزودن مدت» با sum ترکیب می‌شود — دو قانون ۱۰ دقیقه‌ای یعنی ۲۰ دقیقه. */
    public function testAddDurationSumsAcrossPolicies(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'پاکسازی', 20);

        foreach (['ضدعفونی اضافه', 'آماده‌سازی اضافه'] as $name) {
            $this->policy($user, [
                'category' => 'timing',
                'name'     => $name,
                'effects'  => [['type' => 'add_duration_minutes', 'value' => 10]],
            ]);
        }

        $plan = $this->preview($user, $service, $address);

        self::assertSame(40, $plan['data']['total_minutes']);
    }

    /** یک ممنوعیت کافی است؛ ممنوعیت رأی اکثریت نیست. */
    public function testOneForbidVetoesTheSelection(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'بوتاکس', 20);

        $this->policy($user, [
            'category'     => 'selection',
            'name'         => 'این خدمت فعلاً ارائه نمی‌شود',
            'service_uuid' => $service->getUuid(),
            'effects'      => [['type' => 'forbid', 'reason' => 'این خدمت موقتاً متوقف است']],
        ]);

        $body = $this->authJson('POST', '/api/v1/service-selection/validate', $user, [
            'item_uuids'  => [$service->getUuid()],
            'branch_uuid' => $address->getUuid(),
        ]);

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
        self::assertFalse($body['data']['valid']);
        self::assertSame('policy_forbidden', $body['data']['errors'][0]['code']);
        self::assertSame('این خدمت موقتاً متوقف است', $body['data']['errors'][0]['message']);
    }

    // ── شرط‌ها ──────────────────────────────────────────────────────────────

    public function testConditionThatDoesNotMatchLeavesThePlanAlone(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'مشاوره', 20);

        $this->policy($user, [
            'category'  => 'timing',
            'name'      => 'فقط برای انتخاب‌های پرتعداد',
            'condition' => ['match' => 'all', 'conditions' => [['field' => 'item_count', 'operator' => 'greater_than', 'value' => 3]]],
            'effects'   => [['type' => 'add_duration_minutes', 'value' => 30]],
        ]);

        $plan = $this->preview($user, $service, $address);

        self::assertSame(20, $plan['data']['total_minutes']);
    }

    /** حقیقتِ غایب یعنی شرط **برقرار نیست** — نه اینکه بی‌صدا رد شود. */
    public function testMissingFactFailsTheClauseInsteadOfPassingIt(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر بدن', 20);

        $this->policy($user, [
            'category'  => 'timing',
            'name'      => 'وابسته به سن',
            'condition' => ['match' => 'all', 'conditions' => [['field' => 'patient_age', 'operator' => 'less_than', 'value' => 18]]],
            'effects'   => [['type' => 'add_duration_minutes', 'value' => 15]],
        ]);

        // پیش‌نمایش برنامه سن بیمار را نمی‌فرستد.
        $plan = $this->preview($user, $service, $address);

        self::assertSame(20, $plan['data']['total_minutes']);
    }

    public function testExpiredPolicyIsIgnored(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'میکرونیدلینگ', 20);

        $this->policy($user, [
            'category'   => 'timing',
            'name'       => 'کمپین نوروز',
            'valid_from' => time() - 86400 * 30,
            'valid_to'   => time() - 86400,
            'effects'    => [['type' => 'add_duration_minutes', 'value' => 25]],
        ]);

        $plan = $this->preview($user, $service, $address);

        self::assertSame(20, $plan['data']['total_minutes']);
    }

    public function testDeactivatedPolicyIsIgnored(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'هیدرافیشیال', 20);

        $policy = $this->policy($user, [
            'category' => 'timing',
            'name'     => 'قانون خاموش‌شدنی',
            'effects'  => [['type' => 'add_duration_minutes', 'value' => 20]],
        ]);

        self::assertSame(40, $this->preview($user, $service, $address)['data']['total_minutes']);

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/deactivate", $user);
        self::assertSame(200, $this->responseCode());

        self::assertSame(20, $this->preview($user, $service, $address)['data']['total_minutes']);
    }

    // ── ترتیب و اختصاصی‌بودن ────────────────────────────────────────────────

    /**
     * در تساوی اولویت، قانونِ اختصاصی‌تر اول می‌نشیند — همان که برچسبش روی فاکتور
     * می‌رود.
     */
    public function testMoreSpecificPolicyIsRankedFirstOnEqualPriority(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'فیلر', 20, 2_000_000);

        $this->policy($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف عمومی محیط',
            'effects'  => [['type' => 'discount_percent', 'value' => 5]],
        ]);

        $this->policy($user, [
            'category'     => 'pricing',
            'name'         => 'تخفیف همین سرویس',
            'service_uuid' => $service->getUuid(),
            'effects'      => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        $quote = $this->quote($user, $service, $address);

        self::assertSame(200, $this->responseCode(), json_encode($quote, JSON_UNESCAPED_UNICODE));

        $applied = $quote['data']['breakdown']['sources']['applied_policies'];

        self::assertSame('تخفیف همین سرویس', $applied[0]['name']);
        // درصدها جمع می‌شوند: ۵٪ + ۱۰٪ روی ۲٬۰۰۰٬۰۰۰
        self::assertSame(300_000, $quote['data']['discount_rials']);
    }

    public function testHigherPriorityBeatsSpecificity(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'مزوتراپی', 20, 1_000_000);

        $this->policy($user, [
            'category' => 'pricing',
            'name'     => 'قانون محیطی با اولویت بالا',
            'priority' => 100,
            'effects'  => [['type' => 'discount_percent', 'value' => 5]],
        ]);

        $this->policy($user, [
            'category'     => 'pricing',
            'name'         => 'قانون سرویسی با اولویت پایین',
            'service_uuid' => $service->getUuid(),
            'priority'     => 1,
            'effects'      => [['type' => 'discount_percent', 'value' => 5]],
        ]);

        $applied = $this->quote($user, $service, $address)['data']['breakdown']['sources']['applied_policies'];

        self::assertSame('قانون محیطی با اولویت بالا', $applied[0]['name']);
    }

    // ── نسخه ────────────────────────────────────────────────────────────────

    /**
     * قانون **ویرایش نمی‌شود**: تغییر یعنی نسخهٔ تازه، و شمارهٔ نسخه در فاکتور ثبت
     * می‌شود تا سه ماه بعد بشود گفت کدام متن اعمال شده بود (قانون پنجم مستند).
     */
    public function testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر صورت', 20, 1_000_000);

        $policy = $this->policy($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف پاییز',
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        self::assertSame(1, $policy['version']);

        $first = $this->quote($user, $service, $address);
        self::assertSame(100_000, $first['data']['discount_rials']);
        self::assertSame(1, $first['data']['breakdown']['sources']['applied_policies'][0]['version']);

        $updated = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/version", $user, [
            'effects' => [['type' => 'discount_percent', 'value' => 20]],
        ]);

        self::assertSame(200, $this->responseCode(), json_encode($updated, JSON_UNESCAPED_UNICODE));
        self::assertSame(2, $updated['data']['version']);

        // نسخهٔ تازه فعال می‌ماند؛ آزمایش دوباره لازم نیست چون قانون از قبل فعال بود.

        $second = $this->quote($user, $service, $address);
        self::assertSame(200_000, $second['data']['discount_rials']);
        self::assertSame(2, $second['data']['breakdown']['sources']['applied_policies'][0]['version']);

        // هر دو نسخه در تاریخچه می‌مانند.
        $show = $this->authJson('GET', "/api/v1/policy/{$policy['uuid']}", $user);
        self::assertSame([1, 2], array_column($show['data']['versions'], 'version'));
    }

    /**
     * ⭐ نسخهٔ تازه نمی‌تواند ادعا کند از دیروز برقرار بوده.
     *
     * نوبت‌های دیروز با متن قبلی حساب شده‌اند؛ اعتبار عقب‌رونده یعنی ردپای قیمت‌ها با
     * قانونی توضیح داده شود که آن روز وجود نداشت.
     */
    public function testANewVersionCannotStartInThePast(): void
    {
        [$user, , ] = $this->clinicWithBranch();

        $policy = $this->policy($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف پاییز',
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ], false);

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/version", $user, [
            'valid_from' => time() - 7 * 86400,
        ]);

        self::assertSame(422, $this->responseCode());

        // آینده مجاز است.
        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/version", $user, [
            'valid_from' => time() + 86400,
        ]);

        self::assertSame(200, $this->responseCode());
    }

    /**
     * ⭐ شش عملگر، هر کدام جدا. عملگری که غلط بسنجد، قانونی می‌سازد که یا همیشه
     * می‌گیرد یا هرگز — و هیچ‌کدام خطا نمی‌دهند.
     *
     */
    #[\PHPUnit\Framework\Attributes\DataProvider('operatorCases')]
    public function testEachOperatorDecidesOnItsOwn(array $clause, bool $expected): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'خدمت عملگر', 20, 1_000_000);

        $this->policy($user, [
            'category'  => 'pricing',
            'name'      => 'آزمون عملگر',
            'condition' => ['match' => 'all', 'conditions' => [$clause]],
            'effects'   => [['type' => 'discount_percent', 'value' => 50]],
        ]);

        $quote = $this->quote($user, $service, $address);

        self::assertSame(
            $expected ? 500_000 : 0,
            $quote['data']['discount_rials'],
            json_encode($clause, JSON_UNESCAPED_UNICODE),
        );
    }

    /** بدون `item_uuids` هیچ آیتم اضافه‌ای انتخاب نشده، پس `item_count` صفر است. */
    public static function operatorCases(): array
    {
        return [
            'equals می‌گیرد'        => [['field' => 'item_count', 'operator' => 'equals', 'value' => 0], true],
            'equals نمی‌گیرد'       => [['field' => 'item_count', 'operator' => 'equals', 'value' => 9], false],
            'not_equals می‌گیرد'    => [['field' => 'item_count', 'operator' => 'not_equals', 'value' => 9], true],
            'not_equals نمی‌گیرد'   => [['field' => 'item_count', 'operator' => 'not_equals', 'value' => 0], false],
            'greater_than می‌گیرد'  => [['field' => 'item_count', 'operator' => 'greater_than', 'value' => -1], true],
            'greater_than نمی‌گیرد' => [['field' => 'item_count', 'operator' => 'greater_than', 'value' => 5], false],
            'less_than می‌گیرد'     => [['field' => 'item_count', 'operator' => 'less_than', 'value' => 5], true],
            'less_than نمی‌گیرد'    => [['field' => 'item_count', 'operator' => 'less_than', 'value' => 0], false],
            'in می‌گیرد'            => [['field' => 'item_count', 'operator' => 'in', 'value' => [0, 2]], true],
            'in نمی‌گیرد'           => [['field' => 'item_count', 'operator' => 'in', 'value' => [7, 8]], false],
            'contains نمی‌گیرد'     => [['field' => 'patient_tags', 'operator' => 'contains', 'value' => 'vip'], false],
        ];
    }

    /**
     * ⭐ قانون `spacing` در لحظهٔ **رزرو موقت** اجرا می‌شود، نه هنگام تولید کاندید.
     *
     * هزینه‌اش یک اسلات است که نمایش داده می‌شود و بعد رد می‌شود؛ سودش این است که
     * جستجوی وقت به‌ازای هر کاندید یک کوئری تاریخچهٔ بیمار نمی‌زند. این تست همان مرز را
     * پین می‌کند: نوبت نزدیک رد می‌شود، نوبت دور می‌گذرد.
     */
    public function testSpacingRejectsABookingTooCloseToTheLastOne(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20, 1_000_000);

        $this->policy($user, [
            'category' => 'spacing',
            'name'     => 'حداقل ۲۱ روز بین جلسات',
            'effects'  => [['type' => 'min_days_between', 'value' => 21]],
        ]);

        $doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $doctor     = new \App\Doctor\Entity\Doctor($doctorUser, 'دکتر فاصله');
        $this->em->persist($doctor);
        $this->em->flush();

        $patient = $this->createUser(['ROLE_USER']);
        $last    = time() - 5 * 86400;

        $previous = new \App\Appointment\Entity\Appointment($doctor, $patient, $last, $last + 1200);
        $previous->assignTenantPair($address->tenantEntityType(), $address->tenantEntityId());
        $previous->setServiceItem($this->em->getRepository(ServiceItem::class)->find($service->getId()));
        $previous->setAddressId($address->getId());
        $previous->setPatientName('بیمار فاصله');
        $previous->transitionTo(\App\Appointment\Entity\Appointment::STATUS_CONFIRMED);
        $this->em->persist($previous);
        $this->em->flush();

        $guard = static::getContainer()->get(\App\Policy\Service\BookingPolicyGuard::class);

        // پنج روز بعد از جلسهٔ قبلی → رد.
        $rejected = false;
        try {
            $guard->assertSpacing($patient, $service, $address, $last + 5 * 86400);
        } catch (\App\Shared\Exception\AppException $e) {
            $rejected = true;
            self::assertStringContainsString('۲۱', str_replace(
                ['0','1','2','3','4','5','6','7','8','9'],
                ['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹'],
                $e->getMessage(),
            ));
        }
        self::assertTrue($rejected, 'فاصلهٔ کمتر از قانون باید رد شود');

        // سی روز بعد → می‌گذرد.
        $guard->assertSpacing($patient, $service, $address, $last + 30 * 86400);
        self::assertTrue(true);
    }

    /**
     * ⭐ `specificity` هنگام **ذخیره** حساب می‌شود و در تساوی اولویت تصمیم می‌گیرد.
     *
     * محاسبه‌اش در زمان اجرا یعنی کاری که یک بار در عمر قانون کافی بود، در هر رزرو
     * تکرار شود؛ و ذخیره‌شدنش یعنی می‌شود روزی مرتب‌سازی را به SQL برد.
     */
    public function testSpecificityIsStoredAndDecidesTiesAtEqualPriority(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20, 1_000_000);

        // قانون عام: بدون دامنه، بدون شرط.
        $broad = $this->policy($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف عمومی',
            'priority' => 5,
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        // قانون خاص: همان اولویت، ولی سرویس و یک شرط دارد.
        $narrow = $this->policy($user, [
            'category'     => 'pricing',
            'name'         => 'تخفیف همین سرویس',
            'priority'     => 5,
            'service_uuid' => $service->getUuid(),
            'condition'    => ['match' => 'all', 'conditions' => [
                ['field' => 'item_count', 'operator' => 'greater_or_equal', 'value' => 0],
            ]],
            'effects'      => [['type' => 'discount_percent', 'value' => 40]],
        ]);

        self::assertSame(0, $broad['specificity'], 'قانون بی‌دامنه و بی‌شرط');
        self::assertSame(5, $narrow['specificity'], 'سرویس ۴ + یک شرط ۱');

        // هر دو اعمال می‌شوند (تخفیف درصدی جمع می‌شود)، ولی **ترتیب** مال specificity است:
        // اختصاصی‌تر اول می‌آید، و همان ترتیبی است که اثرهای «اولی برنده» را تعیین می‌کند.
        $quote = $this->quote($user, $service, $address);
        $names = array_column($quote['data']['breakdown']['sources']['applied_policies'], 'name');

        self::assertSame(['تخفیف همین سرویس', 'تخفیف عمومی'], $names, 'اختصاصی‌تر باید اول باشد');
    }

    // ── جداسازی محیط ────────────────────────────────────────────────────────

    public function testPolicyOfAnotherClinicIsNeitherVisibleNorApplied(): void
    {
        [$owner, , ] = $this->clinicWithBranch();
        [$other, $section, $address] = $this->clinicWithBranch();

        $service = $this->service($section, 'خدمت کلینیک دوم', 20, 1_000_000);

        $foreign = $this->policy($owner, [
            'category' => 'pricing',
            'name'     => 'تخفیف کلینیک اول',
            'effects'  => [['type' => 'discount_percent', 'value' => 50]],
        ]);

        $this->authJson('GET', "/api/v1/policy/{$foreign['uuid']}", $other);
        self::assertSame(404, $this->responseCode());

        $quote = $this->quote($other, $service, $address);

        self::assertSame(0, $quote['data']['discount_rials']);
        self::assertArrayNotHasKey('applied_policies', $quote['data']['breakdown']['sources']);
    }
}
