<?php

namespace App\Tests\Report;

use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\DoctorAddress;
use App\Patient\Entity\PatientRecord;
use App\Shared\Event\DomainEventPublisher;
use App\Shared\Event\DomainEvents;
use App\Shared\Event\Entity\DomainEventLog;
use App\Shared\Event\Repository\DomainEventLogRepository;
use App\Tests\ApiTestCase;
use Doctrine\ORM\EntityManagerInterface;

/**
 * رویدادهای دامنه و صندوق خروجی — تسک ۱۴.
 *
 * دو تضمین که کل الگو برایشان وجود دارد: رویداد **بعد از** commit منتشر می‌شود، و
 * هیچ رویدادی گم نمی‌شود.
 */
class DomainEventTest extends ApiTestCase
{
    /** @return array{0: User, 1: ServiceSection, 2: DoctorAddress, 3: PatientRecord} */
    private function clinic(): array
    {
        $user   = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
        $clinic = new Clinic($user);
        $clinic->setName('کلینیک رویداد');
        $this->em->persist($clinic);
        $this->em->flush();

        $section = new ServiceSection('clinic', $clinic->getId(), 'لیزر');
        $this->em->persist($section);

        $address = DoctorAddress::forClinic($clinic->getId());
        $address->setName('شعبهٔ مرکزی');
        $this->em->persist($address);

        $patientUser = $this->createUser(['ROLE_USER']);
        $patient     = new PatientRecord('clinic', (int) $clinic->getId(), $patientUser, 'clinic', (int) $clinic->getId());
        $this->em->persist($patient);
        $this->em->flush();

        return [$user, $section, $address, $patient];
    }

