<?php

namespace App\Tests\Appointment;

use App\Appointment\Entity\WeeklySchedule;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Tests\ApiTestCase;

/**
 * یک «محل نوبت‌دهی» فقط وقتی وجود دارد که هم آدرس داشته باشد و هم شیفتی روی همان
 * آدرس. برنامه‌ای که به آدرسِ محیط دیگر (یا هیچ آدرسی) اشاره می‌کند قابل رزرو
 * نیست و نباید به بیمار پیشنهاد شود.
 *
 * همچنین خالی‌بودن یک روز چهار دلیل متفاوت دارد و پنل باید بتواند تفکیکشان کند.
 */
class BookingLocationValidityTest extends ApiTestCase
{
    private function makeDoctor(): Doctor
    {
        $user   = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $doctor = new Doctor($user, 'دکتر محل');
        $doctor->setMobileNumber($user->getMobileNumber());
        $this->em->persist($doctor);
        $this->em->flush();

        return $doctor;
    }

    private function makeClinicWith(Doctor $doctor): Clinic
    {
        $clinic = new Clinic($this->createUser(['ROLE_USER', 'ROLE_CLINIC']));
        $clinic->setName('کلینیک محل');
        $clinic->getDoctors()->add($doctor);
        $this->em->persist($clinic);
        $this->em->flush();

        return $clinic;
    }

    /** برنامه‌ای که همهٔ روزها یک شیفت روی $locationId دارد. */
    private function scheduleFor(Doctor $doctor, ?Clinic $clinic, ?int $locationId): WeeklySchedule
    {
        $day = ['sessions' => [array_filter([
            'active'               => true,
            'location_id'          => $locationId,
            'start_time'           => '09:00',
            'end_time'             => '13:00',
            'duration_per_patient' => 20,
        ], fn($v) => $v !== null)]];

        $schedule = $this->newWeeklySchedule(
            $doctor,
            array_fill_keys(array_map('strval', range(0, 6)), $day),
            $clinic
        );
        $this->em->persist($schedule);
        $this->em->flush();

        return $schedule;
    }

    private function locations(Doctor $doctor, string $query = ''): array
    {
        $body = $this->authJson(
            'GET',
            '/api/v1/appointment-booking-locations/' . $doctor->getUuid() . $query,
            $doctor->getUser()
        );

        return $body['data']['booking_locations'] ?? [];
    }

    public function testLocationWithoutAnyAddressIsNotReturned(): void
    {
        $doctor = $this->makeDoctor();
        $this->scheduleFor($doctor, null, null);

        self::assertSame([], $this->locations($doctor), 'محلی که آدرس ندارد نباید محل به حساب بیاید');
    }

    public function testLocationWhoseShiftsPointOutsideItsContextIsNotReturned(): void
    {
        $doctor = $this->makeDoctor();
        $clinic = $this->makeClinicWith($doctor);

        $clinicAddress = DoctorAddress::forClinic($clinic->getId());
        $personal      = DoctorAddress::forDoctor($doctor);
        $this->em->persist($clinicAddress);
        $this->em->persist($personal);
        $this->em->flush();

        // برنامهٔ شخصی که شیفتش روی آدرس کلینیک نشسته — دقیقاً حالتی که در dev دیده شد.
        $this->scheduleFor($doctor, null, $clinicAddress->getId());

        self::assertSame([], $this->locations($doctor));
    }

    public function testValidLocationIsReturnedWithItsOpeningHours(): void
    {
        $doctor  = $this->makeDoctor();
        $address = DoctorAddress::forDoctor($doctor);
        $this->em->persist($address);
        $this->em->flush();

        $this->scheduleFor($doctor, null, $address->getId());

        $locations = $this->locations($doctor);
        self::assertCount(1, $locations);
        self::assertSame('personal', $locations[0]['type']);
        self::assertSame($address->getUuid(), $locations[0]['location_uuid']);
        self::assertCount(7, $locations[0]['opening_hours']);
        self::assertSame($address->getId(), $locations[0]['opening_hours'][0]['location_id']);
    }

    public function testDateParameterReportsPerDayAvailability(): void
    {
        $doctor  = $this->makeDoctor();
        $address = DoctorAddress::forDoctor($doctor);
        $this->em->persist($address);
        $this->em->flush();

        // فقط شنبه (اندیس 0) شیفت فعال دارد.
        $active = ['sessions' => [[
            'active' => true, 'location_id' => $address->getId(),
            'start_time' => '09:00', 'end_time' => '13:00', 'duration_per_patient' => 20,
        ]]];
        $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]);
        $setting['0'] = $active;
        $this->em->persist($this->newWeeklySchedule($doctor, $setting));
        $this->em->flush();

        $saturday = $this->nextWeekday(6);   // date('w'): 6 = Saturday
        $sunday   = $this->nextWeekday(0);

        $onSaturday = $this->locations($doctor, '?date=' . $saturday);
        $onSunday   = $this->locations($doctor, '?date=' . $sunday);

        self::assertTrue($onSaturday[0]['available_on_date'] ?? null);
        self::assertFalse($onSunday[0]['available_on_date'] ?? null);
    }

    public function testInvalidDateIsRejected(): void
    {
        $doctor = $this->makeDoctor();

        $this->authJson('GET', '/api/v1/appointment-booking-locations/' . $doctor->getUuid() . '?date=2026-13-99', $doctor->getUser());

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

    public function testEmptyReasonDistinguishesTheCauses(): void
    {
        $doctor = $this->makeDoctor();
        $today  = date('Y-m-d');

        // بدون هیچ برنامه‌ای
        $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$today}", $doctor->getUser());
        self::assertSame('no_schedule', $body['data']['empty_reason'] ?? null);

        // برنامه هست ولی این روز شیفت ندارد
        $address = DoctorAddress::forDoctor($doctor);
        $this->em->persist($address);
        $this->em->flush();

        $setting = array_fill_keys(array_map('strval', range(0, 6)), ['sessions' => []]);
        $this->em->persist($this->newWeeklySchedule($doctor, $setting));
        $this->em->flush();

        $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$today}", $doctor->getUser());
        self::assertSame('day_off', $body['data']['empty_reason'] ?? null);

        // تاریخ خارج از بازهٔ نوبت‌دهی
        $farFuture = date('Y-m-d', strtotime('+2 years'));
        $body = $this->authJson('GET', "/api/v1/appointment-slots?doctor_uuid={$doctor->getUuid()}&date={$farFuture}", $doctor->getUser());
        self::assertSame('outside_window', $body['data']['empty_reason'] ?? null);
    }

    /**
     * اولین وقوعِ آن روز هفته در آینده. از فردا شروع می‌شود چون شیفت‌های امروز
     * ممکن است گذشته باشند و getAvailableSlots اسلات گذشته را برنمی‌گرداند —
     * وگرنه تست بسته به ساعت اجرا نتیجهٔ متفاوت می‌دهد.
     *
     * @param int $phpDow خروجی date('w') — 0=یکشنبه ... 6=شنبه
     */
    private function nextWeekday(int $phpDow): string
    {
        for ($i = 1; $i <= 7; $i++) {
            $ts = strtotime("+{$i} day");
            if ((int) date('w', $ts) === $phpDow) {
                return date('Y-m-d', $ts);
            }
        }

        return date('Y-m-d');
    }
}
