<?php

namespace App\Tests\Cancellation;

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\Patient\Entity\PatientRecord;
use App\Payment\Entity\Payment;
use App\Settlement\Service\WalletService;
use App\Tag\Entity\TenantTag;
use App\Tests\ApiTestCase;

/**
 * سیاست لغو، جریمه و عدم حضور — تسک ۱۳.
 *
 * دو قاعده که شکستنشان گران است: لغو توسط کلینیک هرگز جریمه ندارد، و جریمه هرگز از
 * مبلغ پرداختی بیشتر نمی‌شود.
 */
class CancellationTest extends ApiTestCase
{
    private int $slotCursor = 0;

    /** @return array{0: User, 1: ServiceSection, 2: DoctorAddress, 3: Doctor, 4: PatientRecord} */
    private function clinicWithPatient(): 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);

        $patientUser = $this->createUser(['ROLE_USER']);
        $patient     = new PatientRecord('clinic', (int) $clinic->getId(), $patientUser, 'clinic', (int) $clinic->getId());
        $this->em->persist($patient);
        $this->em->flush();

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

    private function service(ServiceSection $section, string $name = 'لیزر'): ServiceItem
    {
        $item = new ServiceItem($section, $name);
        $item->setSoloDurationMinutes(30);
        $item->setPriceRials(4_000_000);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    /** @param array<string, mixed> $body */
    private function savePolicy(User $user, array $body): array
    {
        $saved = $this->authJson('PUT', '/api/v1/cancellation-policy', $user, $body);
        self::assertSame(200, $this->responseCode(), json_encode($saved, JSON_UNESCAPED_UNICODE));

        return $saved['data'];
    }

    /** نوبتی در آینده، با مبلغ ثبت‌شده و در صورت نیاز پرداخت موفق. */
    private function appointment(
        Doctor $doctor,
        PatientRecord $patient,
        ServiceItem $service,
        int $clinicId,
        int $hoursAhead,
        int $price = 4_000_000,
        int $paid = 0,
    ): Appointment {
        $em = static::getContainer()->get(\Doctrine\ORM\EntityManagerInterface::class);

        $start = time() + $hoursAhead * 3600 + (++$this->slotCursor) * 60;

        $appointment = new Appointment(
            $em->getRepository(Doctor::class)->find($doctor->getId()),
            $em->getRepository(PatientRecord::class)->find($patient->getId())->getUser(),
            $start,
            $start + 1800,
        );
        $appointment->assignTenantPair('clinic', $clinicId);
        $appointment->setServiceItem($em->getRepository(ServiceItem::class)->find($service->getId()));
        $appointment->setVisitPriceRials($price);
        $appointment->setPatientName('بیمار لغو');
        $appointment->transitionTo(Appointment::STATUS_CONFIRMED);

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

        if ($paid > 0) {
            $payment = new Payment($appointment->getUser(), $paid, 'zarinpal', Payment::TYPE_APPOINTMENT);
            $payment->assignTenantPair('clinic', $clinicId);
            $payment->setAppointment($appointment);
            $payment->setStatus(Payment::STATUS_SUCCESS);
            $em->persist($payment);
            $em->flush();
        }

        return $appointment;
    }

    private function wallet(): WalletService
    {
        return static::getContainer()->get(WalletService::class);
    }

    // ── پیش‌نمایش ───────────────────────────────────────────────────────────

    public function testInsideTheFreeWindowThereIsNoPenalty(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, [
            'free_window_hours' => 24,
            'penalty_mode'      => 'percent',
            'penalty_value'     => 50,
            'deposit_refundable' => false,
        ]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 48, paid: 4_000_000);

        $preview = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $user);

        self::assertSame(200, $this->responseCode(), json_encode($preview, JSON_UNESCAPED_UNICODE));
        self::assertSame(0, $preview['data']['penalty_rials']);
        self::assertTrue($preview['data']['deposit_refundable']);
        self::assertTrue($preview['data']['within_free_window']);
    }

    public function testOutsideTheFreeWindowThePercentagePenaltyApplies(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, [
            'free_window_hours'  => 24,
            'penalty_mode'       => 'percent',
            'penalty_value'      => 50,
            'deposit_refundable' => false,
        ]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 6, paid: 4_000_000);

        $preview = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $user)['data'];

        self::assertSame(2_000_000, $preview['penalty_rials']);
        self::assertFalse($preview['deposit_refundable']);
        self::assertFalse($preview['within_free_window']);
    }

    /** ⭐ لغو توسط کلینیک هرگز جریمه ندارد، حتی یک ساعت مانده به نوبت. */
    public function testTheClinicCancellingItsOwnAppointmentIsAlwaysFree(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, ['free_window_hours' => 24, 'penalty_mode' => 'percent', 'penalty_value' => 100]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 1, paid: 4_000_000);

        $preview = $this->authJson(
            'GET',
            "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview?by=doctor",
            $user,
        )['data'];

        self::assertSame(0, $preview['penalty_rials']);
        self::assertTrue($preview['deposit_refundable']);
    }

    /** ⭐ جریمهٔ بیشتر از پرداختی یعنی بدهی، و بدهی مسئلهٔ لغو نیست. */
    public function testThePenaltyNeverExceedsWhatWasPaid(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, [
            'free_window_hours' => 24,
            'penalty_mode'      => 'fixed',
            'penalty_value'     => 9_000_000,
        ]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 2, paid: 1_000_000);

        $preview = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $user)['data'];

        self::assertSame(1_000_000, $preview['penalty_rials']);
        self::assertNotEmpty($preview['notes']);
    }

    /** نوبت نقدی: جریمه صفر می‌شود و پاسخ توضیحش را می‌دهد. */
    public function testAnUnpaidAppointmentIsNotCharged(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, ['free_window_hours' => 24, 'penalty_mode' => 'percent', 'penalty_value' => 50]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 2);

        $preview = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $user)['data'];

        self::assertSame(0, $preview['penalty_rials']);
        self::assertStringContainsString('پرداختی نداشته', implode(' ', $preview['notes']));
    }

    public function testWithoutAPolicyNothingIsCharged(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 1, paid: 4_000_000);

        $preview = $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $user)['data'];

        self::assertSame(0, $preview['penalty_rials']);
    }

    // ── لغو واقعی ───────────────────────────────────────────────────────────

    public function testCancellingChargesTheWalletAndReleasesTheSlot(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, ['free_window_hours' => 24, 'penalty_mode' => 'percent', 'penalty_value' => 25]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 3, paid: 4_000_000);

        // کیف پول باید موجودی داشته باشد وگرنه جریمه کسر نمی‌شود.
        $em = static::getContainer()->get(\Doctrine\ORM\EntityManagerInterface::class);
        $this->wallet()->charge($em->getRepository(\App\Auth\Entity\User::class)->find($appointment->getUser()->getId()), 5_000_000);

        $body = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user);

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
        self::assertSame(1_000_000, $body['data']['penalty_rials']);
        self::assertTrue($body['data']['penalty_charged']);
        self::assertSame('cancelled_by_user', $body['data']['status']);

        $patientUser = static::getContainer()->get(\Doctrine\ORM\EntityManagerInterface::class)
            ->getRepository(\App\Auth\Entity\User::class)
            ->find($appointment->getUser()->getId());

        self::assertSame(4_000_000, $this->wallet()->balance($patientUser), 'جریمه باید از کیف پول کسر شود');
    }

    /** موجودی ناکافی نباید لغو را شکست بدهد؛ نوبت باید آزاد شود. */
    public function testAnEmptyWalletDoesNotBlockTheCancellation(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $this->savePolicy($user, ['free_window_hours' => 24, 'penalty_mode' => 'percent', 'penalty_value' => 50]);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 3, paid: 4_000_000);

        $body = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user);

        self::assertSame(200, $this->responseCode());
        self::assertSame(2_000_000, $body['data']['penalty_rials']);
        self::assertFalse($body['data']['penalty_charged'], 'موجودی نبود، پس کسر نشد — ولی نوبت لغو شد');
        self::assertSame('cancelled_by_user', $body['data']['status']);
    }

    public function testCancellingTwiceIsRejected(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 30);

        $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user);
        self::assertSame(200, $this->responseCode());

        $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user);
        self::assertSame(409, $this->responseCode());
    }

    /** برای گذشته `no_show` یا `completed` معنا دارد، نه لغو. */
    public function testAPastAppointmentCannotBeCancelled(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 5);

        $this->em->getConnection()->executeStatement(
            'UPDATE appointments SET slot_start = ?, slot_end = ? WHERE uuid = ?',
            [time() - 7200, time() - 5400, $appointment->getUuid()],
        );
        $this->em->clear();

        $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/cancel", $user);

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

    // ── عدم حضور ────────────────────────────────────────────────────────────

    /** ⭐ سومین عدم حضور برچسب پرریسک می‌گذارد — ولی بیمار را مسدود نمی‌کند. */
    public function testTheThirdNoShowTagsThePatient(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $tag = new TenantTag('clinic', (int) $address->getClinicId(), 'پرریسک', '#dc2626');
        $this->em->persist($tag);
        $this->em->flush();

        $this->savePolicy($user, ['no_show_threshold' => 3, 'risk_tag_uuid' => $tag->getUuid()]);

        $last = null;

        for ($i = 1; $i <= 3; $i++) {
            $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 2);
            $last = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/no-show", $user);
            self::assertSame(200, $this->responseCode(), json_encode($last, JSON_UNESCAPED_UNICODE));
        }

        self::assertSame(3, $last['data']['count']);
        self::assertTrue($last['data']['tagged']);

        $reloaded = static::getContainer()->get(\Doctrine\ORM\EntityManagerInterface::class)
            ->getRepository(PatientRecord::class)
            ->find($patient->getId());

        $tagNames = array_map(static fn (TenantTag $t): string => $t->getName(), $reloaded->getTags()->toArray());

        self::assertContains('پرریسک', $tagNames);
    }

    /** ثبت دوباره روی همان نوبت، عدم حضور دوم نمی‌سازد. */
    public function testRecordingTheSameNoShowTwiceCountsOnce(): void
    {
        [$user, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        $service = $this->service($section);

        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 2);

        $first  = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/no-show", $user);
        $second = $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/no-show", $user);

        self::assertTrue($first['data']['recorded']);
        self::assertFalse($second['data']['recorded']);
        self::assertSame(1, $second['data']['count']);
    }

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

    /**
     * ⭐ سیاست سرویس بر سیاست محیط مقدم است — بدون ترکیب.
     *
     * ترکیب («پنجرهٔ رایگانِ محیط با درصدِ سرویس») یعنی هیچ‌کس نتواند بگوید عدد نهایی از
     * کجا آمد. سرویس اگر سیاست دارد، **همه‌اش** مال اوست.
     */
    public function testTheServicePolicyWinsOverTheTenantPolicy(): void
    {
        [$user, $section, , $doctor, $patient] = $this->clinicWithPatient();
        $clinicId = (int) $patient->getEntityId();
        $service  = $this->service($section);

        // پنجرهٔ محیط یک ساعت است: با ۲۴ ساعت مانده، لغو رایگان می‌شد.
        $this->savePolicy($user, [
            'free_window_hours' => 1,
            'penalty_mode'      => 'percent',
            'penalty_value'     => 10,
        ]);

        // پنجرهٔ سرویس ۴۸ ساعت است: همان لغو، جریمه دارد.
        $saved = $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/cancellation-policy", $user, [
            'free_window_hours' => 48,
            'penalty_mode'      => 'percent',
            'penalty_value'     => 50,
        ]);
        self::assertSame(200, $this->responseCode(), json_encode($saved, JSON_UNESCAPED_UNICODE));

        // عددِ نهایی می‌گوید کدام سیاست حاکم بوده: ۰ یعنی محیط، ۵۰٪ یعنی سرویس.
        $appointment = $this->appointment($doctor, $patient, $service, $clinicId, 24, 4_000_000, 4_000_000);

        $preview = $this->authJson(
            'GET',
            "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview?by=user",
            $user,
        );

        self::assertSame(2_000_000, $preview['data']['penalty_rials'], 'سیاست سرویس حاکم است، نه سیاست محیط');
        self::assertFalse($preview['data']['within_free_window']);
    }

    /**
     * ⭐ برچسب پرریسک **مسدود نمی‌کند**.
     *
     * مسدودسازی یک قانون `eligibility` جداست؛ کلینیکی که می‌خواهد بیمار پرریسک را ببیند
     * ولی بیعانه بگیرد، نباید مجبور شود برچسب را خاموش کند.
     */
    public function testATaggedPatientCanStillBook(): void
    {
        [$user, $section, , $doctor, $patient] = $this->clinicWithPatient();
        $clinicId = (int) $patient->getEntityId();
        $service  = $this->service($section);

        $this->savePolicy($user, ['no_show_threshold' => 2]);

        foreach ([1, 2, 3] as $i) {
            $appointment = $this->appointment($doctor, $patient, $service, $clinicId, -$i * 24);
            $this->authJson('POST', "/api/v1/appointment/{$appointment->getUuid()}/no-show", $user);
            self::assertSame(200, $this->responseCode());
        }

        $summary = $this->authJson('GET', "/api/v1/patient/{$patient->getUuid()}/no-shows", $user);

        self::assertTrue($summary['data']['at_risk']);
        self::assertSame(3, $summary['data']['count']);

        // و با همین وضعیت، نوبت تازه ثبت می‌شود.
        $fresh = $this->appointment($doctor, $patient, $service, $clinicId, 48);

        self::assertSame(Appointment::STATUS_CONFIRMED, $fresh->getStatus());
    }

    public function testAnotherClinicCannotSeeTheNoShowSummary(): void
    {
        [$user, , , , $patient] = $this->clinicWithPatient();
        [$other]                = $this->clinicWithPatient();

        $this->authJson('GET', "/api/v1/patient/{$patient->getUuid()}/no-shows", $other);
        self::assertSame(404, $this->responseCode());

        $this->authJson('GET', "/api/v1/patient/{$patient->getUuid()}/no-shows", $user);
        self::assertSame(200, $this->responseCode());
    }

    public function testAnotherClinicCannotPreviewTheCancellation(): void
    {
        [$owner, $section, $address, $doctor, $patient] = $this->clinicWithPatient();
        [$other] = $this->clinicWithPatient();

        $service     = $this->service($section);
        $appointment = $this->appointment($doctor, $patient, $service, (int) $address->getClinicId(), 5);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/cancellation-preview", $other);

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

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

        $this->authJson('PUT', '/api/v1/cancellation-policy', $user, [
            'penalty_mode'  => 'percent',
            'penalty_value' => 150,
        ]);

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