<?php

namespace App\Tests\Appointment;

use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Clinic\Entity\Clinic;
use App\Clinic\Repository\ClinicDoctorPermissionRepository;
use App\Doctor\Entity\Doctor;
use App\Secretary\Entity\DoctorSecretary;
use App\Shared\Context\EntityContext;
use App\Tests\ApiTestCase;

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

        return $doctor;
    }

    /** @return array{0: User, 1: Clinic} */
    private function makeClinicWith(Doctor ...$doctors): array
    {
        $owner  = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $clinic = new Clinic($owner);
        $clinic->setName('کلینیک تست');
        foreach ($doctors as $d) {
            $clinic->getDoctors()->add($d);
        }
        $this->em->persist($clinic);
        $this->em->flush();

        return [$owner, $clinic];
    }

    private function makeAppointment(Doctor $doctor, ?Clinic $clinic, ?User $patient = null): Appointment
    {
        $patient ??= $this->createUser();
        $start     = strtotime('+3 days 10:00');

        $appointment = $this->newAppointment($doctor, $patient, $start, $start + 900, $clinic);
        $this->em->persist($appointment);
        $this->em->flush();

        return $appointment;
    }

    private function makeClinicSecretary(Clinic $clinic, Doctor $doctor, array $permissionPatch = []): User
    {
        $user      = $this->createUser(['ROLE_USER', 'ROLE_SECRETARY']);
        $secretary = new DoctorSecretary($doctor, $user, $clinic);
        if ($permissionPatch !== []) {
            $secretary->mergePermissions(['resources' => ['appointments' => $permissionPatch]]);
        }
        $this->em->persist($secretary);
        $this->em->flush();

        static::getContainer()->get(UserActiveContextRepository::class)
            ->upsert($user, $clinic->getUuid(), EntityContext::TYPE_CLINIC);

        return $user;
    }

    public function testClinicOwnerCanViewClinicAppointment(): void
    {
        $doctor           = $this->makeDoctor();
        [$owner, $clinic] = $this->makeClinicWith($doctor);
        $appointment      = $this->makeAppointment($doctor, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $owner);

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

    public function testClinicOwnerCanUpdateAndMoveClinicAppointment(): void
    {
        $doctor           = $this->makeDoctor();
        [$owner, $clinic] = $this->makeClinicWith($doctor);
        $appointment      = $this->makeAppointment($doctor, $clinic);
        $newStart         = strtotime('+4 days 11:00');

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $owner, [
            'slot_start' => $newStart,
            'slot_end'   => $newStart + 900,
            'note'       => 'جابه‌جا شد',
            'version'    => $appointment->getVersion(),
        ]);

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

    public function testClinicOwnerCanChangeStatusOfClinicAppointment(): void
    {
        $doctor           = $this->makeDoctor();
        [$owner, $clinic] = $this->makeClinicWith($doctor);
        $appointment      = $this->makeAppointment($doctor, $clinic);

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $owner, [
            'status'  => Appointment::STATUS_CONFIRMED,
            'version' => $appointment->getVersion(),
        ]);

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

    public function testClinicOwnerCanTransferAppointmentToReserveAndBack(): void
    {
        $doctor           = $this->makeDoctor();
        [$owner, $clinic] = $this->makeClinicWith($doctor);
        $appointment      = $this->makeAppointment($doctor, $clinic);
        $midnight         = strtotime('+3 days 00:00');

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $owner, [
            'is_reserve' => true,
            'slot_start' => $midnight,
            'slot_end'   => $midnight,
            'version'    => $appointment->getVersion(),
        ]);
        self::assertSame(200, $this->responseCode(), 'انتقال به لیست رزرو');

        $this->em->clear();
        $reloaded = $this->em->getRepository(Appointment::class)->find($appointment->getId());
        self::assertTrue($reloaded->isReserve());

        $back = strtotime('+5 days 09:00');
        $this->authJson('PATCH', "/api/v1/appointment/{$reloaded->getUuid()}", $owner, [
            'is_reserve' => false,
            'slot_start' => $back,
            'slot_end'   => $back + 900,
            'version'    => $reloaded->getVersion(),
        ]);
        self::assertSame(200, $this->responseCode(), 'بازگشت از لیست رزرو');
    }

    public function testClinicOwnerCanReadAppointmentEvents(): void
    {
        $doctor           = $this->makeDoctor();
        [$owner, $clinic] = $this->makeClinicWith($doctor);
        $appointment      = $this->makeAppointment($doctor, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}/events", $owner);

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

    public function testClinicOwnerCannotTouchDoctorPersonalAppointment(): void
    {
        $doctor      = $this->makeDoctor();
        [$owner]     = $this->makeClinicWith($doctor);
        $appointment = $this->makeAppointment($doctor, null);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $owner);

        self::assertSame(403, $this->responseCode(), 'مطب شخصی پزشک از دسترس کلینیک خارج است');
    }

    public function testForeignClinicOwnerIsDenied(): void
    {
        $doctor        = $this->makeDoctor();
        [, $clinic]    = $this->makeClinicWith($doctor);
        [$otherOwner]  = $this->makeClinicWith($this->makeDoctor('دکتر دیگر'));
        $appointment   = $this->makeAppointment($doctor, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $otherOwner);

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

    public function testMemberDoctorLosesAccessWhenDeactivated(): void
    {
        $member          = $this->makeDoctor('دکتر عضو');
        $colleague       = $this->makeDoctor('همکار');
        [, $clinic]      = $this->makeClinicWith($member, $colleague);
        $appointment     = $this->makeAppointment($colleague, $clinic);

        $permissions = static::getContainer()->get(ClinicDoctorPermissionRepository::class);
        $permissions->getOrCreate($clinic, $member);
        $this->em->flush();

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $member->getUser());
        self::assertSame(200, $this->responseCode(), 'پزشک فعالِ کلینیک نوبت‌های همان کلینیک را می‌بیند');

        $permissions->getOrCreate($clinic, $member)->setActive(false);
        $this->em->flush();

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $member->getUser());
        self::assertSame(403, $this->responseCode(), 'پس از پایان همکاری دسترسی قطع می‌شود');
    }

    public function testClinicSecretaryCanManageAssignedDoctorAppointment(): void
    {
        $doctor      = $this->makeDoctor();
        [, $clinic]  = $this->makeClinicWith($doctor);
        $secretary   = $this->makeClinicSecretary($clinic, $doctor);
        $appointment = $this->makeAppointment($doctor, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $secretary);
        self::assertSame(200, $this->responseCode());

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [
            'status'  => Appointment::STATUS_CONFIRMED,
            'version' => $appointment->getVersion(),
        ]);
        self::assertSame(200, $this->responseCode());
    }

    public function testClinicSecretaryCannotTouchUnassignedDoctorAppointment(): void
    {
        $mine        = $this->makeDoctor('پزشک من');
        $theirs      = $this->makeDoctor('پزشک دیگر');
        [, $clinic]  = $this->makeClinicWith($mine, $theirs);
        $secretary   = $this->makeClinicSecretary($clinic, $mine);
        $appointment = $this->makeAppointment($theirs, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $secretary);

        self::assertSame(403, $this->responseCode(), 'منشی فقط پزشکان تخصیص‌یافتهٔ خودش را دارد');
    }

    public function testSecretaryCancelRequiresCancelPermission(): void
    {
        $doctor      = $this->makeDoctor();
        [, $clinic]  = $this->makeClinicWith($doctor);
        $secretary   = $this->makeClinicSecretary($clinic, $doctor);
        $appointment = $this->makeAppointment($doctor, $clinic);

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [
            'status'  => Appointment::STATUS_CANCELLED_BY_DOCTOR,
            'version' => $appointment->getVersion(),
        ]);

        self::assertSame(403, $this->responseCode(), 'لغو به‌صورت پیش‌فرض برای منشی خاموش است');
    }

    public function testSecretaryWithCancelPermissionCanCancel(): void
    {
        $doctor      = $this->makeDoctor();
        [, $clinic]  = $this->makeClinicWith($doctor);
        $secretary   = $this->makeClinicSecretary($clinic, $doctor, ['cancel' => true]);
        $appointment = $this->makeAppointment($doctor, $clinic);

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $secretary, [
            'status'  => Appointment::STATUS_CANCELLED_BY_DOCTOR,
            'version' => $appointment->getVersion(),
        ]);

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

    public function testInlineStatusCannotBypassCancelGate(): void
    {
        $doctor      = $this->makeDoctor();
        [, $clinic]  = $this->makeClinicWith($doctor);
        $secretary   = $this->makeClinicSecretary($clinic, $doctor);
        $appointment = $this->makeAppointment($doctor, $clinic);

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $secretary, [
            'status'  => Appointment::STATUS_CANCELLED_BY_DOCTOR,
            'version' => $appointment->getVersion(),
        ]);

        self::assertSame(403, $this->responseCode(), 'status درون‌خطی همان گیت لغو را دارد');
    }

    public function testOwnerDoctorKeepsFullAccessToOwnClinicAppointment(): void
    {
        $doctor      = $this->makeDoctor();
        [, $clinic]  = $this->makeClinicWith($doctor);
        $appointment = $this->makeAppointment($doctor, $clinic);

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $doctor->getUser());

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

    public function testPatientCanViewButNotRescheduleOwnAppointment(): void
    {
        $doctor      = $this->makeDoctor();
        $patient     = $this->createUser();
        $appointment = $this->makeAppointment($doctor, null, $patient);
        $newStart    = strtotime('+6 days 10:00');

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $patient);
        self::assertSame(200, $this->responseCode());

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}", $patient, [
            'slot_start' => $newStart,
            'slot_end'   => $newStart + 900,
            'version'    => $appointment->getVersion(),
        ]);
        self::assertSame(403, $this->responseCode(), 'بیمار نوبت خودش را جابه‌جا نمی‌کند');
    }

    public function testPatientCanCancelOwnAppointment(): void
    {
        $doctor      = $this->makeDoctor();
        $patient     = $this->createUser();
        $appointment = $this->makeAppointment($doctor, null, $patient);

        $this->authJson('PATCH', "/api/v1/appointment/{$appointment->getUuid()}/status", $patient, [
            'status'  => Appointment::STATUS_CANCELLED_BY_USER,
            'version' => $appointment->getVersion(),
        ]);

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

    public function testStrangerIsDenied(): void
    {
        $doctor      = $this->makeDoctor();
        $appointment = $this->makeAppointment($doctor, null);
        $stranger    = $this->createUser();

        $this->authJson('GET', "/api/v1/appointment/{$appointment->getUuid()}", $stranger);

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