<?php

namespace App\Tests\Appointment;

use App\Appointment\Entity\Appointment;
use App\Appointment\Entity\WeeklySchedule;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Shared\Constant\ErrorCodes;
use App\Tests\ApiTestCase;

/**
 * `POST /api/v1/appointment/{uuid}/service-reschedule` — کلاینت مدت نمی‌فرستد.
 *
 * تفاوتش با `PATCH`: آنجا کلاینت باید `slot_end` درست را از قبل بداند؛ اینجا فقط
 * `start` می‌دهد. همین است که ورودی دستیِ ساعت را از فرم ویرایش حذف می‌کند.
 */
class ServiceRescheduleTest extends ApiTestCase
{
    private const DAY_OFFSET = '+2 days';

    /** @return array{0:\App\Auth\Entity\User,1:Doctor,2:ServiceSection} */
    private function doctorInMode(string $mode, int $buffer = 0): array
    {
        $owner  = $this->createUser(['ROLE_DOCTOR']);
        $doctor = new Doctor($owner, 'دکتر جابه‌جایی سرویسی');
        $this->em->persist($doctor);
        $this->em->flush();

        $date   = date('Y-m-d', strtotime(self::DAY_OFFSET));
        $dayKey = (string) (((int) date('w', strtotime($date)) + 1) % 7);
        $schedule = $this->newWeeklySchedule($doctor, [
            $dayKey => ['sessions' => [[
                'active' => true, 'start_time' => '09:00', 'end_time' => '18:00',
                'duration_per_patient' => 20, 'location_id' => 1,
            ]]],
        ]);
        $schedule->setMeta(['booking_mode' => $mode, 'buffer_minutes' => $buffer]);
        $this->em->persist($schedule);

        $section = new ServiceSection('doctor', $doctor->getId(), 'بخش جابه‌جایی');
        $this->em->persist($section);
        $this->em->flush();

        return [$owner, $doctor, $section];
    }

    private function service(ServiceSection $section, string $name, int $minutes, bool $bookable = true): ServiceItem
    {
        $item = new ServiceItem($section, $name, 0);
        $item->setDurationMinutes($minutes)->setBookable($bookable);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    private function at(string $time): int
    {
        return (int) strtotime(date('Y-m-d', strtotime(self::DAY_OFFSET)) . ' ' . $time);
    }

    private function appointment(Doctor $doctor, string $time, int $minutes, array $items = []): Appointment
    {
        $patient = $this->createUser(['ROLE_USER']);
        $start   = $this->at($time);
        $appt    = $this->newAppointment($doctor, $patient, $start, $start + $minutes * 60);
        $appt->transitionTo(Appointment::STATUS_CONFIRMED);
        if ($items !== []) {
            $appt->replaceServiceItems($items);
        }
        $this->em->persist($appt);
        $this->em->flush();

        return $appt;
    }

    private function move(Appointment $appt, \App\Auth\Entity\User $actor, array $body): array
    {
        return $this->authJson('POST', '/api/v1/appointment/' . $appt->getUuid() . '/service-reschedule', $actor, $body);
    }

    // ── ✅ موفق ──────────────────────────────────────────────────────────────

    public function testMovingKeepsDurationWithoutTheClientSendingIt(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        $res = $this->move($appt, $owner, ['start' => $this->at('14:00')]);

        self::assertSame(200, $this->responseCode());
        self::assertSame(30, $res['data']['total_duration_minutes']);
        self::assertSame($this->at('14:00'), $res['data']['slot_start']);
        self::assertSame($this->at('14:30'), $res['data']['slot_end'], 'مدت را سرور حساب کرد');
    }

    public function testDroppingAServiceShrinksTheDurationAutomatically(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $face   = $this->service($section, 'صورت', 20);
        $bikini = $this->service($section, 'بیکینی', 15);
        $appt   = $this->appointment($doctor, '10:00', 35, [$face, $bikini]);

        $res = $this->move($appt, $owner, [
            'start'              => $this->at('14:00'),
            'service_item_uuids' => [$face->getUuid()],
        ]);

        self::assertSame(200, $this->responseCode());
        self::assertSame(20, $res['data']['total_duration_minutes'], 'کلاینت هیچ عددی نفرستاد');
        self::assertSame($this->at('14:20'), $res['data']['slot_end']);
    }

    public function testCurrentTimeIsOfferedBackToTheAppointmentItself(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $short = $this->service($section, 'کوتاه', 30);
        $long  = $this->service($section, 'بلند', 60);
        $appt  = $this->appointment($doctor, '10:00', 30, [$short]);

        // همان ساعت ۱۰:۰۰، سرویس بلندتر — بدون exclude، نوبت خودش را اشغال می‌دید.
        $res = $this->move($appt, $owner, [
            'start'              => $this->at('10:00'),
            'service_item_uuids' => [$long->getUuid()],
        ]);

        self::assertSame(200, $this->responseCode());
        self::assertSame(60, $res['data']['total_duration_minutes']);
        self::assertSame($this->at('11:00'), $res['data']['slot_end']);
    }

    public function testInactiveServiceOfAnExistingAppointmentYieldsAWarningNotAnError(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر بازنشسته', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        // کلینیک سرویس را غیرفعال می‌کند؛ نوبت موجود باید همچنان جابه‌جا شود.
        $item->setBookable(false);
        $this->em->flush();

        $res = $this->move($appt, $owner, ['start' => $this->at('14:00')]);

        self::assertSame(200, $this->responseCode(), 'نوبت موجود نباید برای همیشه قفل شود');
        self::assertCount(1, $res['data']['warnings']);
        self::assertStringContainsString('لیزر بازنشسته', $res['data']['warnings'][0]);
    }

    // ── ❌ خطا ───────────────────────────────────────────────────────────────

    public function testSlotModeAppointmentIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SLOT);
        $item = $this->service($section, 'سرویس', 30);
        $appt = $this->appointment($doctor, '10:00', 20, [$item]);

