<?php

namespace Tests\Feature;

use App\Models\Booking;
use App\Models\Professional;
use App\Models\User;
use App\Repositories\Eloquent\BookingRepository;
use App\Repositories\Eloquent\ProfessionalRepository;
use App\Repositories\Eloquent\ProfessionalScheduleRepository;
use App\Services\BookingNotificationService;
use App\Services\BookingService;
use App\Services\ProfessionalService;
use App\Services\PaymentService;
use Carbon\Carbon;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Mockery;
use Tests\TestCase;

class ScheduledBookingLifecycleTest extends TestCase
{
    private BookingRepository $bookings;
    private ProfessionalRepository $professionals;
    private BookingNotificationService $notifications;

    protected function setUp(): void
    {
        parent::setUp();
        // A fresh in-memory database only; never use the application's configured database.
        config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:', 'desxpert.booking_timezone' => 'Asia/Manila']);
        DB::purge('sqlite');
        Carbon::setTestNow(Carbon::parse('2026-09-18 02:00:00', 'UTC'));
        Schema::create('users', function (Blueprint $table) {
            $table->id(); $table->string('uuid'); $table->timestamps();
        });
        Schema::create('profiles', function (Blueprint $table) {
            $table->id(); $table->unsignedInteger('user_id'); $table->string('full_name')->nullable();
        });
        Schema::create('professionals', function (Blueprint $table) {
            $table->id(); $table->string('professional_uuid'); $table->integer('user_id'); $table->integer('profession_id'); $table->timestamps();
        });
        Schema::create('professional_schedules', function (Blueprint $table) {
            $table->id(); $table->integer('professional_id'); $table->string('day_of_week'); $table->string('time_from'); $table->string('time_to'); $table->string('rate');
        });
        Schema::create('bookings', function (Blueprint $table) {
            $table->id(); $table->string('uuid'); $table->integer('client_id'); $table->integer('professional_id'); $table->integer('profession_id');
            $table->string('type'); $table->string('date'); $table->string('time'); $table->string('concern')->nullable(); $table->string('amount')->default('500');
            $table->string('status')->default('pending'); $table->string('payment_status')->default('pending');
            $table->string('room_id')->nullable(); $table->timestamp('consultation_started_at')->nullable(); $table->timestamp('consultation_closed_at')->nullable(); $table->timestamps();
        });
        $migration = require base_path('database/migrations/2026_09_18_000001_add_payment_deadline_to_bookings.php');
        $migration->up();
        User::forceCreate(['id' => 1, 'uuid' => 'professional-user']);
        User::forceCreate(['id' => 2, 'uuid' => 'client-one']);
        User::forceCreate(['id' => 3, 'uuid' => 'client-two']);
        Professional::forceCreate(['id' => 1, 'professional_uuid' => 'professional', 'user_id' => 1, 'profession_id' => 1]);
        DB::table('professional_schedules')->insert(['professional_id' => 1, 'day_of_week' => 'Saturday', 'time_from' => '9:00 am', 'time_to' => '12:00 pm', 'rate' => '500']);
        $this->bookings = new BookingRepository(new Booking);
        $this->professionals = new ProfessionalRepository(new Professional);
        $this->notifications = Mockery::mock(BookingNotificationService::class);
        $dependencies = [];
        $availability = $this->service(ProfessionalService::class, $dependencies);
        $dependencies['scheduleOverrideRepository']->shouldReceive('getOverrideByDate')->andReturnNull();
        $this->app->instance(ProfessionalService::class, $availability);
    }

    protected function tearDown(): void
    {
        Carbon::setTestNow();
        DB::disconnect('sqlite');
        parent::tearDown();
    }

    private function service(string $class, array &$dependencies): object
    {
        $arguments = [];
        foreach ((new \ReflectionClass($class))->getConstructor()->getParameters() as $parameter) {
            $name = $parameter->getName();
            $value = match ($name) {
                'bookingRepository' => $this->bookings,
                'professionalRepository' => $this->professionals,
                'professionalScheduleRepository' => new ProfessionalScheduleRepository(new \App\Models\ProfessionalSchedule),
                'notificationService', 'bookingNotificationService' => $this->notifications,
                default => Mockery::mock($parameter->getType()->getName()),
            };
            $dependencies[$name] = $value;
            $arguments[] = $value;
        }
        return new $class(...$arguments);
    }

    private function request(string $date = '2026-09-19', string $time = '10:00', string $client = 'client-one'): array
    {
        return ['professional_id' => 'professional', 'client_id' => $client, 'type' => 'appointment', 'date' => $date, 'time' => $time, 'concern' => 'Consultation'];
    }

    private function booking(array $values = []): Booking
    {
        return Booking::create(array_merge(['uuid' => 'booking', 'client_id' => 2, 'professional_id' => 1, 'profession_id' => 1, 'type' => 'appointment', 'date' => '2026-09-19', 'time' => '10:00', 'status' => 'pending', 'payment_status' => 'pending'], $values));
    }

