<?php

namespace App\Tests\Report;

use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Doctor\Entity\DoctorAddress;
use App\Tests\ApiTestCase;

/**
 * گزارش بهره‌وری منابع و دقت برنامه — تسک ۱۴.
 *
 * گزارش دقت برنامه تنها بازخوردی است که به کلینیک می‌گوید تعریف بخش‌هایش درست است یا
 * نه؛ بدون آن، ابزار قدرتمند تسک ۰۵ کور کار می‌کند.
 */
class ReportTest extends ApiTestCase
{
    private int $slotCursor = 0;

    /** @return array{0: User, 1: ServiceSection, 2: DoctorAddress, 3: Doctor} */
    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);

        $doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $doctor     = new Doctor($doctorUser, 'دکتر گزارش');
        $this->em->persist($doctor);
        $this->em->flush();

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

    private function service(ServiceSection $section, string $name, int $solo): ServiceItem
    {
        $item = new ServiceItem($section, $name);
        $item->setSoloDurationMinutes($solo);
        $item->setPriceRials(1_000_000);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    /** نوبت انجام‌شده با مدت پیش‌بینی و مدت واقعی مشخص. */
    private function completed(
        Doctor $doctor,
        User $patient,
        ServiceItem $service,
        int $clinicId,
        int $plannedMinutes,
        int $actualMinutes,
        int $daysAgo,
    ): Appointment {
        $em    = static::getContainer()->get(\Doctrine\ORM\EntityManagerInterface::class);
        $start = time() - $daysAgo * 86400 + (++$this->slotCursor) * 60;

        $appointment = new Appointment(
            $em->getRepository(Doctor::class)->find($doctor->getId()),
            $em->getRepository(User::class)->find($patient->getId()),
            $start,
            $start + $actualMinutes * 60,
        );
        $appointment->assignTenantPair('clinic', $clinicId);
        $appointment->setServiceItem($em->getRepository(ServiceItem::class)->find($service->getId()));
        $appointment->setPatientName('بیمار گزارش');
        $appointment->setServiceDuration($plannedMinutes, 0);
        $appointment->transitionTo(Appointment::STATUS_CONFIRMED);
        $appointment->transitionTo(Appointment::STATUS_COMPLETED);

        $em->persist($appointment);
        $em->flush();

        return $appointment;
    }

    // ── دقت برنامه ──────────────────────────────────────────────────────────

    /** ⭐ سرویسی که ۶۰ دقیقه پیش‌بینی شده ولی ۹۰ دقیقه طول می‌کشد. */
    public function testAServiceThatRunsLongIsFlaggedHigh(): void
    {
        [$user, $section, $address, $doctor] = $this->clinic();
        $service = $this->service($section, 'لیزر فول‌بادی', 60);
        $patient = $this->createUser(['ROLE_USER']);

        for ($i = 1; $i <= 10; $i++) {
            $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 90, $i);
        }

        $body = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $user,
        );

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        $row = $body['data']['rows'][0];

        self::assertSame($service->getUuid(), $row['service_uuid']);
        self::assertSame(60, $row['planned_minutes']);
        self::assertSame(90, $row['actual_minutes']);
        self::assertSame(50, $row['deviation_percent']);
        self::assertSame('high', $row['severity']);
    }

    /** انحراف منفی هم غلط است: ظرفیتی که می‌شد فروخت، خالی مانده. */
    public function testAServiceThatRunsShortIsAlsoFlagged(): void
    {
        [$user, $section, $address, $doctor] = $this->clinic();
        $service = $this->service($section, 'مشاوره', 60);
        $patient = $this->createUser(['ROLE_USER']);

        for ($i = 1; $i <= 10; $i++) {
            $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 30, $i);
        }

        $rows = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $user,
        )['data']['rows'];

        self::assertSame(-50, $rows[0]['deviation_percent']);
        self::assertSame('high', $rows[0]['severity']);
    }

    /** زیر سه نمونه، میانگین معنا ندارد. */
    /**
     * ⭐ زیر آستانه **حذف نمی‌شود، بی‌شدت برمی‌گردد**.
     *
     * میانگین دو نمونه معنا ندارد و نباید کسی رویش تصمیم بگیرد؛ ولی حذف کاملش یعنی
     * کلینیک کوچک گزارشی خالی می‌بیند و فکر می‌کند همه‌چیز درست است.
     */
    public function testASmallSampleIsShownWithoutASeverity(): void
    {
        [$user, $section, $address, $doctor] = $this->clinic();
        $service = $this->service($section, 'خدمت کم‌تکرار', 60);
        $patient = $this->createUser(['ROLE_USER']);

        $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 120, 1);
        $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 120, 2);

        $rows = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $user,
        )['data']['rows'];

        $mine = array_values(array_filter(
            $rows,
            static fn (array $r): bool => $r['service_uuid'] === $service->getUuid(),
        ));

        self::assertCount(1, $mine);
        self::assertNull($mine[0]['severity'], 'با دو نمونه نباید شدتی ادعا شود');
        self::assertTrue($mine[0]['below_min_sample']);
        self::assertSame(2, $mine[0]['sample_size']);
    }

    public function testAnAccurateServiceHasNoSeverity(): void
    {
        [$user, $section, $address, $doctor] = $this->clinic();
        $service = $this->service($section, 'خدمت دقیق', 60);
        $patient = $this->createUser(['ROLE_USER']);

        for ($i = 1; $i <= 10; $i++) {
            $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 60, $i);
        }

        $rows = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $user,
        )['data']['rows'];

        $row = current(array_filter($rows, static fn (array $r): bool => $r['service_uuid'] === $service->getUuid()));

        self::assertSame(0, $row['deviation_percent']);
        self::assertSame('none', $row['severity']);
    }

    // ── بهره‌وری منابع ──────────────────────────────────────────────────────

    /** منبعی بدون تقویم «۰٪ بهره‌وری» ندارد — بهره‌وری‌اش تعریف‌نشده است. */
    /**
     * ⭐ چهار آستانه، هر کدام روی مرز خودش.
     *
     * آستانه‌ای که یک درجه اشتباه بیفتد، یا همه‌چیز را قرمز می‌کند (و کسی دیگر نگاه
     * نمی‌کند) یا هیچ‌چیز را (و گزارش بی‌فایده است).
     *
     * @param int $planned مدت برنامه
     * @param int $actual  مدت واقعی
     */
    #[\PHPUnit\Framework\Attributes\DataProvider('severityCases')]
    public function testEachSeverityThresholdIsHitExactly(int $planned, int $actual, string $expected): void
    {
        [$user, $section, $address, $doctor] = $this->clinic();
        $service = $this->service($section, 'خدمت آستانه', $planned);
        $patient = $this->createUser(['ROLE_USER']);

        for ($i = 1; $i <= 10; $i++) {
            $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), $planned, $actual, $i);
        }

        $body = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $user,
        );

        $row = $body['data']['rows'][0];

        self::assertSame($expected, $row['severity'], sprintf(
            'انحراف %d%%',
            $row['deviation_percent'],
        ));
    }

    /** مرزها: ۳۰ · ۱۵ · ۵ درصد، روی قدر مطلق. */
    public static function severityCases(): array
    {
        return [
            'دقیقاً روی مرز high'   => [100, 130, 'high'],
            'یک قدم زیر high'       => [100, 129, 'medium'],
            'دقیقاً روی مرز medium' => [100, 115, 'medium'],
            'یک قدم زیر medium'     => [100, 114, 'low'],
            'دقیقاً روی مرز low'    => [100, 105, 'low'],
            'یک قدم زیر low'        => [100, 104, 'none'],
            'کوتاه‌تر هم شمرده می‌شود' => [100, 70, 'high'],
        ];
    }

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

        $type = $this->authJson('POST', '/api/v1/resource-types', $user, [
            'address_uuid' => $address->getUuid(),
            'code'         => 'device',
            'name'         => 'دستگاه',
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($type, JSON_UNESCAPED_UNICODE));

        $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid' => $address->getUuid(),
            'type_uuid'    => $type['data']['uuid'],
            'name'         => 'لیزر ۱',
        ]);
        self::assertSame(201, $this->responseCode());

        $body = $this->authJson(
            'GET',
            sprintf(
                '/api/v1/reports/resource-utilization?branch_uuid=%s&from=%d&to=%d',
                $address->getUuid(),
                time() - 7 * 86400,
                time(),
            ),
            $user,
        );

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        $row = $body['data']['rows'][0];

        self::assertSame('لیزر ۱', $row['resource_name']);
        self::assertSame(0, $row['available_minutes']);
        self::assertNull($row['utilization'], 'تقسیم بر صفر معنای متفاوتی دارد');
        self::assertNull($row['active_ratio']);
        self::assertFalse($row['wasted_capacity']);
    }

    /**
     * ⭐ سنجه‌های واقعی: اشغال شامل انتظار است، «کار مفید» نه.
     *
     * فاصلهٔ این دو همان چیزی است که تعریف غلط بخش‌ها را لو می‌دهد؛ اگر هر دو یکی
     * برگردند، گزارش بی‌فایده است و کسی متوجه نمی‌شود.
     */
    public function testOccupiedIncludesTheWaitingSegmentButActiveDoesNot(): void
    {
        [$user, , $address] = $this->clinic();

        $type = $this->authJson('POST', '/api/v1/resource-types', $user, [
            'address_uuid' => $address->getUuid(),
            'code'         => 'room',
            'name'         => 'اتاق',
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($type, JSON_UNESCAPED_UNICODE));

        $created = $this->authJson('POST', '/api/v1/resource', $user, [
            'address_uuid' => $address->getUuid(),
            'type_uuid'    => $type['data']['uuid'],
            'name'         => 'اتاق ۱',
        ]);
        self::assertSame(201, $this->responseCode());

        $resource = $this->em->getRepository(\App\Resource\Entity\ClinicResource::class)
            ->findOneBy(['uuid' => $created['data']['uuid']]);

        $from  = time() - 2 * 86400;
        $start = $from + 3600;

        $appointment = $this->bookedAppointment($address, $start, 60);

        // یک ساعت اشغال؛ ولی بیمار فقط ۲۰ دقیقهٔ اولش حاضر است.
        $this->occupy($resource, $appointment, $start, $start + 3600);
        $this->segment($appointment, 1, 'ویزیت', $start, $start + 1200, true);
        $this->segment($appointment, 2, 'انتظار', $start + 1200, $start + 3600, false);

        $body = $this->authJson(
            'GET',
            sprintf(
                '/api/v1/reports/resource-utilization?branch_uuid=%s&from=%d&to=%d',
                $address->getUuid(),
                $from,
                time(),
            ),
            $user,
        );

        self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));

        $row = $body['data']['rows'][0];

        self::assertSame(60, $row['occupied_minutes'], 'انتظار هم اشغال است');
        self::assertSame(20, $row['active_minutes'], 'ولی کار مفید نیست');
    }

    private function bookedAppointment(\App\Doctor\Entity\DoctorAddress $address, int $start, int $minutes): \App\Appointment\Entity\Appointment
    {
        $doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
        $doctor     = new \App\Doctor\Entity\Doctor($doctorUser, 'دکتر گزارش');
        $this->em->persist($doctor);

        $appointment = new \App\Appointment\Entity\Appointment(
            $doctor,
            $this->createUser(['ROLE_USER']),
            $start,
            $start + $minutes * 60,
        );
        $appointment->assignTenantPair('clinic', (int) $address->getClinicId());
        $appointment->setAddressId($address->getId());
        $appointment->setPatientName('بیمار گزارش');
        $appointment->transitionTo(\App\Appointment\Entity\Appointment::STATUS_CONFIRMED);

        $this->em->persist($appointment);
        $this->em->flush();

        return $appointment;
    }

    private function occupy(
        \App\Resource\Entity\ClinicResource $resource,
        \App\Appointment\Entity\Appointment $appointment,
        int $from,
        int $to,
    ): void {
        $row = new \App\Appointment\Availability\Entity\ResourceOccupancy(
            $resource,
            $from,
            $to,
            \App\Appointment\Availability\Entity\ResourceOccupancy::STATUS_BOOKED,
        );
        $row->setAppointmentId($appointment->getId());

        $this->em->persist($row);
        $this->em->flush();
    }

    private function segment(
        \App\Appointment\Entity\Appointment $appointment,
        int $sequence,
        string $name,
        int $from,
        int $to,
        bool $present,
    ): void {
        $this->em->persist(new \App\Appointment\Booking\Entity\AppointmentSegment(
            $appointment,
            $sequence,
            $name,
            $from,
            $to,
            $present,
        ));
        $this->em->flush();
    }

    /**
     * ⭐ اشغال و کار مفید هرکدام **یک** کوئری‌اند، مستقل از تعداد منبع.
     *
     * پیمایش per منبع روی کلینیکی با ۴۰ منبع یعنی ۸۰ کوئری برای یک گزارش. تعداد
     * دقیقش مهم نیست؛ چیزی که این تست نگه می‌دارد این است که با سه برابر شدن منابع،
     * تعداد کوئری‌ها سه برابر **نشود**.
     */
    public function testQueryCountDoesNotGrowWithTheNumberOfResources(): void
    {
        [$user, , $address] = $this->clinic();

        $type = $this->authJson('POST', '/api/v1/resource-types', $user, [
            'address_uuid' => $address->getUuid(),
            'code'         => 'room',
            'name'         => 'اتاق',
        ]);
        self::assertSame(201, $this->responseCode(), json_encode($type, JSON_UNESCAPED_UNICODE));

        $reporter = static::getContainer()->get(\App\Report\Service\ResourceUtilizationReporter::class);

        $count = function (int $resources) use ($user, $address, $type, $reporter): int {
            for ($i = 0; $i < $resources; $i++) {
                $this->authJson('POST', '/api/v1/resource', $user, [
                    'address_uuid' => $address->getUuid(),
                    'type_uuid'    => $type['data']['uuid'],
                    'name'         => sprintf('اتاق %d', $i + 1),
                ]);
                self::assertSame(201, $this->responseCode());
            }

            $all = $this->em->getRepository(\App\Resource\Entity\ClinicResource::class)
                ->findBy(['address' => $address]);

            $connection = $this->em->getConnection();
            $before     = $this->queryCount($connection);

            $reporter->report($all, $address, time() - 7 * 86400, time());

            return $this->queryCount($connection) - $before;
        };

        $withOne   = $count(1);
        $withMany  = $count(5);

        self::assertLessThan(
            $withOne * 3,
            $withMany,
            sprintf('یک منبع %d کوئری، شش منبع %d کوئری — رشد خطی است', $withOne, $withMany),
        );
    }

    /** شمار کوئری از خودِ سرور — `SHOW SESSION STATUS` روی همان اتصال. */
    private function queryCount(\Doctrine\DBAL\Connection $connection): int
    {
        return (int) ($connection->fetchAssociative("SHOW SESSION STATUS LIKE 'Questions'")['Value'] ?? 0);
    }

    // ── محدودیت بازه و دسترسی ───────────────────────────────────────────────

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

        $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 200 * 86400, time()),
            $user,
        );

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

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

        $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time(), time() - 86400),
            $user,
        );

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

    public function testAnotherClinicSeesItsOwnNumbersOnly(): void
    {
        [$owner, $section, $address, $doctor] = $this->clinic();
        [$other] = $this->clinic();

        $service = $this->service($section, 'لیزر', 60);
        $patient = $this->createUser(['ROLE_USER']);

        for ($i = 1; $i <= 10; $i++) {
            $this->completed($doctor, $patient, $service, (int) $address->getClinicId(), 60, 90, $i);
        }

        $rows = $this->authJson(
            'GET',
            sprintf('/api/v1/reports/plan-accuracy?from=%d&to=%d', time() - 10 * 86400, time()),
            $other,
        )['data']['rows'];

        self::assertSame([], array_values(array_filter(
            $rows,
            static fn (array $r): bool => $r['service_uuid'] === $service->getUuid(),
        )));
    }
}