        $res = $this->move($appt, $owner, ['start' => $this->at('14:00')]);

        self::assertSame(422, $this->responseCode());
        self::assertSame(ErrorCodes::ERR_APPOINTMENT_004, $res['errors'][0]['code']);
    }

    public function testTimeOutsideTheOfferedListIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        // ۰۳:۰۰ بیرون از شیفت ۰۹:۰۰–۱۸:۰۰ است. `isSlotTaken` تنها این را نمی‌گرفت.
        $res = $this->move($appt, $owner, ['start' => $this->at('03:00')]);

        self::assertSame(422, $this->responseCode());
        self::assertSame(ErrorCodes::ERR_APPOINTMENT_001, $res['errors'][0]['code']);
        self::assertSame('start', $res['errors'][0]['field']);
    }

    public function testReserveEntryIsRejectedWithAPointerToPatch(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item     = $this->service($section, 'لیزر', 30);
        $midnight = $this->at('00:00');

        $patient = $this->createUser(['ROLE_USER']);
        $reserve = $this->newAppointment($doctor, $patient, $midnight, $midnight);
        $reserve->rescheduleTo($midnight, $midnight, true);
        $reserve->replaceServiceItems([$item]);
        $this->em->persist($reserve);
        $this->em->flush();

        $res = $this->move($reserve, $owner, ['start' => $this->at('14:00')]);

        self::assertSame(422, $this->responseCode());
        self::assertStringContainsString('ویرایش نوبت', $res['errors'][0]['message']);
    }

    public function testMissingStartIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        $res = $this->move($appt, $owner, []);

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

    public function testForeignServiceIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $mine = $this->service($section, 'خودم', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$mine]);

        [, , $otherSection] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $foreign = $this->service($otherSection, 'بیگانه', 30);

        $res = $this->move($appt, $owner, [
            'start'              => $this->at('14:00'),
            'service_item_uuids' => [$foreign->getUuid()],
        ]);

        self::assertSame(422, $this->responseCode());
        self::assertSame('سرویس انتخاب‌شده به این محل نوبت‌دهی تعلق ندارد', $res['errors'][0]['message']);
    }

    public function testAddingAnInactiveServiceExplicitlyIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $active   = $this->service($section, 'فعال', 30);
        $inactive = $this->service($section, 'غیرفعال', 20, bookable: false);
        $appt     = $this->appointment($doctor, '10:00', 30, [$active]);

        $res = $this->move($appt, $owner, [
            'start'              => $this->at('14:00'),
            'service_item_uuids' => [$active->getUuid(), $inactive->getUuid()],
        ]);

        self::assertSame(422, $this->responseCode(), 'افزودن سرویس غیرفعال تازه مجاز نیست');
        self::assertSame('این سرویس برای نوبت‌دهی فعال نیست', $res['errors'][0]['message']);
    }

    // ── ⚠️ مرزی ──────────────────────────────────────────────────────────────

    public function testPastStartIsRejected(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        $res = $this->move($appt, $owner, ['start' => time() - 3600]);

        self::assertSame(422, $this->responseCode());
        self::assertSame('زمان انتخابی در گذشته است', $res['errors'][0]['message']);
    }

    public function testAppointmentWithoutAnyServiceIsRejectedWithAClearReason(): void
    {
        [$owner, $doctor] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $appt = $this->appointment($doctor, '10:00', 30);

        $res = $this->move($appt, $owner, ['start' => $this->at('14:00')]);

        self::assertSame(422, $this->responseCode());
        self::assertStringContainsString('سرویسی ندارد', $res['errors'][0]['message']);
    }

    public function testStaleVersionYieldsConflict(): void
    {
        [$owner, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        $res = $this->move($appt, $owner, [
            'start'   => $this->at('14:00'),
            'version' => $appt->getVersion() + 5,
        ]);

        self::assertSame(409, $this->responseCode());
        self::assertSame(ErrorCodes::ERR_CONFLICT_001, $res['errors'][0]['code']);
    }

    public function testAnotherUsersAppointmentIsForbidden(): void
    {
        [, $doctor, $section] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);
        $item = $this->service($section, 'لیزر', 30);
        $appt = $this->appointment($doctor, '10:00', 30, [$item]);

        $stranger = $this->createUser(['ROLE_USER']);
        $this->move($appt, $stranger, ['start' => $this->at('14:00')]);

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

    public function testUnknownAppointmentIsNotFound(): void
    {
        [$owner] = $this->doctorInMode(WeeklySchedule::MODE_SERVICE);

        $this->authJson(
            'POST',
            '/api/v1/appointment/00000000-0000-4000-8000-000000000000/service-reschedule',
            $owner,
            ['start' => $this->at('14:00')],
        );

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