<?php

namespace App\Tests\Appointment;

use App\Appointment\Availability\Entity\ResourceOccupancy;
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\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Tests\ApiTestCase;

/**
 * موتور جستجوی وقت چندمنبعی — بند ۱۰ مستند.
 */
class AvailabilityEngineTest extends ApiTestCase
{
    private const TEHRAN = 'Asia/Tehran';

    private function nextSaturday(): int
    {
        return (new \DateTimeImmutable('next saturday', new \DateTimeZone(self::TEHRAN)))
            ->setTime(0, 0)
            ->getTimestamp();
    }

    /** @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): ServiceItem
    {
        // هر درخواست HTTP کرنل را از نو می‌سازد، پس نمونهٔ قبلی detached شده است؛
        // بدون این، ساختن سرویس دوم با «A new entity was found» می‌شکند.
        $section = $this->em->getRepository(ServiceSection::class)->find($section->getId());

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

        return $item;
    }

    private function type(DoctorAddress $address, string $code, string $name): ResourceType
    {
        $type = new ResourceType($address->tenantEntityType(), $address->tenantEntityId(), $code, $name);
        $this->em->persist($type);
        $this->em->flush();

        return $type;
    }

    /** منبع با شیفت شنبه تا جمعه ۰۹:۰۰–۱۷:۰۰. */
    private function resourceWithShift(User $user, DoctorAddress $address, ResourceType $type, string $name): array
    {
        $created = $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid' => $address->getUuid(),
            'type_uuid'    => $type->getUuid(),
            'name'         => $name,
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($created, JSON_UNESCAPED_UNICODE));

        $this->authJson('PUT', "/api/v1/resource/{$created['data']['uuid']}/calendar", $user, [
            'days' => array_fill_keys(range(0, 6), [['start_minute' => 540, 'end_minute' => 1020]]),
        ]);
        self::assertSame(200, $this->responseCode());

