<?php

use Illuminate\Support\Facades\Event;
use Modules\Booking\Actions\CreateBookingAction;
use Modules\Booking\Data\CreateBookingData;
use Modules\Booking\Data\VehicleSelectionData;
use Modules\Booking\Enums\BookingChannel;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Events\BookingCreated;
use Modules\Booking\Exceptions\InvalidReturnRouteException;
use Modules\Booking\Exceptions\InvalidVehicleSelectionException;
use Modules\Booking\Models\Booking;
use Modules\Catalog\Models\DepartureTimeSlot;
use Modules\Routing\Exceptions\RoutePricingNotFoundException;
use Modules\Routing\Models\EvRoute;
use Modules\Routing\Models\RoutePricing;
use Modules\Shared\Enums\VehicleOption;

/**
 * @param  array<int, array{0: VehicleOption, 1: string}>  $pricedOptions
 */
function makeBookableRoute(array $pricedOptions): array
{
    $route = EvRoute::factory()->create();
    $timeSlot = DepartureTimeSlot::factory()->create();

    foreach ($pricedOptions as [$vehicleOption, $price]) {
        RoutePricing::factory()->create([
            'ev_route_id' => $route->id,
            'vehicle_option' => $vehicleOption,
            'price' => $price,
        ]);
    }

    return [$route, $timeSlot];
}

function bookingData(EvRoute $route, DepartureTimeSlot $timeSlot, array $selections, array $roundTrip = []): CreateBookingData
{
    return new CreateBookingData(
        evRouteId: $route->id,
        departureTimeSlotId: $timeSlot->id,
        travelDate: now()->addDay()->toDateString(),
        selections: $selections,
        passengerName: 'Jane Doe',
        passengerPhone: '+959123456789',
        pickupAddress: '123 Pickup St',
        dropoffAddress: '456 Dropoff Ave',
        createdByChannel: BookingChannel::MiniApp,
        openid: 'mini-app-openid-123',
        returnEvRouteId: $roundTrip['route']->id ?? null,
        returnDepartureTimeSlotId: $roundTrip['timeSlot']->id ?? null,
        returnTravelDate: $roundTrip['travelDate'] ?? (isset($roundTrip['route']) ? now()->addDays(3)->toDateString() : null),
        returnSelections: $roundTrip['selections'] ?? null,
    );
}

/**
 * Same company as $outbound, from/to swapped — the true reverse route.
 *
 * @param  array<int, array{0: VehicleOption, 1: string}>  $pricedOptions
 */
function makeReverseRoute(EvRoute $outbound, array $pricedOptions): array
{
    $route = EvRoute::factory()->create([
        'ev_company_id' => $outbound->ev_company_id,
        'from_destination_id' => $outbound->to_destination_id,
        'to_destination_id' => $outbound->from_destination_id,
    ]);
    $timeSlot = DepartureTimeSlot::factory()->create();

    foreach ($pricedOptions as [$vehicleOption, $price]) {
        RoutePricing::factory()->create([
            'ev_route_id' => $route->id,
            'vehicle_option' => $vehicleOption,
            'price' => $price,
        ]);
    }

    return [$route, $timeSlot];
}

test('it persists a pending_payment booking with the price snapshotted from PricingService', function () {
    config(['booking.back_seat_enabled' => true]);

    [$route, $timeSlot] = makeBookableRoute([[VehicleOption::BackSeat, '15000.00']]);

    $booking = app(CreateBookingAction::class)->handle(
        bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::BackSeat)])
    );

    expect($booking->exists)->toBeTrue()
        ->and($booking->booking_ref)->toBe('EVB-AAAAA1')
        ->and($booking->status)->toBe(BookingStatus::PendingPayment)
        ->and($booking->price)->toEqual('15000.00')
        ->and($booking->ev_route_id)->toBe($route->id)
        ->and($booking->departure_time_slot_id)->toBe($timeSlot->id)
        ->and($booking->openid)->toBe('mini-app-openid-123')
        ->and($booking->vehicleOptions)->toHaveCount(1)
        ->and($booking->vehicleOptions->first()->vehicle_option)->toBe(VehicleOption::BackSeat)
        ->and($booking->vehicleOptions->first()->unit_price)->toEqual('15000.00');
});

