<?php

namespace App\Tests\ClinicInvitation;

use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicInvitation\Entity\ClinicDoctorInvitation;
use App\Doctor\Entity\Doctor;
use App\Tests\ApiTestCase;

/**
 * Covers the full clinic → doctor invitation lifecycle: inviting provisions a
 * doctor profile, accepting creates login credentials and links the doctor to
 * the clinic, and every admin action on an invitation stays reachable.
 */
class ClinicInvitationFlowTest extends ApiTestCase
{
    private function createClinicOwner(): array
    {
        $owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $clinic = new Clinic($owner);
        $clinic->setName('کلینیک تست');
        $this->em->persist($clinic);
        $this->em->flush();

        return [$owner, $clinic];
    }

    private function invitedMobile(): string
    {
        return '09' . str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
    }

    public function testInviteCreatesDoctorProfileWithoutLinkingClinic(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $mobile = $this->invitedMobile();

        $res = $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $mobile,
            'name'   => 'دکتر تست',
        ]);

        self::assertSame(201, $this->responseCode());
        self::assertNotNull($res['data']['doctor'], 'invite must provision a doctor profile');

        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['mobileNumber' => $mobile]);
        self::assertNotNull($doctor);
        self::assertSame('unclaimed', $doctor->getOwnerStatus());
        self::assertNull($doctor->getUser()->getPasswordHash(), 'no password before acceptance');

        $this->em->refresh($clinic);
        self::assertFalse($clinic->getDoctors()->contains($doctor), 'clinic link happens only on accept');
    }

    public function testAcceptLinksDoctorAndIssuesLoginCredentials(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $mobile = $this->invitedMobile();

        $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $mobile,
            'name'   => 'دکتر تست',
        ]);

        $inv = $this->em->getRepository(ClinicDoctorInvitation::class)->findOneBy(['mobile' => $mobile]);
        $this->client->request('POST', "/api/v1/clinic-invitation/{$inv->getToken()}/accept");
        self::assertSame(200, $this->responseCode());

        $this->em->clear();
        $inv    = $this->em->getRepository(ClinicDoctorInvitation::class)->findOneBy(['mobile' => $mobile]);
        $doctor = $this->em->getRepository(Doctor::class)->findOneBy(['mobileNumber' => $mobile]);
        $user   = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);

        self::assertSame(ClinicDoctorInvitation::STATUS_ACCEPTED, $inv->getStatus());
        self::assertSame($doctor->getId(), $inv->getDoctor()?->getId());
        self::assertSame('claimed', $doctor->getOwnerStatus());
        self::assertNotNull($user->getPasswordHash(), 'accepted doctor must be able to log in');
        self::assertContains('ROLE_DOCTOR', $user->getRoles());
        self::assertTrue($inv->getClinic()->getDoctors()->contains($doctor));
    }

    public function testAcceptDoesNotOverwriteExistingPassword(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $existing = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $existing->setPasswordHash('$2y$13$alreadySetHashValueForTesting.aaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
        $this->em->flush();
        $mobile = $existing->getMobileNumber();

        $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $mobile,
        ]);

        $inv = $this->em->getRepository(ClinicDoctorInvitation::class)->findOneBy(['mobile' => $mobile]);
        $this->client->request('POST', "/api/v1/clinic-invitation/{$inv->getToken()}/accept");
        self::assertSame(200, $this->responseCode());

        $this->em->clear();
        $reloaded = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]);
        self::assertStringStartsWith('$2y$13$alreadySet', $reloaded->getPasswordHash());
    }

    public function testSuspendAndReactivateInvitation(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $mobile = $this->invitedMobile();

        $created = $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $mobile,
        ]);
        $invUuid = $created['data']['uuid'];

        $this->authJson('PATCH', "/api/v1/admin/clinic/invitation/{$invUuid}/status", $owner, ['status' => 'suspended']);
        self::assertSame(200, $this->responseCode());

        $res = $this->authJson('PATCH', "/api/v1/admin/clinic/invitation/{$invUuid}/status", $owner, ['status' => 'pending']);
        self::assertSame(200, $this->responseCode());
        self::assertSame('pending', $res['data']['status']);

        $this->em->clear();
        $inv = $this->em->getRepository(ClinicDoctorInvitation::class)->findOneBy(['uuid' => $invUuid]);
        self::assertTrue($inv->isUsable(), 'reactivated invitation must have a fresh usable token');
    }

    public function testInvalidStatusIsRejected(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $created = $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $this->invitedMobile(),
        ]);

        $this->authJson('PATCH', "/api/v1/admin/clinic/invitation/{$created['data']['uuid']}/status", $owner, ['status' => 'bogus']);
        self::assertSame(422, $this->responseCode());
    }

    public function testDeleteInvitationReturnsJsonBody(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $created = $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $this->invitedMobile(),
        ]);

        $res = $this->authJson('DELETE', "/api/v1/admin/clinic/invitation/{$created['data']['uuid']}", $owner);
        self::assertSame(200, $this->responseCode());
        self::assertTrue($res['success']);

        $this->authJson('DELETE', "/api/v1/admin/clinic/invitation/{$created['data']['uuid']}", $owner);
        self::assertSame(404, $this->responseCode());
    }

    public function testAcceptedInvitationCannotReturnToPending(): void
    {
        [$owner, $clinic] = $this->createClinicOwner();
        $mobile  = $this->invitedMobile();
        $created = $this->authJson('POST', "/api/v1/admin/clinic/{$clinic->getUuid()}/invite-doctor", $owner, [
            'mobile' => $mobile,
        ]);

        $inv = $this->em->getRepository(ClinicDoctorInvitation::class)->findOneBy(['mobile' => $mobile]);
        $this->client->request('POST', "/api/v1/clinic-invitation/{$inv->getToken()}/accept");

        $this->authJson('PATCH', "/api/v1/admin/clinic/invitation/{$created['data']['uuid']}/status", $owner, ['status' => 'pending']);
        self::assertSame(409, $this->responseCode());
    }
}
