<?php

namespace App\Tests\Pricing;

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;

/**
 * لیست قیمت بازه‌دار و فاکتور تفکیک‌شده — بند ۱۲ و قانون پنجم مستند.
 */
class PricingTest extends ApiTestCase
{
    /** @return array{user: User, section: ServiceSection, address: DoctorAddress} */
    private function clinic(): 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' => $user, 'section' => $section, 'address' => $address];
    }

    private function service(ServiceSection $section, string $name, int $price): ServiceItem
    {
        $section = $this->em->getRepository(ServiceSection::class)->find($section->getId());

        $item = new ServiceItem($section, $name);
        $item->setPriceRials($price);
        $item->setSoloDurationMinutes(20);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    /** @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(),
        ]);
    }

    private function priceList(User $user, string $name, int $from, int $to, ?string $addressUuid = null): array
    {
        $body = $this->authJson('POST', '/api/v1/price-lists', $user, array_filter([
            'name'         => $name,
            'starts_at'    => $from,
            'ends_at'      => $to,
            'address_uuid' => $addressUuid,
        ]));
        self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        return $body['data'];
    }

    /** بدون هیچ لیست قیمتی، قیمت خودِ سرویس برمی‌گردد — هرگز صفر یا خطا. */
    public function testFallsBackToTheServicePrice(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'بوتاکس', 5_000_000);

        $body = $this->quote($c['user'], $service, $c['address']);

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
        self::assertSame(5_000_000, $body['data']['base_rials']);
        self::assertSame(5_000_000, $body['data']['final_rials']);
        self::assertSame('service_item', $body['data']['breakdown']['sources'][$service->getUuid()]);
    }

    /** لیست قیمت فقط در بازهٔ خودش حاکم است. */
    public function testPriceListAppliesOnlyInsideItsRange(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'بوتاکس', 5_000_000);

        $from = strtotime('+10 days');
        $to   = strtotime('+40 days');

        $list = $this->priceList($c['user'], 'نیمهٔ دوم', $from, $to);

        $this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 8_000_000]],
        ]);
        self::assertSame(200, $this->responseCode());

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

        $inside = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);
        self::assertSame(8_000_000, $inside['data']['base_rials'], 'داخل بازه: قیمت جدید');

        $before = $this->quote($c['user'], $service, $c['address'], ['at' => $from - 86400]);
        self::assertSame(5_000_000, $before['data']['base_rials'], 'پیش از بازه: قیمت قبلی');
    }

    /** دو لیست فعالِ هم‌پوشان یعنی یک تاریخ دو قیمت — هنگام فعال‌سازی رد می‌شود. */
    public function testOverlappingActiveListsAreRejected(): void
    {
        $c    = $this->clinic();
        $from = strtotime('+10 days');

        $first = $this->priceList($c['user'], 'اول', $from, $from + 30 * 86400);
        $this->authJson('POST', "/api/v1/price-list/{$first['uuid']}/activate", $c['user']);
        self::assertSame(200, $this->responseCode());

        $second = $this->priceList($c['user'], 'دوم', $from + 10 * 86400, $from + 50 * 86400);
        $body   = $this->authJson('POST', "/api/v1/price-list/{$second['uuid']}/activate", $c['user']);

        self::assertSame(422, $this->responseCode());
        self::assertStringContainsString('هم‌پوشانی', $body['errors'][0]['message']);
    }

    /** لیستِ یک شعبه با لیست عمومی تداخل ندارد و بر آن مقدم است. */
    public function testBranchListWinsOverTheGeneralList(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'بوتاکس', 5_000_000);

        $from = strtotime('+10 days');
        $to   = $from + 30 * 86400;

        $general = $this->priceList($c['user'], 'عمومی', $from, $to);
        $this->authJson('PUT', "/api/v1/price-list/{$general['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 7_000_000]],
        ]);
        $this->authJson('POST', "/api/v1/price-list/{$general['uuid']}/activate", $c['user']);
        self::assertSame(200, $this->responseCode());

        $branch = $this->priceList($c['user'], 'شعبهٔ مرکزی', $from, $to, $c['address']->getUuid());
        $this->authJson('PUT', "/api/v1/price-list/{$branch['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 9_000_000]],
        ]);
        $this->authJson('POST', "/api/v1/price-list/{$branch['uuid']}/activate", $c['user']);
        self::assertSame(200, $this->responseCode(), 'لیست شعبه با لیست عمومی تداخل ندارد');

        $body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);

        self::assertSame(9_000_000, $body['data']['base_rials']);
    }

    /** override شعبه (تسک ۰۴) بر لیست قیمت مقدم است. */
    public function testBranchOverrideBeatsThePriceList(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'بوتاکس', 5_000_000);

        $from = strtotime('+10 days');
        $list = $this->priceList($c['user'], 'عمومی', $from, $from + 30 * 86400);
        $this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 7_000_000]],
        ]);
        $this->authJson('POST', "/api/v1/price-list/{$list['uuid']}/activate", $c['user']);

        $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/branch-overrides", $c['user'], [
            'overrides' => [['address_uuid' => $c['address']->getUuid(), 'price_rials' => 11_000_000]],
        ]);
        self::assertSame(200, $this->responseCode());

        $body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);

        self::assertSame(11_000_000, $body['data']['base_rials']);
        self::assertSame('branch_override', $body['data']['breakdown']['sources'][$service->getUuid()]);
    }

    public function testFullChainAppliesInOrder(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'لیزر', 10_000_000);
        $extra   = $this->service($c['section'], 'ناحیهٔ اضافه', 2_000_000);

        $body = $this->quote($c['user'], $service, $c['address'], [
            'item_uuids' => [$extra->getUuid()],
            'policy'     => [
                'discount_percent'                => 10,
                'insurance_base_percent'          => 20,
                'insurance_supplementary_percent' => 50,
                'tax_percent'                     => 10,
                'deposit_percent'                 => 30,
            ],
        ]);

        $d = $body['data'];

        self::assertSame(10_000_000, $d['base_rials']);
        self::assertSame(2_000_000, $d['items_rials']);
        self::assertSame(1_200_000, $d['discount_rials'], '۱۰٪ از ۱۲ میلیون');
        self::assertSame(2_160_000, $d['insurance_base_rials'], '۲۰٪ از ۱۰٫۸ میلیون');
        self::assertSame(4_320_000, $d['insurance_supplementary_rials'], '۵۰٪ از باقیمانده');
        self::assertSame(432_000, $d['tax_rials'], '۱۰٪ روی سهم بیمار، نه روی کل');
        self::assertSame(4_752_000, $d['final_rials']);
        self::assertSame(1_425_600, $d['deposit_rials']);
    }

    /** تخفیف بیشتر از مبلغ، مبلغ را صفر می‌کند نه منفی. */
    public function testDiscountLargerThanTheAmountFloorsAtZero(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'ویزیت', 1_000_000);

        $body = $this->quote($c['user'], $service, $c['address'], [
            'policy' => ['discount_rials' => 5_000_000],
        ]);

        self::assertSame(0, $body['data']['final_rials']);
        self::assertGreaterThanOrEqual(0, $body['data']['discount_rials']);
    }

    /** سقف جمع تخفیف‌ها per محیط اعمال می‌شود. */
    public function testTotalDiscountCapIsApplied(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'ویزیت', 10_000_000);

        $body = $this->quote($c['user'], $service, $c['address'], [
            'policy' => [
                'discount_percent'           => 40,
                'discount_rials'             => 3_000_000,
                'max_total_discount_percent' => 25,
            ],
        ]);

        self::assertSame(2_500_000, $body['data']['discount_rials'], 'سقف ۲۵٪ از ۱۰ میلیون');
        self::assertSame(7_500_000, $body['data']['final_rials']);
    }

    /**
     * ⭐ جمعِ ردیف‌ها **باید** برابر مبلغ نهایی باشد.
     *
     * تست زنجیره عددها را تک‌تک می‌سنجد؛ این یکی خودِ رابطه را می‌سنجد. اگر روزی ردیف
     * تازه‌ای اضافه شود و در جمع نهایی حساب نشود، آن تست سبز می‌ماند و این قرمز.
     */
    public function testTheRowsAlwaysAddUpToTheFinalAmount(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'لیزر', 9_500_000);
        $extra   = $this->service($c['section'], 'ناحیهٔ اضافه', 3_300_000);

        $d = $this->quote($c['user'], $service, $c['address'], [
            'item_uuids' => [$extra->getUuid()],
            'policy'     => [
                'discount_percent'                => 7,
                'insurance_base_percent'          => 15,
                'insurance_supplementary_percent' => 25,
                'tax_percent'                     => 9,
            ],
        ])['data'];

        $sum = $d['base_rials']
            + $d['items_rials']
            - $d['discount_rials']
            - $d['insurance_base_rials']
            - $d['insurance_supplementary_rials']
            + $d['tax_rials'];

        self::assertSame($d['final_rials'], $sum, json_encode($d, JSON_UNESCAPED_UNICODE));
    }

    /** بیعانهٔ مبلغی بر درصدی مقدم است و هرگز از مبلغ نهایی بیشتر نمی‌شود. */
    public function testAFixedDepositOverridesThePercentageAndIsCapped(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'ویزیت', 2_000_000);

        $fixed = $this->quote($c['user'], $service, $c['address'], [
            'policy' => ['deposit_percent' => 50, 'deposit_rials' => 300_000],
        ])['data'];

        self::assertSame(300_000, $fixed['deposit_rials'], 'مبلغی برنده است، نه ۵۰٪');

        $capped = $this->quote($c['user'], $service, $c['address'], [
            'policy' => ['deposit_rials' => 9_000_000],
        ])['data'];

        self::assertSame(
            $capped['final_rials'],
            $capped['deposit_rials'],
            'بیعانهٔ بیشتر از مبلغ نهایی یعنی بدهکار کردن بیمار پیش از ویزیت',
        );
    }

    public function testForeignServiceIsNotFound(): void
    {
        $c     = $this->clinic();
        $other = $this->clinic();
        $foreign = $this->service($other['section'], 'سرویس بیگانه', 1_000_000);

        $this->quote($c['user'], $foreign, $c['address']);

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

    public function testNegativePriceIsRejected(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'ویزیت', 1_000_000);
        $list    = $this->priceList($c['user'], 'تست', strtotime('+1 day'), strtotime('+30 days'));

        $body = $this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => -100]],
        ]);

        self::assertSame(422, $this->responseCode());
        self::assertSame('price_rials', $body['errors'][0]['field']);
    }

    /**
     * ⭐ قانون پنجم مستند: «تغییر قیمت هرگز نوبت‌های ثبت‌شده را عوض نمی‌کند.»
     *
     * نوبت ثبت می‌شود، بعد قیمت سرویس دو برابر می‌شود، و فاکتور همان اعداد قبلی را
     * می‌دهد. بدون این تست، تسک تأییدشده نیست.
     */
    public function testBookedAppointmentKeepsItsOriginalInvoiceAfterAPriceChange(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'لیزر', 4_000_000);

        $room = new \App\Resource\Entity\ResourceType(
            $c['address']->tenantEntityType(),
            $c['address']->tenantEntityId(),
            'room',
            'اتاق',
        );
        $this->em->persist($room);
        $this->em->flush();

        $resource = $this->authJson('POST', '/api/v1/resource', $c['user'], [
            'address_uuid' => $c['address']->getUuid(),
            'type_uuid'    => $room->getUuid(),
            'name'         => 'اتاق ۱',
        ]);
        self::assertSame(201, $this->responseCode());

        $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/calendar", $c['user'], [
            'days' => array_fill_keys(range(0, 6), [['start_minute' => 0, 'end_minute' => 1440]]),
        ]);

        $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $c['user'], [
            'segments' => [[
                'sequence' => 1, 'name' => 'لیزر', 'duration_minutes' => 20,
                'requirements' => [['type_uuid' => $room->getUuid()]],
            ]],
        ]);
        self::assertSame(200, $this->responseCode());

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

        $start = (new \DateTimeImmutable('next saturday', new \DateTimeZone('Asia/Tehran')))
            ->setTime(9, 0)
            ->getTimestamp();

        $hold = $this->authJson('POST', '/api/v1/appointment-hold', $c['user'], [
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $c['address']->getUuid(),
            'start'        => $start,
            'assignment'   => ['room' => [$resource['data']['uuid']]],
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($hold, JSON_UNESCAPED_UNICODE));

        $confirmed = $this->authJson('POST', '/api/v1/appointment-confirm', $c['user'], [
            'hold_uuid'    => $hold['data']['hold_uuid'],
            'doctor_uuid'  => $doctor->getUuid(),
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $c['address']->getUuid(),
        ]);
        self::assertSame(200, $this->responseCode(), json_encode($confirmed, JSON_UNESCAPED_UNICODE));
        self::assertSame(4_000_000, $confirmed['data']['price_snapshot']['final_rials']);

        $appointmentUuid = $confirmed['data']['appointment_uuid'];

        // حالا قیمت دو برابر می‌شود.
        $this->em->clear();
        $reloaded = $this->em->getRepository(ServiceItem::class)->findOneBy(['uuid' => $service->getUuid()]);
        $reloaded->setPriceRials(8_000_000);
        $this->em->flush();

        // قیمت جدید در quote دیده می‌شود…
        $fresh = $this->quote($c['user'], $reloaded, $c['address']);
        self::assertSame(8_000_000, $fresh['data']['final_rials']);

        // …ولی فاکتور نوبتِ ثبت‌شده دست‌نخورده است.
        $snapshot = $this->authJson('GET', "/api/v1/appointment/$appointmentUuid/price-snapshot", $c['user']);

        self::assertSame(200, $this->responseCode(), json_encode($snapshot, JSON_UNESCAPED_UNICODE));
        self::assertSame(4_000_000, $snapshot['data']['final_rials'], 'قانون پنجم: فاکتور ثبت‌شده عوض نمی‌شود');
        self::assertSame(4_000_000, $snapshot['data']['base_rials']);
    }

    /**
     * ⭐ نوبت بدون سرویس هم فاکتور می‌گیرد.
     *
     * حالت اسلاتی سرویس ندارد؛ اگر فاکتورش ساخته نمی‌شد، گزارش مالی یک ردیف کم داشت و
     * هیچ‌جا هم معلوم نمی‌شد کدام ردیف.
     */
    public function testAnAppointmentWithoutAServiceStillGetsAnInvoice(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'ویزیت', 1_500_000);

        $room = new \App\Resource\Entity\ResourceType(
            $c['address']->tenantEntityType(),
            $c['address']->tenantEntityId(),
            'room',
            'اتاق',
        );
        $this->em->persist($room);
        $this->em->flush();

        $resource = $this->authJson('POST', '/api/v1/resource', $c['user'], [
            'address_uuid' => $c['address']->getUuid(),
            'type_uuid'    => $room->getUuid(),
            'name'         => 'اتاق ویزیت',
        ]);
        self::assertSame(201, $this->responseCode());

        $this->authJson('PUT', "/api/v1/resource/{$resource['data']['uuid']}/calendar", $c['user'], [
            'days' => array_fill_keys(range(0, 6), [['start_minute' => 0, 'end_minute' => 1440]]),
        ]);

        $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $c['user'], [
            'segments' => [[
                'sequence' => 1, 'name' => 'ویزیت', 'duration_minutes' => 15,
                'requirements' => [['type_uuid' => $room->getUuid()]],
            ]],
        ]);
        self::assertSame(200, $this->responseCode());

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

        $start = (new \DateTimeImmutable('next saturday', new \DateTimeZone('Asia/Tehran')))
            ->setTime(11, 0)
            ->getTimestamp();

        $hold = $this->authJson('POST', '/api/v1/appointment-hold', $c['user'], [
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $c['address']->getUuid(),
            'start'        => $start,
            'assignment'   => ['room' => [$resource['data']['uuid']]],
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($hold, JSON_UNESCAPED_UNICODE));

        // ثبت **بدون** `service_uuid`: مسیر فاکتور تخت.
        $confirmed = $this->authJson('POST', '/api/v1/appointment-confirm', $c['user'], [
            'hold_uuid'   => $hold['data']['hold_uuid'],
            'doctor_uuid' => $doctor->getUuid(),
        ]);
        self::assertSame(200, $this->responseCode(), json_encode($confirmed, JSON_UNESCAPED_UNICODE));

        $snapshot = $this->authJson(
            'GET',
            "/api/v1/appointment/{$confirmed['data']['appointment_uuid']}/price-snapshot",
            $c['user'],
        );

        self::assertSame(200, $this->responseCode(), 'فاکتور باید وجود داشته باشد');
        self::assertSame(0, $snapshot['data']['items_rials']);
        self::assertSame($snapshot['data']['base_rials'], $snapshot['data']['final_rials']);
    }

    public function testDraftListHasNoEffectUntilActivated(): void
    {
        $c       = $this->clinic();
        $service = $this->service($c['section'], 'بوتاکس', 5_000_000);

        $from = strtotime('+2 days');
        $list = $this->priceList($c['user'], 'پیش‌نویس', $from, $from + 30 * 86400);
        $this->authJson('PUT', "/api/v1/price-list/{$list['uuid']}/items", $c['user'], [
            'items' => [['service_uuid' => $service->getUuid(), 'price_rials' => 9_999_999]],
        ]);

        $body = $this->quote($c['user'], $service, $c['address'], ['at' => $from + 86400]);

        self::assertSame(5_000_000, $body['data']['base_rials'], 'پیش‌نویس نباید قیمت را عوض کند');
    }
}
