<?php

namespace App\Tests\Policy;

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

/**
 * آزمایشگاه قانون — تسک ۱۰.
 *
 * مهم‌ترین تستِ این فایل `testSimulationWritesNothingButItsOwnRun` است: هر بار که کسی
 * `PolicySimulator` را عوض کند، همان تست جلوی نوشتنِ ناخواسته را می‌گیرد.
 */
class PolicySimulationTest extends ApiTestCase
{
    /** @return array{0: User, 1: ServiceSection, 2: DoctorAddress, 3: Doctor} */
    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);

        $doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $doctor     = new Doctor($doctorUser, 'دکتر آزمون');
        $this->em->persist($doctor);
        $this->em->flush();

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

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

        return $item;
    }

    /** نوبت گذشتهٔ ثبت‌شده — نمونهٔ آزمایش از همین‌ها ساخته می‌شود. */
    private function pastAppointment(
        Doctor $doctor,
        User $patient,
        ServiceItem $service,
        Clinic|int $clinicId,
        int $daysAgo,
        int $price = 1_000_000,
    ): Appointment {
        $start = time() - $daysAgo * 86400;

        $appointment = new Appointment($doctor, $patient, $start, $start + 1200);
        $appointment->assignTenantPair('clinic', is_int($clinicId) ? $clinicId : (int) $clinicId->getId());
        $appointment->setServiceItem($service);
        $appointment->setVisitPriceRials($price);
        $appointment->setPatientName('بیمار نمونه');
        $appointment->transitionTo(Appointment::STATUS_CONFIRMED);
        $appointment->transitionTo(Appointment::STATUS_COMPLETED);

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

        return $appointment;
    }

    /** @param array<string, mixed> $body */
    private function draft(User $user, array $body): array
    {
        $created = $this->authJson('POST', '/api/v1/policy', $user, $body);
        self::assertSame(201, $this->responseCode(), json_encode($created, JSON_UNESCAPED_UNICODE));

        return $created['data'];
    }

    /** @param string[] $tables */
    private function countRows(array $tables): array
    {
        $connection = $this->em->getConnection();
        $counts     = [];

        foreach ($tables as $table) {
            $counts[$table] = (int) $connection->fetchOne("SELECT COUNT(*) FROM $table");
        }

        return $counts;
    }

    // ── الگوها ──────────────────────────────────────────────────────────────

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

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

        $keys = array_column($body['data'], 'key');

        self::assertContains('min_days_between_sessions', $keys);
        self::assertContains('vip_discount', $keys);

        $vip = current(array_filter($body['data'], static fn (array $t): bool => $t['key'] === 'vip_discount'));

        self::assertSame('pricing', $vip['category']);
        self::assertSame(['visit_count', 'percent'], array_column($vip['inputs'], 'key'));
    }

    /** الگو باید همان قانونی را بسازد که کاربر دستی می‌ساخت — نه چیز دیگری. */
    public function testTemplateBuildsAValidPolicy(): void
    {
        [$user] = $this->clinicWithBranch();

        $policy = $this->draft($user, [
            'name'     => 'تخفیف مشتری وفادار',
            'template' => 'vip_discount',
            'values'   => ['visit_count' => 3, 'percent' => 15],
        ]);

        self::assertSame('pricing', $policy['category']);
        self::assertSame(
            [['field' => 'visit_count', 'operator' => 'greater_than', 'value' => 3]],
            $policy['condition']['conditions'],
        );
        self::assertSame([['type' => 'discount_percent', 'value' => 15]], $policy['effects']);
    }

    /**
     * ⭐ هر شش الگو باید قانونِ **معتبر** بسازند.
     *
     * الگو میان‌بُر است، نه مسیر دوم: اگر خروجی یکی از آن‌ها از اعتبارسنجی عادی رد
     * نشود، کاربر با یک کلیک قانونی می‌سازد که هیچ‌وقت کار نمی‌کند.
     *
     * @param array<string, mixed> $values
     */
    #[\PHPUnit\Framework\Attributes\DataProvider('templateCases')]
    public function testEveryTemplateBuildsAValidPolicy(string $key, array $values, string $category): void
    {
        [$user, , $address] = $this->clinicWithBranch();

        // الگوی نقش‌محور به یک نوع منبع واقعی نیاز دارد.
        if (isset($values['role'])) {
            $type = $this->authJson('POST', '/api/v1/resource-types', $user, [
                'address_uuid' => $address->getUuid(),
                'code'         => 'surgeon',
                'name'         => 'جراح',
            ]);
            self::assertSame(201, $this->responseCode(), json_encode($type, JSON_UNESCAPED_UNICODE));

            $values['role'] = 'surgeon';
        }

        $policy = $this->draft($user, [
            'name'     => sprintf('الگوی %s', $key),
            'template' => $key,
            'values'   => $values,
        ]);

        self::assertSame($category, $policy['category']);
        self::assertNotSame([], $policy['effects'], 'قانونی بدون اثر، قانون نیست');

        // و باید از مسیر عادیِ آزمایش و فعال‌سازی رد شود.
        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);
        self::assertSame(201, $this->responseCode());

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

    public static function templateCases(): array
    {
        return [
            'فاصلهٔ جلسات'   => ['min_days_between_sessions', ['days' => 21], 'spacing'],
            'حداقل مدت'      => ['complex_min_duration', ['minutes' => 60], 'timing'],
            'زمان اضافه'     => ['extra_time_for_many_items', ['item_count' => 2, 'minutes' => 15], 'timing'],
            'نقش لازم'       => ['surgery_needs_surgeon', ['role' => 'surgeon'], 'resource'],
            'رضایت والدین'   => ['minor_needs_consent', ['age' => 18], 'eligibility'],
            'تخفیف وفادار'   => ['vip_discount', ['visit_count' => 3, 'percent' => 15], 'pricing'],
        ];
    }

    /**
     * ⭐ آزمایش نباید هیچ نیمه‌حالتی جا بگذارد که **flushِ بعدیِ همین درخواست** ثبتش کند.
     *
     * این دقیقاً همان باگی است که `finally { rollback(); clear(); }` جلویش را می‌گیرد و
     * پیدا کردنش روزها می‌برد: خطا در صفحهٔ آزمایش ظاهر نمی‌شود، در عملیاتِ بعدی ظاهر
     * می‌شود.
     */
    public function testSimulationLeavesNoPendingStateForALaterFlush(): void
    {
        [$user, , $address] = $this->clinicWithBranch();

        $policy = $this->draft($user, [
            'category' => 'pricing',
            'name'     => 'قانون آزمایشی',
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        $entity = static::getContainer()
            ->get(\App\Policy\Repository\PolicyRepository::class)
            ->findOneBy(['uuid' => $policy['uuid']]);

        // یک entity در انتظار flush — دقیقاً وضعیتی که سناریوی خطرناک با آن شروع می‌شود.
        $pending = new \App\Resource\Entity\ResourceType(
            $address->tenantEntityType(),
            $address->tenantEntityId(),
            'pending_type',
            'نوع در انتظار',
        );
        $this->em->persist($pending);

        static::getContainer()->get(\App\Policy\Simulation\PolicySimulator::class)->simulate($entity, 5);

        self::assertSame(
            0,
            $this->em->getConnection()->getTransactionNestingLevel(),
            'تراکنش آزمایش باید بسته شده باشد',
        );

        // flushِ بعدی نباید چیزی از قبل از آزمایش را ثبت کند.
        $this->em->flush();

        $written = (int) $this->em->getConnection()->fetchOne(
            'SELECT COUNT(*) FROM resource_types WHERE code = ?',
            ['pending_type'],
        );

        self::assertSame(0, $written, 'آزمایش نباید حالتِ در انتظار را به ثبت برساند');
    }

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

        $this->authJson('POST', '/api/v1/policy', $user, [
            'name'     => 'بدون مقدار',
            'template' => 'vip_discount',
            'values'   => ['visit_count' => 3],
        ]);

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

    // ── شبیه‌سازی ───────────────────────────────────────────────────────────

    /** ⭐ ارزشمندترین تست این تسک. */
    public function testSimulationWritesNothingButItsOwnRun(): void
    {
        [$user, $section, $address, $doctor] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر');
        $clinic  = $address->getClinicId();

        for ($i = 1; $i <= 3; $i++) {
            $this->pastAppointment($doctor, $user, $service, (int) $clinic, $i * 10);
        }

        $policy = $this->draft($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف ۱۰٪',
            'effects'  => [['type' => 'discount_percent', 'value' => 10]],
        ]);

        $tables = ['appointments', 'price_snapshots', 'resource_occupancy', 'policies', 'policy_version_logs'];
        $before = $this->countRows($tables);
        $runsBefore = $this->countRows(['policy_simulation_runs'])['policy_simulation_runs'];

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);
        self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        self::assertSame($before, $this->countRows($tables), 'شبیه‌سازی نباید هیچ ردیفی بنویسد');

        // دیتابیس تست هرگز ریست نمی‌شود، پس تفاوت شمرده می‌شود نه مقدار مطلق.
        self::assertSame(
            $runsBefore + 1,
            $this->countRows(['policy_simulation_runs'])['policy_simulation_runs'],
            'تنها ردیفی که باید نوشته شود، خودِ نتیجهٔ آزمایش است',
        );
    }

    public function testPricingSimulationShowsThePerAppointmentDifference(): void
    {
        [$user, $section, $address, $doctor] = $this->clinicWithBranch();
        $service = $this->service($section, 'فیلر');

        $this->pastAppointment($doctor, $user, $service, (int) $address->getClinicId(), 5, 2_000_000);

        $policy = $this->draft($user, [
            'category' => 'pricing',
            'name'     => 'تخفیف ۲۵٪',
            'effects'  => [['type' => 'discount_percent', 'value' => 25]],
        ]);

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);

        self::assertSame(1, $body['data']['sample_size']);
        self::assertSame(1, $body['data']['affected_count']);
        self::assertSame(100, $body['data']['affected_percent']);
        self::assertSame('high', $body['data']['severity']);

        $row = $body['data']['rows'][0];

        self::assertSame('2,000,000 ریال', $row['before']);
        self::assertSame('1,500,000 ریال', $row['after']);
    }

    /** کلینیک تازه نوبتی ندارد؛ اگر این حالت خطا بود، هرگز قانونی فعال نمی‌کرد. */
    public function testEmptySampleSucceedsWithAWarning(): void
    {
        [$user] = $this->clinicWithBranch();

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

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);

        self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
        self::assertSame(0, $body['data']['sample_size']);
        self::assertSame('none', $body['data']['severity']);
        self::assertSame('داده‌ای برای آزمایش نیست', $body['data']['warning']);
    }

    /** قانونی که همهٔ نمونه را رد می‌کند تقریباً همیشه اشتباه نوشته شده. */
    public function testAPolicyThatRejectsEverythingIsFlaggedHigh(): void
    {
        [$user, $section, $address, $doctor] = $this->clinicWithBranch();
        $service = $this->service($section, 'بوتاکس');

        for ($i = 1; $i <= 3; $i++) {
            $this->pastAppointment($doctor, $user, $service, (int) $address->getClinicId(), $i);
        }

        $policy = $this->draft($user, [
            'category' => 'selection',
            'name'     => 'توقف کامل خدمت',
            'effects'  => [['type' => 'forbid', 'reason' => 'این خدمت متوقف است']],
        ]);

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);

        self::assertSame(3, $body['data']['affected_count']);
        self::assertSame('high', $body['data']['severity']);
        self::assertSame('رد می‌شد', $body['data']['rows'][0]['after']);
    }

    /** شرطی که هرگز برقرار نمی‌شود هم هشدار است، نه موفقیت. */
    public function testAPolicyThatMatchesNothingIsFlaggedNone(): void
    {
        [$user, $section, $address, $doctor] = $this->clinicWithBranch();
        $service = $this->service($section, 'مشاوره');

        $this->pastAppointment($doctor, $user, $service, (int) $address->getClinicId(), 2);

        $policy = $this->draft($user, [
            'category'  => 'timing',
            'name'      => 'فقط برای سبد بزرگ',
            'condition' => ['match' => 'all', 'conditions' => [
                ['field' => 'item_count', 'operator' => 'greater_than', 'value' => 50],
            ]],
            'effects'   => [['type' => 'add_duration_minutes', 'value' => 15]],
        ]);

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);

        self::assertSame(1, $body['data']['sample_size']);
        self::assertSame(0, $body['data']['affected_count']);
        self::assertSame('none', $body['data']['severity']);
    }

    // ── دروازهٔ فعال‌سازی ────────────────────────────────────────────────────

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

        $policy = $this->draft($user, [
            'category' => 'timing',
            'name'     => 'قانون آزمایش‌نشده',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 45]],
        ]);

        $body = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/activate", $user);

        self::assertSame(422, $this->responseCode());
        self::assertSame('ابتدا قانون را آزمایش کنید و نتیجه را ببینید', $body['errors'][0]['message']);
    }

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

        $policy = $this->draft($user, [
            'category' => 'timing',
            'name'     => 'قانون نسخه‌دار',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 45]],
        ]);

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

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/version", $user, [
            'effects' => [['type' => 'min_duration_minutes', 'value' => 90]],
        ]);
        self::assertSame(200, $this->responseCode());

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/activate", $user);
        self::assertSame(422, $this->responseCode(), 'آزمایش نسخهٔ ۱ نباید نسخهٔ ۲ را باز کند');

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);
        $activated = $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/activate", $user);

        self::assertSame(200, $this->responseCode());
        self::assertTrue($activated['data']['active']);
    }

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

        $policy = $this->draft($user, [
            'category' => 'timing',
            'name'     => 'قانون با تاریخچه',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 30]],
        ]);

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);
        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user);

        $body = $this->authJson('GET', "/api/v1/policy/{$policy['uuid']}/simulations", $user);

        self::assertSame(200, $this->responseCode());
        self::assertCount(2, $body['data']);
    }

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

        $policy = $this->draft($user, [
            'category' => 'timing',
            'name'     => 'قانون با نمونهٔ بزرگ',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 30]],
        ]);

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $user, ['sample_size' => 500]);

        // سقف بی‌صدا اعمال نمی‌شود: کاربری که ۵۰۰ خواسته باید بداند نگرفته.
        self::assertSame(422, $this->responseCode());
    }

    public function testSimulatingAnotherClinicsPolicyIsNotFound(): void
    {
        [$owner] = $this->clinicWithBranch();
        [$other] = $this->clinicWithBranch();

        $policy = $this->draft($owner, [
            'category' => 'timing',
            'name'     => 'قانون کلینیک اول',
            'effects'  => [['type' => 'min_duration_minutes', 'value' => 30]],
        ]);

        $this->authJson('POST', "/api/v1/policy/{$policy['uuid']}/simulate", $other);

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