test('it books front seat and back seat together and sums the price across both lines', function () {
    config(['booking.back_seat_enabled' => true]);

    [$route, $timeSlot] = makeBookableRoute([
        [VehicleOption::FrontSeat, '12000.00'],
        [VehicleOption::BackSeat, '9000.00'],
    ]);

    $booking = app(CreateBookingAction::class)->handle(bookingData($route, $timeSlot, [
        new VehicleSelectionData(VehicleOption::FrontSeat, 1),
        new VehicleSelectionData(VehicleOption::BackSeat, 2),
    ]));

    expect($booking->price)->toEqual('30000.00') // 12000 + (9000 * 2)
        ->and($booking->vehicleOptions)->toHaveCount(2);

    $frontSeatLine = $booking->vehicleOptions->firstWhere('vehicle_option', VehicleOption::FrontSeat);
    $backSeatLine = $booking->vehicleOptions->firstWhere('vehicle_option', VehicleOption::BackSeat);

    expect($frontSeatLine->passenger_count)->toBe(1)
        ->and($frontSeatLine->line_total)->toEqual('12000.00')
        ->and($backSeatLine->passenger_count)->toBe(2)
        ->and($backSeatLine->line_total)->toEqual('18000.00');
});

test('it dispatches BookingCreated', function () {
    Event::fake([BookingCreated::class]);
    config(['booking.whole_vehicle_enabled' => true]);

    [$route, $timeSlot] = makeBookableRoute([[VehicleOption::WholeVehicle, '30000.00']]);

    $booking = app(CreateBookingAction::class)->handle(
        bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::WholeVehicle)])
    );

    Event::assertDispatched(BookingCreated::class, fn (BookingCreated $event) => $event->booking->is($booking));
});

test('it rejects a disabled vehicle option before touching the database', function () {
    config(['booking.whole_vehicle_enabled' => false]);

    [$route, $timeSlot] = makeBookableRoute([[VehicleOption::WholeVehicle, '30000.00']]);

    expect(fn () => app(CreateBookingAction::class)->handle(
        bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::WholeVehicle)])
    ))->toThrow(InvalidVehicleSelectionException::class);

    expect(Booking::count())->toBe(0);
});

test('it rejects mixing whole vehicle with another option before touching the database', function () {
    config([
        'booking.back_seat_enabled' => true,
        'booking.whole_vehicle_enabled' => true,
    ]);

    [$route, $timeSlot] = makeBookableRoute([
        [VehicleOption::WholeVehicle, '30000.00'],
        [VehicleOption::BackSeat, '9000.00'],
    ]);

    expect(fn () => app(CreateBookingAction::class)->handle(bookingData($route, $timeSlot, [
        new VehicleSelectionData(VehicleOption::WholeVehicle),
        new VehicleSelectionData(VehicleOption::BackSeat),
    ])))->toThrow(InvalidVehicleSelectionException::class);

    expect(Booking::count())->toBe(0);
});

test('each booking created gets a unique, sequential booking_ref', function () {
    config(['booking.back_seat_enabled' => true]);

    [$route, $timeSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);

    $first = app(CreateBookingAction::class)->handle(bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::BackSeat)]));
    $second = app(CreateBookingAction::class)->handle(bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::BackSeat)]));

    expect($first->booking_ref)->toBe('EVB-AAAAA1')
        ->and($second->booking_ref)->toBe('EVB-AAAAA2');
});

test('a plain one-way booking has no linked leg', function () {
    config(['booking.back_seat_enabled' => true]);

    [$route, $timeSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);

    $booking = app(CreateBookingAction::class)->handle(
        bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::BackSeat)])
    );

    expect($booking->linked_booking_id)->toBeNull()
        ->and($booking->is_round_trip)->toBeFalse()
        ->and($booking->is_return_leg)->toBeFalse()
        ->and(Booking::count())->toBe(1);
});

