<?php

namespace Tests\Feature;

use App\Domain\Booking\BookingService;
use App\Domain\Booking\Exceptions\SlotUnavailableException;
use App\Domain\Booking\SlotEngine;
use App\Enums\AppointmentStatus;
use App\Models\Dentist;
use App\Models\Patient;
use App\Models\ScheduleException;
use App\Models\Tenant;
use App\Support\Tenancy;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookingEngineTest extends TestCase
{
    use RefreshDatabase;

    protected Tenant $tenant;
    protected Dentist $dentist;
    protected Patient $patient;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed();

        $this->tenant = Tenant::create([
            'name' => 'Smile', 'slug' => 'smile', 'timezone' => 'Asia/Baghdad',
        ]);
        app(Tenancy::class)->set($this->tenant);

        $this->dentist = Dentist::factory()->create();
        // Works every day 09:00–12:00 so tests are weekday-independent.
        foreach (range(0, 6) as $weekday) {
            $this->dentist->schedules()->create([
                'weekday' => $weekday, 'start_time' => '09:00', 'end_time' => '12:00',
            ]);
        }
        $this->patient = Patient::factory()->create();
    }

    protected function tomorrowAt(string $time): CarbonImmutable
    {
        return CarbonImmutable::tomorrow('Asia/Baghdad')->setTimeFromTimeString($time);
    }

    public function test_slot_engine_generates_schedule_slots(): void
    {
        $slots = app(SlotEngine::class)->availableSlots($this->dentist, $this->tomorrowAt('00:00'));

        // 09:00–12:00 with 30-min slots = 6 slots.
        $this->assertCount(6, $slots);
        $this->assertSame('09:00', $slots->first()['label']);
        $this->assertSame('11:30', $slots->last()['label']);
    }

    public function test_booked_slot_disappears_from_availability(): void
    {
        $start = $this->tomorrowAt('09:00')->utc();
        app(BookingService::class)->book($this->dentist, $this->patient, $start);

        $labels = app(SlotEngine::class)
            ->availableSlots($this->dentist, $this->tomorrowAt('00:00'))
            ->pluck('label');

        $this->assertFalse($labels->contains('09:00'));
        $this->assertCount(5, $labels);
    }

    public function test_double_booking_same_slot_is_rejected(): void
    {
        $start = $this->tomorrowAt('10:00')->utc();
        $service = app(BookingService::class);
        $service->book($this->dentist, $this->patient, $start);

        $this->expectException(SlotUnavailableException::class);
        $service->book($this->dentist, Patient::factory()->create(), $start);
    }

    public function test_unique_slot_key_blocks_writes_that_bypass_the_service(): void
    {
        $start = $this->tomorrowAt('10:00')->utc();
        app(BookingService::class)->book($this->dentist, $this->patient, $start);

        $this->expectException(\Illuminate\Database\QueryException::class);
        // Direct model write (simulating a code path that skipped the service)
        \App\Models\Appointment::create([
            'dentist_id' => $this->dentist->id,
            'patient_id' => Patient::factory()->create()->id,
            'starts_at' => $start,
            'ends_at' => $start->addMinutes(30),
            'status' => AppointmentStatus::Confirmed,
        ]);
    }

    public function test_cancelling_frees_the_slot(): void
    {
        $start = $this->tomorrowAt('11:00')->utc();
        $service = app(BookingService::class);
        $appointment = $service->book($this->dentist, $this->patient, $start);

        $service->changeStatus($appointment, AppointmentStatus::Cancelled, 'test');

        $rebooked = $service->book($this->dentist, Patient::factory()->create(), $start);
        $this->assertTrue($rebooked->exists);
    }

    public function test_whole_day_off_removes_all_slots(): void
    {
        ScheduleException::create([
            'dentist_id' => $this->dentist->id,
            'date' => $this->tomorrowAt('00:00')->toDateString(),
            'type' => 'off',
        ]);

        $this->assertCount(
            0,
            app(SlotEngine::class)->availableSlots($this->dentist, $this->tomorrowAt('00:00'))
        );
    }

    public function test_partial_off_and_extra_windows_are_applied(): void
    {
        $date = $this->tomorrowAt('00:00');

        ScheduleException::create([
            'dentist_id' => $this->dentist->id, 'date' => $date->toDateString(),
            'type' => 'off', 'start_time' => '09:00', 'end_time' => '10:00',
        ]);
        ScheduleException::create([
            'dentist_id' => $this->dentist->id, 'date' => $date->toDateString(),
            'type' => 'extra', 'start_time' => '14:00', 'end_time' => '15:00',
        ]);

        $labels = app(SlotEngine::class)->availableSlots($this->dentist, $date)->pluck('label');

        $this->assertFalse($labels->contains('09:00'));
        $this->assertFalse($labels->contains('09:30'));
        $this->assertTrue($labels->contains('10:00'));
        $this->assertTrue($labels->contains('14:00'));
        $this->assertTrue($labels->contains('14:30'));
    }

    public function test_completing_a_visit_updates_patient_stats(): void
    {
        $service = app(BookingService::class);
        $appointment = $service->book($this->dentist, $this->patient, $this->tomorrowAt('09:30')->utc());

        $service->changeStatus($appointment, AppointmentStatus::Confirmed);
        $service->changeStatus($appointment, AppointmentStatus::Completed);

        $this->assertSame(1, $this->patient->fresh()->total_visits);
        $this->assertNotNull($this->patient->fresh()->last_visit_at);
    }

    public function test_invalid_status_transition_is_rejected(): void
    {
        $service = app(BookingService::class);
        $appointment = $service->book($this->dentist, $this->patient, $this->tomorrowAt('09:00')->utc());
        $service->changeStatus($appointment, AppointmentStatus::Cancelled);

        $this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
        $service->changeStatus($appointment, AppointmentStatus::Completed);
    }
}
