<?php

namespace App\Tests\Appointment;

use App\Appointment\Entity\WeeklySchedule;
use App\Appointment\Service\ServiceBookingCalculator;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceSection;
use App\Doctor\Entity\Doctor;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Tests\ApiTestCase;

/**
 * `ServiceBookingCalculator` — تنها مرجعِ «مدتِ مجازِ یک ترکیب سرویس».
 *
 * منطق پیش‌تر داخل `AppointmentController::serviceSlots()` بود؛ این تست تضمین می‌کند
 * استخراجش رفتار را عوض نکرده و چهار مسیر خطا با همان کد/پیام/فیلد قبلی برمی‌گردند.
 */
class ServiceBookingCalculatorTest extends ApiTestCase
{
    private function calculator(): ServiceBookingCalculator
    {
        return static::getContainer()->get(ServiceBookingCalculator::class);
    }

    private function serviceModeDoctor(int $buffer = 0): Doctor
    {
        $owner  = $this->createUser(['ROLE_DOCTOR']);
        $doctor = new Doctor($owner, 'دکتر محاسبهٔ سرویس');
        $this->em->persist($doctor);

        $schedule = $this->newWeeklySchedule($doctor, [
            '0' => ['sessions' => [[
                'active' => true, 'start_time' => '09:00', 'end_time' => '17:00',
                'duration_per_patient' => 20, 'location_id' => 1,
            ]]],
        ]);
        $schedule->setMeta(['booking_mode' => WeeklySchedule::MODE_SERVICE, 'buffer_minutes' => $buffer]);
        $this->em->persist($schedule);
        $this->em->flush();

        return $doctor;
    }

    private function slotModeDoctor(): Doctor
    {
        $owner  = $this->createUser(['ROLE_DOCTOR']);
        $doctor = new Doctor($owner, 'دکتر اسلاتی');
        $this->em->persist($doctor);

        $schedule = $this->newWeeklySchedule($doctor, ['0' => ['sessions' => []]]);
        $schedule->setMeta(['booking_mode' => WeeklySchedule::MODE_SLOT]);
        $this->em->persist($schedule);
        $this->em->flush();

        return $doctor;
    }

    private function service(Doctor $doctor, string $name, ?int $minutes, bool $bookable = true): ServiceItem
    {
        $section = new ServiceSection('doctor', $doctor->getId(), 'بخش ' . $name);
        $this->em->persist($section);
        $item = new ServiceItem($section, $name, 0);
        $item->setDurationMinutes($minutes)->setBookable($bookable);
        $this->em->persist($item);
        $this->em->flush();

        return $item;
    }

    // ── ✅ مسیر موفق ─────────────────────────────────────────────────────────

    public function testSumsDurationsAndCarriesBuffer(): void
    {
        $doctor = $this->serviceModeDoctor(buffer: 10);
        $face   = $this->service($doctor, 'لیزر صورت', 20);
        $bikini = $this->service($doctor, 'لیزر بیکینی', 15);

        $result = $this->calculator()->calculate($doctor, null, [$face->getUuid(), $bikini->getUuid()]);

        self::assertSame(35, $result->totalMinutes);
        self::assertSame(10, $result->bufferMinutes, 'بافر از meta برنامه می‌آید');
        self::assertSame([], $result->warnings);
        self::assertCount(2, $result->serviceItems);
    }

    public function testEndForExcludesBuffer(): void
    {
        $doctor = $this->serviceModeDoctor(buffer: 10);
        $item   = $this->service($doctor, 'مشاوره', 30);

        $result = $this->calculator()->calculate($doctor, null, [$item->getUuid()]);
        $start  = 1_800_000_000;

        self::assertSame($start + 30 * 60, $result->endFor($start), 'بافر جزو مدت نوبت نیست');
    }

    public function testSecretaryOverrideWinsWithoutTouchingTheService(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'تزریق', 20);

        $result = $this->calculator()->calculate(
            $doctor, null, [$item->getUuid()], [$item->getUuid() => 45],
        );

        self::assertSame(45, $result->totalMinutes);