test('a round trip creates two bookings linked bidirectionally, each priced independently', function () {
    config(['booking.back_seat_enabled' => true]);

    [$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
    [$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::BackSeat, '11000.00']]);

    $outbound = app(CreateBookingAction::class)->handle(bookingData(
        $outboundRoute,
        $outboundSlot,
        [new VehicleSelectionData(VehicleOption::BackSeat)],
        roundTrip: [
            'route' => $returnRoute,
            'timeSlot' => $returnSlot,
            'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
        ],
    ));

    expect(Booking::count())->toBe(2)
        ->and($outbound->is_return_leg)->toBeFalse()
        ->and($outbound->is_round_trip)->toBeTrue()
        ->and($outbound->price)->toEqual('9000.00');

    $return = $outbound->linkedBooking;

    expect($return)->not->toBeNull()
        ->and($return->is_return_leg)->toBeTrue()
        ->and($return->is_round_trip)->toBeTrue()
        ->and($return->linked_booking_id)->toBe($outbound->id)
        ->and($return->ev_route_id)->toBe($returnRoute->id)
        ->and($return->departure_time_slot_id)->toBe($returnSlot->id)
        ->and($return->price)->toEqual('11000.00');
});

test('a round trip dispatches BookingCreated for both legs', function () {
    Event::fake([BookingCreated::class]);
    config(['booking.back_seat_enabled' => true]);

    [$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
    [$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::BackSeat, '9000.00']]);

    $outbound = app(CreateBookingAction::class)->handle(bookingData(
        $outboundRoute,
        $outboundSlot,
        [new VehicleSelectionData(VehicleOption::BackSeat)],
        roundTrip: [
            'route' => $returnRoute,
            'timeSlot' => $returnSlot,
            'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
        ],
    ));

    Event::assertDispatched(BookingCreated::class, 2);
    Event::assertDispatched(BookingCreated::class, fn (BookingCreated $event) => $event->booking->is($outbound));
    Event::assertDispatched(BookingCreated::class, fn (BookingCreated $event) => $event->booking->is($outbound->linkedBooking));
});

test('it rejects a return route that is not the reverse of the outbound route', function () {
    config(['booking.back_seat_enabled' => true]);

    [$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
    // Unrelated route — not from/to swapped.
    [$unrelatedRoute, $unrelatedSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);

    expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
        $outboundRoute,
        $outboundSlot,
        [new VehicleSelectionData(VehicleOption::BackSeat)],
        roundTrip: [
            'route' => $unrelatedRoute,
            'timeSlot' => $unrelatedSlot,
            'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
        ],
    )))->toThrow(InvalidReturnRouteException::class);

    // The whole transaction rolls back — no orphan outbound-only booking.
    expect(Booking::count())->toBe(0);
});

test('return leg selections are validated independently of the outbound leg', function () {
    config(['booking.back_seat_enabled' => true]);

    [$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::FrontSeat, '12000.00']]);
    [$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::FrontSeat, '12000.00']]);

    expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
        $outboundRoute,
        $outboundSlot,
        [new VehicleSelectionData(VehicleOption::FrontSeat, 1)],
        roundTrip: [
            'route' => $returnRoute,
            'timeSlot' => $returnSlot,
            // Front seat max per booking is 1 — this should fail validation
            // for the return leg even though the outbound leg is valid.
            'selections' => [new VehicleSelectionData(VehicleOption::FrontSeat, 2)],
        ],
    )))->toThrow(InvalidVehicleSelectionException::class);

    expect(Booking::count())->toBe(0);
});

test('a failed return-leg price lookup rolls back the outbound leg too', function () {
    config(['booking.back_seat_enabled' => true]);

    [$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
    // Return route exists (true reverse) but has no pricing rows at all.
    $returnRoute = EvRoute::factory()->create([
        'ev_company_id' => $outboundRoute->ev_company_id,
        'from_destination_id' => $outboundRoute->to_destination_id,
        'to_destination_id' => $outboundRoute->from_destination_id,
    ]);
    $returnSlot = DepartureTimeSlot::factory()->create();

    expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
        $outboundRoute,
        $outboundSlot,
        [new VehicleSelectionData(VehicleOption::BackSeat)],
        roundTrip: [
            'route' => $returnRoute,
            'timeSlot' => $returnSlot,
            'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
        ],
    )))->toThrow(RoutePricingNotFoundException::class);

    expect(Booking::count())->toBe(0);
});