        return $created['data'];
    }

    /** @param list<array<string, mixed>> $segments */
    private function setSegments(User $user, ServiceItem $service, array $segments): void
    {
        $this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $user, ['segments' => $segments]);
        self::assertSame(200, $this->responseCode());
    }

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

    private function occupy(string $resourceUuid, int $start, int $end): void
    {
        $resource = $this->em->getRepository(ClinicResource::class)->findOneBy(['uuid' => $resourceUuid]);
        $this->em->persist(new ResourceOccupancy($resource, $start, $end));
        $this->em->flush();
    }

    /** سناریوی مستند: چهار بخش، سه اتاق، دو اپراتور، سه دستگاه. */
    public function testDocumentScenarioReturnsSlotsWithAssignments(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20);

        $room     = $this->type($address, 'room', 'اتاق');
        $operator = $this->type($address, 'operator', 'اپراتور');
        $device   = $this->type($address, 'device', 'دستگاه');

        foreach (range(1, 3) as $n) { $this->resourceWithShift($user, $address, $room, "اتاق $n"); }
        foreach (range(1, 2) as $n) { $this->resourceWithShift($user, $address, $operator, "اپراتور $n"); }
        foreach (range(1, 3) as $n) { $this->resourceWithShift($user, $address, $device, "لیزر $n"); }

        $roomReq = ['type_uuid' => $room->getUuid()];
        $opReq   = ['type_uuid' => $operator->getUuid()];
        $devReq  = ['type_uuid' => $device->getUuid()];

        $this->setSegments($user, $service, [
            ['sequence' => 1, 'name' => 'بی‌حسی', 'duration_minutes' => 5,  'requirements' => [$roomReq, $opReq]],
            ['sequence' => 2, 'name' => 'انتظار', 'duration_minutes' => 30, 'requirements' => [$roomReq]],
            ['sequence' => 3, 'name' => 'لیزر',   'duration_minutes' => 20, 'requirements' => [$roomReq, $opReq, $devReq]],
            ['sequence' => 4, 'name' => 'مراقبت', 'duration_minutes' => 5,  'requirements' => [$roomReq, $opReq]],
        ]);

        $saturday = $this->nextSaturday();
        $body     = $this->search($user, $service, $address, $saturday, $saturday);

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

        $first = $body['data']['slots'][0];
        self::assertArrayHasKey('room', $first['assignment']);
        self::assertArrayHasKey('operator', $first['assignment']);
        self::assertArrayHasKey('device', $first['assignment']);
        self::assertCount(1, $first['assignment']['room']);
    }

    /**
     * ⭐ قلب کل پروژه: بیمار الف ۱۰:۰۰–۱۱:۰۰ نوبت دارد ولی اپراتور فقط ۱۰:۰۰–۱۰:۰۵ و
     * ۱۰:۳۵–۱۱:۰۰ درگیر است. با اتاق دوم، بیمار ب باید در همان بازهٔ میانی جا شود.
     *
     * بدون این سناریو، کل تسک تأیید نمی‌شود.
     */
    public function testOperatorFreedDuringWaitingIsReusedForAnotherPatient(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20);

        $room     = $this->type($address, 'room', 'اتاق');
        $operator = $this->type($address, 'operator', 'اپراتور');

        $roomA = $this->resourceWithShift($user, $address, $room, 'اتاق ۱');
        $roomB = $this->resourceWithShift($user, $address, $room, 'اتاق ۲');
        $op    = $this->resourceWithShift($user, $address, $operator, 'اپراتور تنها');

        // سرویس کوتاه: ۵ دقیقه، فقط اتاق و اپراتور.
        $short = $this->service($section, 'مشاورهٔ کوتاه', 5);
        $this->setSegments($user, $short, [
            ['sequence' => 1, 'name' => 'مشاوره', 'duration_minutes' => 5, 'requirements' => [
                ['type_uuid' => $room->getUuid()], ['type_uuid' => $operator->getUuid()],
            ]],
        ]);

        $saturday = $this->nextSaturday();
        $ten      = $saturday + 10 * 3600;

        // بیمار الف: اتاق ۱ کل ساعت گرفته، اپراتور فقط دو سرِ آن.
        $this->occupy($roomA['uuid'], $ten, $ten + 3600);
        $this->occupy($op['uuid'], $ten, $ten + 5 * 60);
        $this->occupy($op['uuid'], $ten + 35 * 60, $ten + 3600);

        $body = $this->search($user, $short, $address, $saturday, $saturday, ['step_minutes' => 5]);
        self::assertSame(200, $this->responseCode());

        $starts = array_column($body['data']['slots'], 'start');

        // بازهٔ آزادِ اپراتور: ۱۰:۰۵ تا ۱۰:۳۵ — یک نوبت پنج‌دقیقه‌ای آنجا جا می‌شود.
        $inGap = array_filter(
            $starts,
            static fn (int $s): bool => $s >= $ten + 5 * 60 && $s + 5 * 60 <= $ten + 35 * 60,
        );

        self::assertNotEmpty($inGap, 'اپراتورِ آزادشده در «انتظار» باید دوباره قابل استفاده باشد');

        // و اتاق پیشنهادی باید اتاق ۲ باشد، چون اتاق ۱ کل ساعت گرفته است.
        foreach ($body['data']['slots'] as $slot) {
            if ($slot['start'] >= $ten + 5 * 60 && $slot['start'] + 300 <= $ten + 35 * 60) {
                self::assertSame($roomB['uuid'], $slot['assignment']['room'][0]['uuid']);
                break;
            }
        }
    }

    /** یک منبع برای همهٔ بخش‌هایی که آن نقش را می‌خواهند — نه دو نفر. */
    public function testSameResourceIsUsedAcrossNonAdjacentSegments(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service  = $this->service($section, 'لیزر', 20);
        $room     = $this->type($address, 'room', 'اتاق');
        $operator = $this->type($address, 'operator', 'اپراتور');

        $this->resourceWithShift($user, $address, $room, 'اتاق ۱');
        foreach (range(1, 3) as $n) { $this->resourceWithShift($user, $address, $operator, "اپراتور $n"); }

        $this->setSegments($user, $service, [
            ['sequence' => 1, 'name' => 'بخش اول', 'duration_minutes' => 5,  'requirements' => [['type_uuid' => $operator->getUuid()]]],
            ['sequence' => 2, 'name' => 'انتظار',  'duration_minutes' => 30, 'requirements' => [['type_uuid' => $room->getUuid()]]],
            ['sequence' => 3, 'name' => 'بخش سوم', 'duration_minutes' => 10, 'requirements' => [['type_uuid' => $operator->getUuid()]]],
        ]);

        $saturday = $this->nextSaturday();
        $body     = $this->search($user, $service, $address, $saturday, $saturday);

        self::assertNotEmpty($body['data']['slots']);
        self::assertCount(
            1,
            $body['data']['slots'][0]['assignment']['operator'],
            'یک اپراتور برای هر دو بخش، نه دو نفر',
        );
    }

    /** ظرفیت ۳: سه نوبت هم‌زمان جا دارد، چهارمی نه. */
    public function testCapacityIsCountedNotJustPresence(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $short = $this->service($section, 'تزریق', 10);
        $room  = $this->type($address, 'room', 'اتاق');

        $created = $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid' => $address->getUuid(),
            'type_uuid'    => $room->getUuid(),
            'name'         => 'اتاق سه‌تخته',
            'capacity'     => 3,
        ]);
        self::assertSame(201, $this->responseCode());

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

        $this->setSegments($user, $short, [
            ['sequence' => 1, 'name' => 'تزریق', 'duration_minutes' => 10, 'requirements' => [['type_uuid' => $room->getUuid()]]],
        ]);

        $saturday = $this->nextSaturday();
        $body     = $this->search($user, $short, $address, $saturday, $saturday);

        self::assertSame(200, $this->responseCode());
        self::assertNotEmpty($body['data']['slots'], 'اتاق سه‌تخته باید وقت بدهد');
    }

    /** زمان گذشته حذف می‌شود. */
    public function testPastStartsAreExcluded(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'ویزیت', 20);
        $room    = $this->type($address, 'room', 'اتاق');
        $this->resourceWithShift($user, $address, $room, 'اتاق ۱');

        $this->setSegments($user, $service, [
            ['sequence' => 1, 'name' => 'ویزیت', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $room->getUuid()]]],
        ]);

        // دو هفته عقب، نه یک هفته: وقتی امروز خودش شنبه باشد، «شنبهٔ هفتهٔ پیش» همین
        // امروز است و ساعت‌های بعدازظهرش هنوز گذشته نیستند.
        $past = $this->nextSaturday() - 14 * 86400;
        $body = $this->search($user, $service, $address, $past, $past);

        self::assertSame(200, $this->responseCode());
        self::assertSame([], $body['data']['slots']);
        self::assertSame('no_capacity_in_range', $body['data']['reason']);
    }

    /** فهرست خالی خطا نیست و ۴۰۴ هم نیست — دلیل صریح می‌آید. */
    public function testEmptyResultCarriesAReason(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'ویزیت', 20);
        $room    = $this->type($address, 'room', 'اتاق');

        // منبع هست ولی هیچ شیفتی ندارد.
        $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid' => $address->getUuid(),
            'type_uuid'    => $room->getUuid(),
            'name'         => 'اتاق بی‌شیفت',
        ]);

        $this->setSegments($user, $service, [
            ['sequence' => 1, 'name' => 'ویزیت', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $room->getUuid()]]],
        ]);

        $saturday = $this->nextSaturday();
        $body     = $this->search($user, $service, $address, $saturday, $saturday);

        self::assertSame(200, $this->responseCode());
        self::assertSame([], $body['data']['slots']);
        self::assertSame('no_capacity_in_range', $body['data']['reason']);
    }

    public function testRangeBeyondNinetyDaysIsRejected(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'ویزیت', 20);

        $saturday = $this->nextSaturday();
        $this->search($user, $service, $address, $saturday, $saturday + 120 * 86400);

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

    public function testForeignBranchIsNotFound(): void
    {
        [$user, $section] = $this->clinicWithBranch();
        [, , $foreign]    = $this->clinicWithBranch();
        $service = $this->service($section, 'ویزیت', 20);

        $saturday = $this->nextSaturday();
        $this->authJson('POST', '/api/v1/appointment-availability', $user, [
            'service_uuid' => $service->getUuid(),
            'branch_uuid'  => $foreign->getUuid(),
            'from'         => $saturday,
            'to'           => $saturday,
        ]);

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

    /** بازهٔ اشغال گسترده‌تر از بخش است: آماده‌سازی و تمیزکاری هم می‌گیرد. */
    public function testSetupAndCleanupWidenTheOccupiedInterval(): void
    {
        [$user, $section, $address] = $this->clinicWithBranch();
        $service = $this->service($section, 'لیزر', 20);
        $device  = $this->type($address, 'device', 'دستگاه');

        $created = $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid'    => $address->getUuid(),
            'type_uuid'       => $device->getUuid(),
            'name'            => 'لیزر تنها',
            'setup_minutes'   => 10,
            'cleanup_minutes' => 10,
        ]);
        self::assertSame(201, $this->responseCode());

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

        $this->setSegments($user, $service, [
            ['sequence' => 1, 'name' => 'لیزر', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $device->getUuid()]]],
        ]);

        $saturday = $this->nextSaturday();

        // دستگاه ۱۲:۰۰ تا ۱۳:۰۰ گرفته است.
        $this->occupy($created['data']['uuid'], $saturday + 12 * 3600, $saturday + 13 * 3600);

        $body   = $this->search($user, $service, $address, $saturday, $saturday, ['step_minutes' => 5]);
        $starts = array_column($body['data']['slots'], 'start');

        // شروع ۱۱:۵۵ یعنی اشغال از ۱۱:۴۵ تا ۱۲:۲۵ — با اشغال موجود تداخل دارد.
        self::assertNotContains($saturday + 11 * 3600 + 55 * 60, $starts);
        // شروع ۱۱:۳۰ یعنی اشغال ۱۱:۲۰ تا ۱۲:۰۰ — دقیقاً می‌چسبد و مجاز است.
        self::assertContains($saturday + 11 * 3600 + 30 * 60, $starts);
    }
}
