<?php

use App\Models\User;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Models\Booking;
use Modules\Payment\Contracts\PaymentGatewayInterface;
use Modules\Payment\Data\PaymentRequestData;
use Modules\Payment\Data\PaymentResultData;
use Modules\Payment\Data\RefundResultData;
use Modules\Payment\Enums\PaymentMethod;
use Modules\Payment\Enums\PaymentStatus;
use Modules\Payment\Factories\PaymentGatewayFactory;
use Modules\Payment\Models\Payment;
use Spatie\Permission\Models\Permission;

/**
 * A fake PaymentGatewayInterface swapped in via the factory — never call the
 * real KBZ gateway in tests (T5.8).
 */
class FakeInitiatePaymentGateway implements PaymentGatewayInterface
{
    public static ?PaymentRequestData $lastRequest = null;

    public static int $initiateCalls = 0;

    public static int $verifyCalls = 0;

    public static PaymentStatus $verifyStatus = PaymentStatus::Pending;

    public function initiate(PaymentRequestData $data): PaymentResultData
    {
        self::$lastRequest = $data;
        self::$initiateCalls++;

        return new PaymentResultData(
            status: PaymentStatus::Pending,
            gatewayTransactionId: $data->merchantOrderId,
            gatewayPayload: ['prepay_id' => 'PREPAY123'],
        );
    }

    public function verify(string $gatewayTransactionId): PaymentResultData
    {
        self::$verifyCalls++;

        return new PaymentResultData(
            status: self::$verifyStatus,
            gatewayTransactionId: $gatewayTransactionId,
            gatewayPayload: ['trade_status' => self::$verifyStatus->value],
        );
    }

    public function refund(string $gatewayTransactionId, string $amount, string $reason): RefundResultData
    {
        throw new RuntimeException('not needed for this test');
    }

    public function handleWebhook(array $payload): PaymentResultData
    {
        throw new RuntimeException('not needed for this test');
    }
}

beforeEach(function () {
    Permission::findOrCreate('manage_bookings', 'web');

    app(PaymentGatewayFactory::class)->register(PaymentMethod::KbzMiniApp, FakeInitiatePaymentGateway::class);

    FakeInitiatePaymentGateway::$lastRequest = null;
    FakeInitiatePaymentGateway::$initiateCalls = 0;
    FakeInitiatePaymentGateway::$verifyCalls = 0;
    FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Pending;

    $this->owner = User::factory()->create();
    $this->token = $this->owner->createToken('test-token')->plainTextToken;
});

test('the owner can initiate payment for their own pending_payment booking', function () {
    $booking = Booking::factory()->create([
        'user_id' => $this->owner->id,
        'status' => BookingStatus::PendingPayment,
        'price' => 15000,
    ]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated()
        ->assertJsonPath('data.status', PaymentStatus::Pending->value)
        ->assertJsonPath('data.gateway_payload.prepay_id', 'PREPAY123');

    expect(Payment::where('booking_id', $booking->id)->count())->toBe(1);

    $payment = Payment::where('booking_id', $booking->id)->first();
    expect($payment->status)->toBe(PaymentStatus::Pending)
        ->and($payment->gateway)->toBe(PaymentMethod::KbzMiniApp)
        ->and((float) $payment->amount)->toBe(15000.0)
        ->and($payment->gateway_transaction_id)->toBe("{$booking->booking_ref}-1");
});

test('a repeat call while the previous attempt is still pending reuses it instead of precreating again', function () {
    $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
    FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Pending;

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated();

    $firstPaymentId = Payment::where('booking_id', $booking->id)->sole()->id;

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated()
        ->assertJsonPath('data.id', $firstPaymentId);

    expect(Payment::where('booking_id', $booking->id)->count())->toBe(1)
        ->and(FakeInitiatePaymentGateway::$initiateCalls)->toBe(1)
        ->and(FakeInitiatePaymentGateway::$verifyCalls)->toBe(1);
});

test('a repeat call reused attempt found completed on re-verify is returned without precreating again', function () {
    $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated();

    FakeInitiatePaymentGateway::$verifyStatus = PaymentStatus::Completed;

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated()
        ->assertJsonPath('data.status', PaymentStatus::Completed->value);

    expect(Payment::where('booking_id', $booking->id)->count())->toBe(1)
        ->and(FakeInitiatePaymentGateway::$initiateCalls)->toBe(1);
});

test('a retried payment attempt gets a unique merchant order id', function () {
    $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
    Payment::factory()->failed()->create(['booking_id' => $booking->id]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated();

    expect(FakeInitiatePaymentGateway::$lastRequest->merchantOrderId)->toBe("{$booking->booking_ref}-2");
});

test('initiating payment on a non-pending_payment booking surfaces as 422', function () {
    $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Confirmed]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertStatus(422);

    expect(Payment::where('booking_id', $booking->id)->count())->toBe(0);
});

test('a non-owner without manage_bookings cannot initiate payment for someone else\'s booking', function () {
    $booking = Booking::factory()->create([
        'user_id' => User::factory()->create()->id,
        'status' => BookingStatus::PendingPayment,
    ]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertForbidden();

    expect(Payment::where('booking_id', $booking->id)->count())->toBe(0);
});

test('staff with manage_bookings can initiate payment on behalf of a customer', function () {
    $staff = User::factory()->create()->givePermissionTo('manage_bookings');
    $staffToken = $staff->createToken('staff-token')->plainTextToken;

    $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);

    $this->withHeader('Authorization', "Bearer {$staffToken}")
        ->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")
        ->assertCreated();
});

test('unauthenticated requests are rejected', function () {
    $booking = Booking::factory()->create(['status' => BookingStatus::PendingPayment]);

    $this->postJson("/api/v1/payments/{$booking->booking_ref}/initiate")->assertUnauthorized();
});

test('404s for a booking that does not exist', function () {
    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson('/api/v1/payments/EVB-DOES-NOT-EXIST/initiate')
        ->assertNotFound();
});

test('round trip: initiating payment on the primary leg charges the combined total of both legs', function () {
    $outbound = Booking::factory()->create([
        'user_id' => $this->owner->id,
        'status' => BookingStatus::PendingPayment,
        'price' => 9000,
    ]);
    $return = Booking::factory()->create([
        'status' => BookingStatus::PendingPayment,
        'price' => 11000,
        'is_return_leg' => true,
        'linked_booking_id' => $outbound->id,
    ]);
    $outbound->update(['linked_booking_id' => $return->id]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$outbound->booking_ref}/initiate")
        ->assertCreated();

    $payment = Payment::where('booking_id', $outbound->id)->sole();

    expect((float) $payment->amount)->toBe(20000.0)
        ->and(Payment::where('booking_id', $return->id)->count())->toBe(0);
});

test('round trip: initiating payment on the return leg directly surfaces as 422', function () {
    $outbound = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
    $return = Booking::factory()->create([
        'user_id' => $this->owner->id,
        'status' => BookingStatus::PendingPayment,
        'is_return_leg' => true,
        'linked_booking_id' => $outbound->id,
    ]);
    $outbound->update(['linked_booking_id' => $return->id]);

    $this->withHeader('Authorization', "Bearer {$this->token}")
        ->postJson("/api/v1/payments/{$return->booking_ref}/initiate")
        ->assertStatus(422);

    expect(Payment::where('booking_id', $return->id)->count())->toBe(0);
});