    public function test_today_is_rejected_and_tomorrow_reserves_the_slot_for_one_client(): void
    {
        $dependencies = [];
        $service = $this->service(BookingService::class, $dependencies);
        $this->assertSame(422, $service->createBooking($this->request('2026-09-18'))['status']);
        $dependencies['userRepository']->shouldReceive('getByColumn')->once()->with('uuid', 'client-one')->andReturn(User::find(2));
        $this->notifications->shouldReceive('bookingCreated')->once();
        $this->assertTrue($service->createBooking($this->request() + ['status' => 'accepted', 'payment_status' => 'paid', 'payment_due_at' => now()->addYear()])['success']);
        $this->assertSame('pending', Booking::first()->payment_status);
        $this->assertNull(Booking::first()->payment_due_at);
        $this->assertSame('pending', Booking::first()->status);
        $secondResult = $service->createBooking($this->request(client: 'client-two'));
        $this->assertFalse($secondResult['success']);
        $this->assertSame(409, $secondResult['status']);
        $this->assertSame('This time slot is already taken. Please choose another time.', $secondResult['message']);
        $this->assertSame(1, Booking::count());
        $this->assertSame(2, Booking::first()->client_id);
        $availability = app(ProfessionalService::class)->getScheduleOfTheDay('professional', '2026-09-19');
        $this->assertSame(0, collect($availability['data']['time_slots'])->firstWhere('time', '10:00')['is_available']);
    }

    public function test_schedule_remains_available_before_payment_deadline_migration(): void
    {
        $this->booking();
        Schema::table('bookings', fn (Blueprint $table) => $table->dropIndex(['payment_due_at']));
        Schema::table('bookings', fn (Blueprint $table) => $table->dropColumn('payment_due_at'));

        $result = app(ProfessionalService::class)->getScheduleOfTheDay('professional', '2026-09-19');
        $slots = collect($result['data']['time_slots'])->keyBy('time');

        $this->assertTrue($result['success']);
        $this->assertSame(0, $slots['10:00']['is_available']);
        $this->assertSame(1, $slots['10:30']['is_available']);
    }

    public function test_acceptance_sets_deadline_once_and_exact_cutoff_releases_slot(): void
    {
        $booking = $this->booking();
        $dependencies = [];
        $service = $this->service(BookingService::class, $dependencies);
        $this->notifications->shouldReceive('bookingStatusChanged')->once()->withArgs(fn ($booking) => $booking->payment_due_at->equalTo(now()->addMinutes(15)));
        $this->assertTrue($service->updateBookingStatus(['booking_id' => 'booking', 'status' => 'accepted'], 'professional-user')['success']);
        $deadline = $booking->fresh()->payment_due_at;
        Carbon::setTestNow(now()->addMinutes(5));
        $this->assertTrue($service->updateBookingStatus(['booking_id' => 'booking', 'status' => 'accepted'], 'professional-user')['success']);
        $this->assertTrue($booking->fresh()->payment_due_at->equalTo($deadline));
        $this->assertCount(1, $this->bookings->getBookingByDate(1, '2026-09-19'));
        Carbon::setTestNow($deadline);
        $this->assertTrue($booking->fresh()->unpaidDeadlinePassed());
        $this->assertCount(0, $this->bookings->getBookingByDate(1, '2026-09-19'));
        $this->assertTrue($this->bookings->expireUnpaidBooking($booking->id));
        $this->assertSame('expired', $booking->fresh()->status);
        $this->assertFalse($service->updateBookingStatus(['booking_id' => 'booking', 'status' => 'accepted'], 'professional-user')['success']);
    }

    public function test_expiry_job_expires_future_booking_but_never_paid_booking(): void
    {
        $expired = $this->booking(['status' => 'accepted', 'payment_due_at' => now()]);
        $paid = $this->booking(['uuid' => 'paid', 'time' => '11:00', 'status' => 'accepted', 'payment_status' => 'paid', 'payment_due_at' => now()->subMinute()]);
        $this->notifications->shouldReceive('bookingExpiredUnpaid')->once()->withArgs(fn ($booking) => $booking->id === $expired->id);
        $this->app->instance(\App\Repositories\Interfaces\BookingRepositoryInterface::class, $this->bookings);
        $this->app->instance(BookingNotificationService::class, $this->notifications);
        $this->artisan('bookings:expire-unpaid')->assertSuccessful();
        $this->assertSame('expired', $expired->fresh()->status);
        $this->assertSame('accepted', $paid->fresh()->status);
        $this->assertFalse($paid->fresh()->unpaidDeadlinePassed());
        $this->assertCount(1, $this->bookings->getBookingByDate(1, '2026-09-19'));
    }

