<?php

namespace App\Tests\Resource;

use App\Appointment\Entity\WeeklySchedule;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Resource\Repository\ClinicResourceRepository;
use App\Resource\Service\ResourceLinker;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class BackfillResourceTest extends ResourceTestCase
{
    /**
     * همیشه با `--pair` اجرا می‌شود: `db_test` هرگز ریست نمی‌شود و هزاران محیط از
     * تست‌های دیگر دارد، پس اجرای بی‌دامنه هم کند است و هم به دادهٔ نامرتبط وابسته.
     */
    private function runBackfill(bool $force, string $pair): string
    {
        $application = new Application(static::$kernel);
        $tester      = new CommandTester($application->find('app:resource:backfill'));

        $args = ['--pair' => $pair];

        if ($force) {
            $args['--force'] = true;
        }

        $tester->execute($args);

        return $tester->getDisplay();
    }

    private function runCalendarBackfill(): string
    {
        $application = new Application(static::$kernel);
        $tester      = new CommandTester($application->find('app:resource:calendar:backfill'));
        $tester->execute(['--force' => true]);

        return $tester->getDisplay();
    }

    private function pairOf(DoctorAddress $address): string
    {
        return $address->tenantEntityType() . ':' . $address->tenantEntityId();
    }

    private function resources(): ClinicResourceRepository
    {
        return static::getContainer()->get(ClinicResourceRepository::class);
    }

    /** dry-run پیش‌فرض است: بدون `--force` هیچ ردیفی نوشته نمی‌شود. */
    public function testDryRunWritesNothing(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $room = $this->room($address, 'اتاق دراِی‌ران');

        $output = $this->runBackfill(false, $this->pairOf($address));

        self::assertStringContainsString('Dry run', $output);
        self::assertNull($this->resources()->findForSubject($room));
    }

    public function testRoomBecomesAResourceCarryingItsCapacity(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $room = $this->room($address, 'اتاق تزریق سه‌تخته', 3);

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $resource = $this->resources()->findForSubject(
            $this->em->getRepository(\App\Branch\Entity\Room::class)->find($room->getId())
        );

        self::assertNotNull($resource);
        self::assertSame(3, $resource->getCapacity(), 'اتاق سه‌تخته یک منبع با ظرفیت ۳ است، نه سه منبع');
        self::assertSame(ResourceType::CODE_ROOM, $resource->getType()->getCode());
        self::assertTrue($resource->getType()->isSystem());
    }

    public function testStaffOfASingleBranchEnvironmentIsBridged(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $staff = $this->staff($address, 'اپراتور تک‌شعبه');

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $resource = $this->resources()->findForSubject(
            $this->em->getRepository(\App\Staff\Entity\ClinicStaff::class)->find($staff->getId())
        );

        self::assertNotNull($resource);
        self::assertSame(ResourceType::CODE_STAFF, $resource->getType()->getCode());
        self::assertSame(1, $resource->getCapacity());
    }

    /**
     * پرسنل هیچ ستونی ندارد که آدرسش را بگوید. با چند شعبه، انتخاب یکی حدس است و
     * او را در ساختمان اشتباه می‌نشاند — پس رد و **گزارش** می‌شود، نه حدس بی‌صدا.
     */
    public function testStaffOfAMultiBranchEnvironmentIsReportedNotGuessed(): void
    {
        [, $clinic, $address] = $this->clinicWithAddress();
        $this->extraAddress($clinic);
        $staff = $this->staff($address, 'اپراتور چندشعبه');

        $output = $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        self::assertStringContainsString('شعبهٔ یکتا ندارد', $output);
        self::assertNull($this->resources()->findForSubject(
            $this->em->getRepository(\App\Staff\Entity\ClinicStaff::class)->find($staff->getId())
        ));
    }

    /**
     * منبعِ پزشک از `location_id` شیفت‌های برنامهٔ هفتگی مشتق می‌شود — آنجا دقیقاً
     * نوشته در کدام آدرس‌ها شیفت دارد. «اولین شعبهٔ محیط» حدس می‌بود.
     */
    public function testDoctorIsBridgedOncePerScheduledLocation(): void
    {
        [$user, $doctor, $address] = $this->doctorWithAddress();
        $second = DoctorAddress::forDoctor($doctor);
        $second->setName('مطب دوم');
        $this->em->persist($second);

        $third = DoctorAddress::forDoctor($doctor);
        $third->setName('مطب بی‌شیفت');
        $this->em->persist($third);
        $this->em->flush();

        // شیفت فعال روی دو آدرس اول، هیچ شیفتی روی سومی.
        $this->em->persist($this->newWeeklySchedule($doctor, [
            '0' => ['sessions' => [
                ['active' => true, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20],
                ['active' => true, 'location_id' => $second->getId(), 'start_time' => '16:00', 'end_time' => '20:00', 'duration_per_patient' => 20],
            ]],
        ]));
        $this->em->flush();

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $doctorEntity = $this->em->getRepository(Doctor::class)->find($doctor->getId());
        $all = $this->resources()->findAllForSubject($doctorEntity);

        self::assertCount(2, $all, 'یک منبع per آدرسی که شیفت دارد');

        $addressIds = array_map(static fn (ClinicResource $r): int => (int) $r->getAddress()->getId(), $all);
        sort($addressIds);
        $expected = [(int) $address->getId(), (int) $second->getId()];
        sort($expected);
        self::assertSame($expected, $addressIds);
    }

    /** شیفت غیرفعال منبع نمی‌سازد. */
    public function testInactiveSessionDoesNotBridgeTheDoctor(): void
    {
        [, $doctor, $address] = $this->doctorWithAddress();

        $this->em->persist($this->newWeeklySchedule($doctor, [
            '0' => ['sessions' => [
                ['active' => false, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20],
            ]],
        ]));
        $this->em->flush();

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $doctorEntity = $this->em->getRepository(Doctor::class)->find($doctor->getId());
        self::assertSame([], $this->resources()->findAllForSubject($doctorEntity));
    }

    /** idempotent: تکیه‌گاهش وجود منبع است، نه یک پرچم جداگانه. */
    public function testRunningTwiceCreatesNothingNew(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $this->room($address, 'اتاق تکراری');
        $this->staff($address, 'اپراتور تکراری');

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $secondOutput = $this->runBackfill(true, $this->pairOf($address));

        self::assertStringContainsString('اتاق: 0', $secondOutput);
        self::assertStringContainsString('پرسنل: 0', $secondOutput);
    }

    /**
     * توگلِ پرسنل از راه endpoint واقعی باید منبعش را هم ببندد — نه فقط سرویس.
     * بدون این سیم‌کشی، پرسنل غیرفعال همچنان در جستجوی وقتِ تسک ۰۶ ظاهر می‌شد.
     */
    public function testTogglingStaffThroughTheEndpointClosesItsResource(): void
    {
        [$owner, , $address] = $this->clinicWithAddress();
        $staff = $this->staff($address, 'اپراتور توگل');

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $this->authJson('PATCH', "/api/v1/staff/{$staff->getUuid()}/toggle", $owner);
        self::assertSame(200, $this->responseCode());

        $this->em->clear();
        $reloaded = $this->em->getRepository(\App\Staff\Entity\ClinicStaff::class)->find($staff->getId());

        self::assertFalse($reloaded->isActive());
        self::assertFalse($this->resources()->findForSubject($reloaded)->isActive());
    }

    /**
     * شیفت منابعِ پزشک از برنامهٔ هفتگی ساخته می‌شود، وگرنه منبعِ تازه‌ساخته هیچ
     * ساعتی ندارد و لایهٔ منبع تا ورود دستی بی‌اثر می‌ماند.
     */
    public function testCalendarBackfillDerivesShiftsFromTheWeeklySchedule(): void
    {
        [, $doctor, $address] = $this->doctorWithAddress();

        $this->em->persist($this->newWeeklySchedule($doctor, [
            '0' => ['sessions' => [
                ['active' => true, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20],
                ['active' => true, 'location_id' => $address->getId(), 'start_time' => '16:00', 'end_time' => '20:00', 'duration_per_patient' => 20],
            ]],
            '1' => ['sessions' => [
                ['active' => false, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20],
            ]],
        ]));
        $this->em->flush();

        $this->runBackfill(true, $this->pairOf($address));
        $this->runCalendarBackfill();
        $this->em->clear();

        $doctorEntity = $this->em->getRepository(Doctor::class)->find($doctor->getId());
        $resource     = $this->resources()->findForSubject($doctorEntity);

        $calendars = static::getContainer()
            ->get(\App\Resource\Repository\ResourceCalendarRepository::class)
            ->findForResource($resource);

        self::assertCount(2, $calendars, 'دو شیفت فعالِ شنبه؛ شیفت غیرفعال یکشنبه نمی‌آید');
        self::assertSame([0, 0], array_map(static fn ($c) => $c->getDayOfWeek(), $calendars));
        self::assertSame([540, 960], array_map(static fn ($c) => $c->getStartMinute(), $calendars));
        self::assertSame([0, 1], array_map(static fn ($c) => $c->getSequence(), $calendars));
    }

    /** منبعی که کاربر خودش تنظیم کرده دست نمی‌خورد — همین idempotent بودن را می‌دهد. */
    public function testCalendarBackfillLeavesAnAlreadyConfiguredResourceAlone(): void
    {
        [$user, $doctor, $address] = $this->doctorWithAddress();

        $this->em->persist($this->newWeeklySchedule($doctor, [
            '0' => ['sessions' => [
                ['active' => true, 'location_id' => $address->getId(), 'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20],
            ]],
        ]));
        $this->em->flush();

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $doctorEntity = $this->em->getRepository(Doctor::class)->find($doctor->getId());
        $resource     = $this->resources()->findForSubject($doctorEntity);

        $this->authJson('PUT', "/api/v1/resource/{$resource->getUuid()}/calendar", $user, [
            'days' => [3 => [['start_minute' => 600, 'end_minute' => 660]]],
        ]);
        self::assertSame(200, $this->responseCode());

        $output = $this->runCalendarBackfill();
        self::assertStringContainsString('دست‌نخورده ماند', $output);

        $this->em->clear();
        $calendars = static::getContainer()
            ->get(\App\Resource\Repository\ResourceCalendarRepository::class)
            ->findForResource($this->resources()->findByUuid($resource->getUuid()));

        self::assertCount(1, $calendars);
        self::assertSame(3, $calendars[0]->getDayOfWeek(), 'تنظیم دستی کاربر سرِ جایش است');
    }

    /** حداکثر یک پل: قید در سازنده است، چون MariaDB `CHECK` چندستونی را اجرا نمی‌کند. */
    public function testASecondBridgeIsRefused(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $room  = $this->room($address, 'اتاق دوپل');
        $staff = $this->staff($address, 'پرسنل دوپل');

        $type     = $this->resourceType($address, 'mixed', 'ترکیبی');
        $resource = new \App\Resource\Entity\ClinicResource($address, $type, 'منبع دوپل');
        $resource->linkTo($room);

        $this->expectException(\InvalidArgumentException::class);
        $resource->linkTo($staff);
    }

    /**
     * غیرفعال شدن پرسنل باید منبعش را هم ببندد، وگرنه در جستجوی وقتِ تسک ۰۶ ظاهر
     * می‌شود. عکسش برقرار نیست.
     */
    public function testSyncActiveClosesEveryResourceOfASubject(): void
    {
        [, , $address] = $this->clinicWithAddress();
        $staff = $this->staff($address, 'اپراتور خاموش‌شونده');

        $this->runBackfill(true, $this->pairOf($address));
        $this->em->clear();

        $linker      = static::getContainer()->get(ResourceLinker::class);
        $staffEntity = $this->em->getRepository(\App\Staff\Entity\ClinicStaff::class)->find($staff->getId());

        self::assertSame(1, $linker->syncActive($staffEntity, false));
        self::assertFalse($this->resources()->findForSubject($staffEntity)->isActive());

        // دوباره صدا زدن چیزی را عوض نمی‌کند.
        self::assertSame(0, $linker->syncActive($staffEntity, false));
    }
}
