<?php

namespace App\Tests\Resource;

use App\Auth\Entity\User;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ResourceException;
use App\Resource\Service\HolidayService;

/**
 * کسرِ لایه‌ها: ساعت شعبه ∩ شیفت منبع − تعطیلات − استثناها.
 *
 * زمان‌ها نسبت به «شنبهٔ آیندهٔ» محاسبه‌شده ساخته می‌شوند نه یک تاریخ ثابت: تاریخ
 * ثابت با گذشت زمان معنایش عوض می‌شود و تست را به مرور دروغگو می‌کند.
 */
class ResourceAvailabilityTest extends ResourceTestCase
{
    private const TEHRAN = 'Asia/Tehran';

    /** نیمه‌شبِ شنبهٔ بعدی به وقت تهران. */
    private function nextSaturday(): int
    {
        return (new \DateTimeImmutable('next saturday', new \DateTimeZone(self::TEHRAN)))
            ->setTime(0, 0)
            ->getTimestamp();
    }

    /**
     * شنبه‌ای دور، مخصوصِ تست‌های تعطیلات رسمی.
     *
     * `national_holidays` عمداً سراسری است و `db_test` هرگز ریست نمی‌شود، پس ردیفی که
     * اینجا ساخته شود روی **همهٔ** تست‌های دیگری که همان روز را می‌سنجند اثر می‌گذارد.
     * فاصله گرفتن از پنجرهٔ یک‌هفته‌ایِ بقیه + پاک کردن در tearDown، هر دو لازم‌اند.
     */
    private function farSaturday(): int
    {
        return $this->dayAfter($this->nextSaturday(), 210);
    }

    protected function tearDown(): void
    {
        // ردیف سراسری را همان تستی که ساخته پاک می‌کند؛ وگرنه بدهی‌اش را تست بعدی می‌دهد.
        $this->em->createQuery('DELETE FROM App\Resource\Entity\NationalHoliday h')->execute();

        // بدون clear()، همان ردیفِ حذف‌شده در identity map می‌ماند و `findByDate()` تستِ
        // بعدی آن را برمی‌گرداند؛ آن‌وقت upsert فکر می‌کند رکورد هست و چیزی نمی‌نویسد.
        // این تست‌ها تنها وقتی جدا اجرا می‌شدند سبز بودند — دقیقاً نشانهٔ همین.
        $this->em->clear();

        parent::tearDown();
    }

    private function dayAfter(int $midnight, int $days): int
    {
        return (new \DateTimeImmutable('@' . $midnight))
            ->setTimezone(new \DateTimeZone(self::TEHRAN))
            ->modify("+$days day")
            ->setTime(0, 0)
            ->getTimestamp();
    }

    /** @return array{0: User, 1: DoctorAddress, 2: string} کاربر، شعبه، uuid منبع */
    private function resourceWithShifts(array $days = [0, 1, 2, 3, 4]): array
    {
        [$user, , $address] = $this->clinicWithAddress();
        $type     = $this->resourceType($address, 'operator', 'اپراتور');
        $created  = $this->createResource($user, $address, $type, ['name' => 'اپراتور مریم']);
        $uuid     = $created['data']['uuid'];

        $shifts = [];
        foreach ($days as $day) {
            $shifts[$day] = [['start_minute' => 540, 'end_minute' => 1020]];  // 09:00-17:00
        }

        $this->authJson('PUT', "/api/v1/resource/$uuid/calendar", $user, ['days' => $shifts]);
        self::assertSame(200, $this->responseCode());

        return [$user, $address, $uuid];
    }

    private function availability(User $user, string $uuid, int $from, int $to): array
    {
        $body = $this->authJson('GET', "/api/v1/resource/$uuid/availability?from=$from&to=$to", $user);
        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        return $body['data']['days'];
    }

    public function testFiveWorkingDaysAndAnEmptyFriday(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $this->dayAfter($saturday, 6));

        self::assertCount(7, $days);

        $withHours = array_values(array_filter($days, static fn (array $d): bool => $d['intervals'] !== []));
        self::assertCount(5, $withHours, 'شنبه تا چهارشنبه');

        // ۶ = جمعه در قرارداد ۰=شنبه
        $friday = $days[6];
        self::assertSame(6, $friday['day_of_week']);
        self::assertSame([], $friday['intervals']);
        self::assertContains('no_shift', $friday['reasons']);