    public function test_client_gets_full_fifteen_minutes_when_accepted_shortly_before_start(): void
    {
        $booking = $this->booking();
        Carbon::setTestNow(Carbon::parse('2026-09-19 09:55:00', 'Asia/Manila'));
        $dependencies = [];
        $service = $this->service(BookingService::class, $dependencies);
        $this->notifications->shouldReceive('bookingStatusChanged')->once();
        $this->assertTrue($service->updateBookingStatus(['booking_id' => 'booking', 'status' => 'accepted'], 'professional-user')['success']);
        $deadline = $booking->fresh()->payment_due_at;
        Carbon::setTestNow(now()->addMinutes(6));
        $this->assertFalse($booking->fresh()->unpaidDeadlinePassed());
        Carbon::setTestNow($deadline);
        $this->assertTrue($booking->fresh()->unpaidDeadlinePassed());
    }

    public function test_legacy_duplicate_request_cannot_be_accepted_over_reserved_interval(): void
    {
        $this->booking(['uuid' => 'already-accepted', 'status' => 'accepted', 'payment_due_at' => now()->addMinutes(15)]);
        $this->booking(['time' => '10:15']);
        $dependencies = [];
        $service = $this->service(BookingService::class, $dependencies);
        $result = $service->updateBookingStatus(['booking_id' => 'booking', 'status' => 'accepted'], 'professional-user');
        $this->assertSame(409, $result['status']);
        $this->assertSame('pending', Booking::where('uuid', 'booking')->first()->status);
    }

    public function test_checkout_after_deadline_is_rejected_and_releases_reservation(): void
    {
        $booking = $this->booking(['status' => 'accepted', 'payment_due_at' => now()]);
        $dependencies = [];
        $service = $this->service(PaymentService::class, $dependencies);
        $this->notifications->shouldReceive('bookingExpiredUnpaid')->once();
        $result = $service->checkout(\Illuminate\Http\Request::create('/checkout', 'POST', ['booking_id' => 'booking']), []);
        $this->assertFalse($result['success']);
        $this->assertSame(422, $result['status']);
        $this->assertSame('expired', $booking->fresh()->status);
        $this->assertCount(0, $this->bookings->getBookingByDate(1, '2026-09-19'));
    }

    public function test_on_time_payment_keeps_accepted_slot_after_deadline_and_old_failure_callback(): void
    {
        $booking = $this->booking(['status' => 'accepted', 'payment_due_at' => now()->addMinute()]);
        $dependencies = [];
        $service = $this->service(PaymentService::class, $dependencies);
        $payment = (new \App\Models\Payment)->forceFill(['id' => 1, 'booking_id' => $booking->id]);
        $dependencies['paymentRepository']->shouldReceive('getByColumn')->twice()->andReturn($payment);
        $dependencies['paymentRepository']->shouldReceive('update')->once()->andReturn($payment);
        $dependencies['messagingService']->shouldReceive('sendToUser')->once()->andReturnTrue();
        $this->notifications->shouldReceive('paymentCompleted')->once();
        $this->assertTrue($service->processWebhook(['id' => 'checkout'], 'PAID'));
        Carbon::setTestNow(now()->addMinutes(2));
        $this->assertTrue($service->processWebhook(['id' => 'checkout'], 'FAILED'));
        $this->assertSame('accepted', $booking->fresh()->status);
        $this->assertSame('paid', $booking->fresh()->payment_status);
        $this->assertNotNull($booking->fresh()->room_id);
        $this->assertCount(1, $this->bookings->getBookingByDate(1, '2026-09-19'));
        $this->assertFalse($this->bookings->expireUnpaidBooking($booking->id));
    }

    public function test_late_payment_cannot_restore_released_slot_or_generate_room(): void
    {
        $booking = $this->booking(['status' => 'accepted', 'payment_due_at' => now()]);
        $this->booking(['uuid' => 'replacement', 'client_id' => 3]);
        $dependencies = [];
        $service = $this->service(PaymentService::class, $dependencies);
        $payment = (new \App\Models\Payment)->forceFill(['id' => 1, 'booking_id' => $booking->id]);
        $dependencies['paymentRepository']->shouldReceive('getByColumn')->twice()->andReturn($payment);
        $dependencies['paymentRepository']->shouldReceive('update')->once()->andReturn($payment);
        $this->notifications->shouldReceive('latePaymentReceived')->once();
        $this->notifications->shouldReceive('bookingExpiredUnpaid')->once();
        $this->notifications->shouldNotReceive('paymentCompleted');
        $this->assertTrue($service->processWebhook(['id' => 'checkout'], 'PAID'));
        $this->assertTrue($service->processWebhook(['id' => 'checkout'], 'PAID'));
        $this->assertSame('expired', $booking->fresh()->status);
        $this->assertSame('paid', $booking->fresh()->payment_status);
        $this->assertNull($booking->fresh()->room_id);
        $this->assertCount(1, $this->bookings->getBookingByDate(1, '2026-09-19'));
    }
}