        $this->em->refresh($item);
        self::assertSame(20, $item->getDurationMinutes(), 'مدت پیش‌فرض سرویس نباید عوض شود');
    }

    public function testIsServiceModeReadsBookingModeOnly(): void
    {
        self::assertTrue($this->calculator()->isServiceMode($this->serviceModeDoctor(), null));
        self::assertFalse($this->calculator()->isServiceMode($this->slotModeDoctor(), null));
    }

    // ── ❌ مسیرهای خطا — همان کد/پیام/فیلد قبلی کنترلر ────────────────────────

    public function testServiceWithoutDurationIsRejected(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'بدون مدت', null);

        try {
            $this->calculator()->calculate($doctor, null, [$item->getUuid()]);
            self::fail('سرویس بدون مدت باید رد شود');
        } catch (AppException $e) {
            self::assertSame(ErrorCodes::ERR_VALIDATION_001, $e->getErrorCode());
            self::assertSame('مدت سرویس تعریف نشده است', $e->getMessage());
            self::assertSame('service_item_uuids', $e->getField());
            self::assertSame(422, $e->getHttpStatus());
        }
    }

    public function testInactiveServiceIsRejectedByDefault(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'غیرفعال', 30, bookable: false);

        try {
            $this->calculator()->calculate($doctor, null, [$item->getUuid()]);
            self::fail('سرویس غیرفعال باید رد شود');
        } catch (AppException $e) {
            self::assertSame('این سرویس برای نوبت‌دهی فعال نیست', $e->getMessage());
            self::assertSame(422, $e->getHttpStatus());
        }
    }

    public function testForeignTenantServiceLeaksNothing(): void
    {
        $mine    = $this->serviceModeDoctor();
        $other   = $this->serviceModeDoctor();
        $foreign = $this->service($other, 'سرویس محیط دیگر', 30);

        try {
            $this->calculator()->calculate($mine, null, [$foreign->getUuid()]);
            self::fail('سرویس محیط دیگر باید رد شود');
        } catch (AppException $e) {
            // پیام نباید وجود، فعال‌بودن یا مدت سرویس را لو بدهد.
            self::assertSame('سرویس انتخاب‌شده به این محل نوبت‌دهی تعلق ندارد', $e->getMessage());
            self::assertStringNotContainsString('سرویس محیط دیگر', $e->getMessage());
        }
    }

    public function testUnknownUuidIsIndistinguishableFromForeignService(): void
    {
        $doctor = $this->serviceModeDoctor();

        try {
            $this->calculator()->calculate($doctor, null, ['00000000-0000-4000-8000-000000000000']);
            self::fail('uuid ناموجود باید رد شود');
        } catch (AppException $e) {
            self::assertSame('سرویس انتخاب‌شده به این محل نوبت‌دهی تعلق ندارد', $e->getMessage());
        }
    }

    // ── ⚠️ مرزی ──────────────────────────────────────────────────────────────

    public function testAllowInactiveTurnsRejectionIntoWarning(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'لیزر بازنشسته', 30, bookable: false);

        $result = $this->calculator()->calculate($doctor, null, [$item->getUuid()], allowInactive: true);

        self::assertSame(30, $result->totalMinutes, 'نوبت موجود باید قابل جابه‌جایی بماند');
        self::assertCount(1, $result->warnings);
        self::assertStringContainsString('لیزر بازنشسته', $result->warnings[0]);
    }

    public function testAllowInactiveStillRejectsMissingDuration(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'غیرفعال و بی‌مدت', null, bookable: false);

        $this->expectException(AppException::class);
        $this->expectExceptionMessage('مدت سرویس تعریف نشده است');
        $this->calculator()->calculate($doctor, null, [$item->getUuid()], allowInactive: true);
    }

    public function testEmptySelectionYieldsZeroMinutes(): void
    {
        $doctor = $this->serviceModeDoctor(buffer: 5);

        $result = $this->calculator()->calculate($doctor, null, []);

        // صفر یعنی «هیچ سرویسی انتخاب نشده»؛ رد کردنِ فهرست خالی کارِ کنترلر است، نه
        // این کلاس — تا مسیر PATCH بتواند نوبتِ بدون سرویس را بدون خطا رد کند.
        self::assertSame(0, $result->totalMinutes);
        self::assertSame(5, $result->bufferMinutes);
    }

    public function testNonPositiveOverrideFallsBackToServiceDuration(): void
    {
        $doctor = $this->serviceModeDoctor();
        $item   = $this->service($doctor, 'مشاوره', 25);

        foreach ([0, -10] as $bad) {
            $result = $this->calculator()->calculate(
                $doctor, null, [$item->getUuid()], [$item->getUuid() => $bad],
            );
            self::assertSame(25, $result->totalMinutes, 'override نامعتبر نادیده گرفته می‌شود');
        }
    }

    public function testDoctorWithoutScheduleFallsBackToDefaultMeta(): void
    {
        $owner  = $this->createUser(['ROLE_DOCTOR']);
        $doctor = new Doctor($owner, 'دکتر بی‌برنامه');
        $this->em->persist($doctor);
        $this->em->flush();

        self::assertFalse($this->calculator()->isServiceMode($doctor, null), 'پیش‌فرض slot است');
        self::assertSame(0, $this->calculator()->bufferMinutes($doctor, null));
    }
}