        self::assertSame(480, $days[0]['total_minutes'], '۹ تا ۱۷ یعنی ۴۸۰ دقیقه');
    }

    public function testAnExceptionEmptiesThatDay(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();
        $tuesday  = $this->dayAfter($saturday, 3);

        $this->authJson('POST', "/api/v1/resource/$uuid/exception", $user, [
            'type'      => ResourceException::TYPE_LEAVE,
            'starts_at' => $tuesday,
            'ends_at'   => $this->dayAfter($tuesday, 1),
            'reason'    => 'مرخصی استحقاقی',
        ]);
        self::assertSame(201, $this->responseCode());

        $days = $this->availability($user, $uuid, $saturday, $this->dayAfter($saturday, 6));

        self::assertSame([], $days[3]['intervals']);
        self::assertContains('exception', $days[3]['reasons']);
        self::assertNotSame([], $days[2]['intervals'], 'روزهای دیگر دست‌نخورده‌اند');
    }

    /** استثنای نیم‌روزه فقط همان تکه را می‌بُرد، نه کل روز. */
    public function testHalfDayExceptionCutsOnlyItsOwnWindow(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();
        $monday   = $this->dayAfter($saturday, 2);

        $this->authJson('POST', "/api/v1/resource/$uuid/exception", $user, [
            'type'      => ResourceException::TYPE_MAINTENANCE,
            'starts_at' => $monday + 14 * 3600,
            'ends_at'   => $monday + 18 * 3600,
        ]);
        self::assertSame(201, $this->responseCode());

        $days = $this->availability($user, $uuid, $monday, $monday);

        self::assertCount(1, $days[0]['intervals']);
        self::assertSame($monday + 9 * 3600, $days[0]['intervals'][0]['start']);
        self::assertSame($monday + 14 * 3600, $days[0]['intervals'][0]['end']);
        self::assertSame(300, $days[0]['total_minutes'], '۹ تا ۱۴ یعنی ۳۰۰ دقیقه');
    }

    /** دو استثنای هم‌پوشان مجازند و اتحاد گرفته می‌شود، نه خطا. */
    public function testOverlappingExceptionsAreUnioned(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();

        foreach ([[10, 13], [12, 16]] as [$startHour, $endHour]) {
            $this->authJson('POST', "/api/v1/resource/$uuid/exception", $user, [
                'type'      => ResourceException::TYPE_ABSENCE,
                'starts_at' => $saturday + $startHour * 3600,
                'ends_at'   => $saturday + $endHour * 3600,
            ]);
            self::assertSame(201, $this->responseCode());
        }

        $days = $this->availability($user, $uuid, $saturday, $saturday);

        // ۹-۱۰ و ۱۶-۱۷ باقی می‌ماند؛ ۱۰ تا ۱۶ یکجا بریده می‌شود.
        self::assertCount(2, $days[0]['intervals']);
        self::assertSame(120, $days[0]['total_minutes']);
    }

    /** تعطیل رسمی بدون هیچ ثبت دستی، روز را برای همهٔ محیط‌ها می‌بندد. */
    public function testNationalHolidayClosesTheDayForEveryone(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->farSaturday();
        $holidays = static::getContainer()->get(HolidayService::class);

        $jalali = static::getContainer()->get(\App\Representation\Service\JalaliDateService::class);
        [$jy, $jm, $jd] = $jalali->toJalali(
            (new \DateTimeImmutable('@' . $saturday))->setTimezone(new \DateTimeZone(self::TEHRAN)),
        );

        $holidays->upsertNational($jy, $jm, $jd, 'تعطیل آزمایشی');

        $days = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertSame([], $days[0]['intervals']);
        self::assertContains('national_holiday', $days[0]['reasons']);
    }

    /** محیطی که آن روز کار می‌کند با override باز می‌شود — فقط منابع همان محیط. */
    public function testTenantOverrideReopensANationalHoliday(): void
    {
        [$user, , $uuid]        = $this->resourceWithShifts();
        [$otherUser, , $otherUuid] = $this->resourceWithShifts();

        $saturday = $this->farSaturday();
        $holidays = static::getContainer()->get(HolidayService::class);
        $jalali   = static::getContainer()->get(\App\Representation\Service\JalaliDateService::class);

        [$jy, $jm, $jd] = $jalali->toJalali(
            (new \DateTimeImmutable('@' . $saturday))->setTimezone(new \DateTimeZone(self::TEHRAN)),
        );
        $holidays->upsertNational($jy, $jm, $jd, 'تعطیل آزمایشی');

        $this->authJson('POST', '/api/v1/holiday-overrides', $user, [
            'date'       => $saturday,
            'is_working' => true,
            'note'       => 'کلینیک ما این روز باز است',
        ]);
        self::assertSame(201, $this->responseCode());

        $mine = $this->availability($user, $uuid, $saturday, $saturday);
        self::assertNotSame([], $mine[0]['intervals'], 'محیطِ دارای override باز می‌شود');

        $theirs = $this->availability($otherUser, $otherUuid, $saturday, $saturday);
        self::assertSame([], $theirs[0]['intervals'], 'محیط دیگر همچنان بسته است');
    }

    /** جهت دوم: روزی که رسمی نیست ولی این محیط تعطیل است. */
    public function testTenantOverrideCanCloseANormalDay(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();

        $this->authJson('POST', '/api/v1/holiday-overrides', $user, [
            'date'       => $saturday,
            'is_working' => false,
        ]);
        self::assertSame(201, $this->responseCode());

        $days = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertSame([], $days[0]['intervals']);
        self::assertContains('tenant_holiday', $days[0]['reasons']);
    }

    /** شیفت بیرون از ساعت شعبه رد نمی‌شود — تقاطع گرفته می‌شود. */
    public function testShiftIsIntersectedWithBranchHours(): void
    {
        [$user, $address, $uuid] = $this->resourceWithShifts([0]);

        // شعبه فقط ۱۰ تا ۱۲ باز است؛ شیفت منبع ۹ تا ۱۷.
        $this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, [
            'days' => [0 => [['start_minute' => 600, 'end_minute' => 720]]],
        ]);
        self::assertSame(200, $this->responseCode());

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertCount(1, $days[0]['intervals']);
        self::assertSame(120, $days[0]['total_minutes'], 'تقاطع ۱۰ تا ۱۲');
    }

    /** تقاطع خالی → روز خالی، با دلیل صریح تا از یک باگ قابل تشخیص باشد. */
    public function testEmptyIntersectionReportsOutsideBranchHours(): void
    {
        [$user, $address, $uuid] = $this->resourceWithShifts([0]);

        $this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, [
            'days' => [0 => [['start_minute' => 1080, 'end_minute' => 1200]]],   // 18:00-20:00
        ]);

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertSame([], $days[0]['intervals']);
        self::assertContains('outside_branch_hours', $days[0]['reasons']);
    }

    /** شعبهٔ بدون ساعت کاری = «تعریف‌نشده»، پس شیفت منبع بی‌قید اعمال می‌شود. */
    public function testBranchWithoutHoursDoesNotConstrainTheShift(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts([0]);

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertSame(480, $days[0]['total_minutes']);
    }

    /** روزی که شعبه بسته است با «تعریف‌نشده» یکی نیست. */
    public function testBranchClosedDayIsDistinctFromUndefined(): void
    {
        [$user, $address, $uuid] = $this->resourceWithShifts([0, 1]);

        $this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, [
            'days' => [0 => [['start_minute' => 540, 'end_minute' => 1020]]],
        ]);

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $this->dayAfter($saturday, 1));

        self::assertNotSame([], $days[0]['intervals']);
        self::assertSame([], $days[1]['intervals']);
        self::assertContains('branch_closed', $days[1]['reasons']);
    }

    public function testInactiveResourceHasNoAvailability(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $this->authJson('PATCH', "/api/v1/resource/$uuid", $user, ['active' => false]);

        $saturday = $this->nextSaturday();
        $days     = $this->availability($user, $uuid, $saturday, $saturday);

        self::assertSame([], $days[0]['intervals']);
        self::assertContains('resource_inactive', $days[0]['reasons']);
    }

    public function testRangeLongerThanTheCapIsRejected(): void
    {
        [$user, , $uuid] = $this->resourceWithShifts();

        $saturday = $this->nextSaturday();
        $tooFar   = $this->dayAfter($saturday, 200);

        $this->authJson('GET', "/api/v1/resource/$uuid/availability?from=$saturday&to=$tooFar", $user);

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