    private function service(ServiceSection $section): ServiceItem
    {
        $item = new ServiceItem($section, 'لیزر فول‌بادی');
        $item->setSoloDurationMinutes(30);
        $item->setPriceRials(4_000_000);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    private function publisher(): DomainEventPublisher
    {
        return static::getContainer()->get(DomainEventPublisher::class);
    }

    private function repo(): DomainEventLogRepository
    {
        return static::getContainer()->get(DomainEventLogRepository::class);
    }

    private function containerEm(): EntityManagerInterface
    {
        return static::getContainer()->get(EntityManagerInterface::class);
    }

    // ── قرارداد ─────────────────────────────────────────────────────────────

    /** نام رویداد قرارداد عمومی است؛ تایپو باید همان‌جا بترکد نه در سکوت. */
    public function testAnUnknownEventNameIsRejected(): void
    {
        [$user] = $this->clinic();

        self::expectException(\InvalidArgumentException::class);

        $this->publisher()->record('clinic', 1, 'AppointmentBookd', ['appointment_uuid' => 'x']);
    }

    /** ⭐ payload فقط اسکالر و uuid — هیچ entity ای در رویداد نیست. */
    public function testNonScalarPayloadValuesAreDropped(): void
    {
        $event = new DomainEventLog('clinic', 1, DomainEvents::APPOINTMENT_BOOKED, [
            'appointment_uuid' => 'abc',
            'count'            => 3,
            'nested'           => ['a' => 1],
            'object'           => new \stdClass(),
        ]);

        self::assertSame(['appointment_uuid' => 'abc', 'count' => 3], $event->getPayload());
    }

    // ── انتشار بعد از commit ────────────────────────────────────────────────

    /** ⭐⭐ تراکنشی که برمی‌گردد، هیچ رویدادی جا نمی‌گذارد. */
    public function testARolledBackTransactionLeavesNoEvent(): void
    {
        $this->clinic();

        $before = $this->repo()->count([]);
        $em     = $this->containerEm();

        $em->beginTransaction();

        try {
            $this->publisher()->record('clinic', 999, DomainEvents::APPOINTMENT_BOOKED, ['appointment_uuid' => 'ghost']);
            $em->flush();
        } finally {
            $em->rollback();
            $em->clear();
        }

        self::assertSame($before, $this->repo()->count([]), 'رویداد نباید از تراکنشِ برگشته جا بماند');
    }

    // ── صندوق خروجی ────────────────────────────────────────────────────────

    public function testPendingEventsArePublishedAndMarked(): void
    {
        [$user, $section, $address, $patient] = $this->clinic();

        $event = $this->publisher()->recordAndFlush(
            'clinic',
            (int) $address->getClinicId(),
            DomainEvents::PACKAGE_PURCHASED,
            ['patient_package_uuid' => 'pkg-1'],
        );

        self::assertNull($event->getPublishedAt());
        self::assertContains($event->getUuid(), array_map(
            static fn (DomainEventLog $e): string => $e->getUuid(),
            $this->repo()->findPending(500),
        ));

        $command = static::getContainer()->get(\App\Shared\Event\Command\PublishDomainEventsCommand::class);
        $tester  = new \Symfony\Component\Console\Tester\CommandTester($command);
        $tester->execute(['--limit' => '500']);

        $this->containerEm()->clear();

        $reloaded = $this->repo()->findOneBy(['uuid' => $event->getUuid()]);

        self::assertNotNull($reloaded->getPublishedAt(), 'رویداد باید منتشر و علامت‌گذاری شود');
        self::assertSame(0, $reloaded->getAttempts());
    }

    /** ردیفی که سقف تلاش را رد کرده دیگر برداشته نمی‌شود، ولی حذف هم نمی‌شود. */
    public function testAnExhaustedEventIsNoLongerPickedUpButStays(): void
    {
        [$user, , $address] = $this->clinic();

        $event = $this->publisher()->recordAndFlush(
            'clinic',
            (int) $address->getClinicId(),
            DomainEvents::CREDIT_CONSUMED,
            ['patient_package_uuid' => 'pkg-2'],
        );

        for ($i = 0; $i < DomainEventLog::MAX_ATTEMPTS; $i++) {
            $event->markFailed('اتصال Redis برقرار نشد');
        }

        $this->containerEm()->flush();

        $pendingUuids = array_map(
            static fn (DomainEventLog $e): string => $e->getUuid(),
            $this->repo()->findPending(500),
        );

        self::assertNotContains($event->getUuid(), $pendingUuids);
        self::assertNotNull($this->repo()->findOneBy(['uuid' => $event->getUuid()]), 'ردیف مرده باید بماند تا دیده شود');
        self::assertSame('اتصال Redis برقرار نشد', $event->getLastError());
    }

    // ── رویدادهای واقعی ────────────────────────────────────────────────────

    public function testSellingAPackageRecordsItsEvent(): void
    {
        [$user, $section, , $patient] = $this->clinic();
        $service = $this->service($section);

        $package = $this->authJson('POST', '/api/v1/packages', $user, [
            'name'          => '۶ جلسه',
            'session_count' => 6,
            'price_rials'   => 10_000_000,
            'service_uuids' => [$service->getUuid()],
        ])['data'];

        $this->authJson('POST', "/api/v1/patient/{$patient->getUuid()}/package", $user, [
            'package_uuid' => $package['uuid'],
        ]);
        self::assertSame(201, $this->responseCode());

        $names = array_map(
            static fn (DomainEventLog $e): string => $e->getName(),
            $this->repo()->search(DomainEvents::PACKAGE_PURCHASED, null, null, 10),
        );

        self::assertContains(DomainEvents::PACKAGE_PURCHASED, $names);
    }

    public function testStartingACourseRecordsItsEvent(): void
    {
        [$user, $section, , $patient] = $this->clinic();
        $service = $this->service($section);

        $protocol = $this->authJson('POST', '/api/v1/course-protocols', $user, [
            'service_uuid'  => $service->getUuid(),
            'session_count' => 4,
            'min_days'      => 7,
            'ideal_days'    => 14,
            'max_days'      => 21,
        ])['data'];

        $this->authJson('POST', '/api/v1/treatment-course', $user, [
            'patient_uuid'  => $patient->getUuid(),
            'protocol_uuid' => $protocol['uuid'],
        ]);
        self::assertSame(201, $this->responseCode());

        $events = $this->repo()->search(DomainEvents::COURSE_STARTED, null, null, 10);

        self::assertNotEmpty($events);
        self::assertArrayHasKey('course_uuid', $events[0]->getPayload());
    }

    // ── دسترسی ──────────────────────────────────────────────────────────────

    public function testOnlyAdminsCanReadTheEventLog(): void
    {
        [$user] = $this->clinic();

        $this->authJson('GET', '/api/v1/domain-events', $user);
        self::assertSame(403, $this->responseCode());

        $admin = $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);

        $this->authJson('GET', '/api/v1/domain-events', $admin);
        self::assertSame(200, $this->responseCode());
    }
}
