<?php

namespace App\Tests\Appointment;

use App\Appointment\Availability\Entity\ResourceOccupancy;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Shared\Event\DomainEvents;
use App\Shared\Event\Entity\DomainEventLog;
use App\Tests\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;

/**
 * مسدودسازی موردی منبع — «این بعدازظهر دستگاه سرویس دارد».
 *
 * با استثنای تقویم فرق دارد: آن الگوی کاری را عوض می‌کند، این یک بازهٔ مشخص را
 * می‌بندد. مرز بینشان همان چیزی است که این تست‌ها نگه می‌دارند.
 */
class ResourceBlockTest extends ApiTestCase
{
    /** @return array{0: User, 1: ClinicResource} */
    private function clinicWithResource(): array
    {
        $user   = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $clinic = new Clinic($user);
        $clinic->setName('کلینیک مسدودسازی');
        $this->em->persist($clinic);
        $this->em->flush();

        $address = DoctorAddress::forClinic($clinic->getId());
        $address->setName('شعبه');
        $this->em->persist($address);

        $type = new ResourceType('clinic', (int) $clinic->getId(), 'device', 'دستگاه');
        $this->em->persist($type);
        $this->em->flush();

        $resource = new ClinicResource($address, $type, 'لیزر ۱');
        $this->em->persist($resource);
        $this->em->flush();

        return [$user, $resource];
    }

    public function testBlockingAFreeRangeSucceedsAndIsListed(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 86400;

        $body = $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $user, [
            'starts_at' => $start,
            'ends_at'   => $start + 4 * 3600,
            'reason'    => 'سرویس دوره‌ای دستگاه',
        ]);

        self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
        self::assertSame('سرویس دوره‌ای دستگاه', $body['data']['segment_name']);

        $list = $this->authJson(
            'GET',
            sprintf('/api/v1/resource/%s/blocks?from=%d&to=%d', $resource->getUuid(), $start - 3600, $start + 86400),
            $user,
        );

        self::assertCount(1, $list['data']);
    }

    public function testAnInvertedRangeIsRejected(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 86400;

        $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $user, [
            'starts_at' => $start,
            'ends_at'   => $start - 3600,
        ]);

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

    /** ⭐ مسدودسازی روی بازه‌ای که نوبت دارد، ظرفیت را پس نمی‌گیرد. */
    public function testBlockingOverABookedRangeIsRejected(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 2 * 86400;

        $em       = static::getContainer()->get(EntityManagerInterface::class);
        $reloaded = $em->getRepository(ClinicResource::class)->find($resource->getId());

        $booked = new ResourceOccupancy($reloaded, $start, $start + 3600);
        $booked->setAppointmentId(4242);
        $em->persist($booked);
        $em->flush();

        $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $user, [
            'starts_at' => $start,
            'ends_at'   => $start + 7200,
        ]);

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

    /** ⭐ اشغالِ یک نوبت واقعی از این مسیر حذف نمی‌شود. */
    public function testAnAppointmentOccupancyCannotBeDeletedAsABlock(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 3 * 86400;

        $em       = static::getContainer()->get(EntityManagerInterface::class);
        $reloaded = $em->getRepository(ClinicResource::class)->find($resource->getId());

        $booked = new ResourceOccupancy($reloaded, $start, $start + 3600);
        $booked->setAppointmentId(777);
        $em->persist($booked);
        $em->flush();

        $this->authJson('DELETE', "/api/v1/resource-block/{$booked->getUuid()}", $user);

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

    public function testAManualBlockCanBeDeleted(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 86400;

        $created = $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $user, [
            'starts_at' => $start,
            'ends_at'   => $start + 3600,
        ]);

        $this->authJson('DELETE', "/api/v1/resource-block/{$created['data']['uuid']}", $user);
        self::assertSame(200, $this->responseCode());

        $list = $this->authJson(
            'GET',
            sprintf('/api/v1/resource/%s/blocks?from=%d&to=%d', $resource->getUuid(), $start - 3600, $start + 86400),
            $user,
        );

        self::assertCount(0, $list['data']);
    }

    public function testAnotherClinicCannotBlockTheResource(): void
    {
        [, $resource] = $this->clinicWithResource();
        [$other]      = $this->clinicWithResource();

        $start = time() + 86400;

        $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $other, [
            'starts_at' => $start,
            'ends_at'   => $start + 3600,
        ]);

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

    /**
     * ظرفیتی که برمی‌گردد باید همان‌قدر شنیده شود که ظرفیتی که می‌رود: مصرف‌کننده‌ای که
     * فقط مسدودسازی را بشنود، منبع را برای همیشه اشغال می‌بیند.
     */
    public function testBlockingAndReleasingEachRecordADomainEvent(): void
    {
        [$user, $resource] = $this->clinicWithResource();

        $start = time() + 86400;

        $created = $this->authJson('POST', "/api/v1/resource/{$resource->getUuid()}/blocks", $user, [
            'starts_at' => $start,
            'ends_at'   => $start + 3600,
        ]);

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

        $blocked = $this->latestEvent(DomainEvents::RESOURCE_BLOCKED);
        self::assertNotNull($blocked);
        self::assertSame($resource->getUuid(), $blocked->getPayload()['resource_uuid']);
        self::assertSame($start, $blocked->getPayload()['starts_at']);

        $this->authJson('DELETE', "/api/v1/resource-block/{$created['data']['uuid']}", $user);
        self::assertSame(200, $this->responseCode());

        $released = $this->latestEvent(DomainEvents::RESOURCE_RELEASED);
        self::assertNotNull($released);
        self::assertSame($created['data']['uuid'], $released->getPayload()['block_uuid']);
    }

    private function latestEvent(string $name): ?DomainEventLog
    {
        return $this->em->getRepository(DomainEventLog::class)
            ->findOneBy(['name' => $name], ['id' => 'DESC']);
    }
}
