﻿<?php

use App\Models\Chama;
use App\Models\ChamaFund;
use App\Models\Contribution;
use App\Models\ContributionPlan;
use App\Models\MpesaSetting;
use App\Models\Transaction;
use App\Models\User;
use App\Services\MpesaService;
use App\Services\MpesaPaymentReconciliationService;
use Illuminate\Database\QueryException;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;

function createMpesaFixture(): array
{
    $creator = User::factory()->create();

    $member = User::factory()->create([
        'phone_number' => '0712345678',
    ]);

    $chama = Chama::create([
        'name' => 'M-PESA Test Chama',
        'slug' => 'mpesa-test-chama-' . uniqid(),
        'description' => 'M-PESA integration test chama.',
        'status' => 'active',
        'currency' => 'KES',
        'created_by' => $creator->id,
    ]);

    $chama->members()->create([
        'user_id' => $creator->id,
        'role' => 'chairperson',
        'status' => 'active',
        'joined_at' => now(),
    ]);

    $chama->members()->create([
        'user_id' => $member->id,
        'role' => 'member',
        'status' => 'active',
        'joined_at' => now(),
    ]);

    $fund = ChamaFund::create([
        'chama_id' => $chama->id,
        'name' => 'Main Savings Fund',
        'slug' => 'main-savings-fund-' . uniqid(),
        'description' => 'M-PESA integration test fund.',
        'opening_balance' => 0,
        'currency' => 'KES',
        'status' => 'active',
    ]);

    $plan = ContributionPlan::create([
        'chama_id' => $chama->id,
        'chama_fund_id' => $fund->id,
        'name' => 'Monthly Savings',
        'amount' => 100,
        'frequency' => 'monthly',
        'start_date' => now()->toDateString(),
        'end_date' => null,
        'status' => 'active',
    ]);

    $contribution = Contribution::create([
        'chama_id' => $chama->id,
        'chama_fund_id' => $fund->id,
        'contribution_plan_id' => $plan->id,
        'user_id' => $member->id,
        'amount' => 100,
        'due_date' => now()->toDateString(),
        'status' => 'pending',
    ]);

    return compact(
        'creator',
        'member',
        'chama',
        'fund',
        'plan',
        'contribution'
    );
}

function createPendingMpesaTransaction(
    array $fixture,
    string $gatewayReference = 'ws_CO_test_status'
): Transaction {
    return Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-STATUS-TEST',
        'external_reference' => null,
        'gateway_reference' => $gatewayReference,
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);
}

test('database rejects duplicate M-PESA receipt references', function () {
    $fixture = createMpesaFixture();

    Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-UNIQUE-RECEIPT-1',
        'external_reference' => 'DATABASE-UNIQUE-RECEIPT-001',
        'gateway_reference' => 'ws_CO_database_unique_1',
        'description' => 'M-PESA contribution payment',
        'processed_at' => now(),
    ]);

    expect(fn () => Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-UNIQUE-RECEIPT-2',
        'external_reference' => 'DATABASE-UNIQUE-RECEIPT-001',
        'gateway_reference' => 'ws_CO_database_unique_2',
        'description' => 'M-PESA contribution payment',
        'processed_at' => now(),
    ]))->toThrow(QueryException::class);
});

test('database allows the same external reference for a manual transaction', function () {
    $fixture = createMpesaFixture();

    Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-UNIQUE-RECEIPT-3',
        'external_reference' => 'SHARED-EXTERNAL-REFERENCE-001',
        'gateway_reference' => 'ws_CO_database_unique_3',
        'description' => 'M-PESA contribution payment',
        'processed_at' => now(),
    ]);

    $manualTransaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'manual',
        'reference' => 'MANUAL-SHARED-REFERENCE-1',
        'external_reference' => 'SHARED-EXTERNAL-REFERENCE-001',
        'gateway_reference' => null,
        'description' => 'Manual contribution payment',
        'processed_at' => now(),
    ]);

    expect($manualTransaction->external_reference)
        ->toBe('SHARED-EXTERNAL-REFERENCE-001')
        ->and($manualTransaction->payment_method)
        ->toBe('manual');
});
function fakeMpesaStatusQuery(array $response, int $status = 200): void
{
    Http::fake([
        'https://sandbox.safaricom.co.ke/oauth/v1/generate*' =>
            Http::response([
                'access_token' => 'test-access-token',
            ], 200),

        'https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query' =>
            Http::response($response, $status),
    ]);
}

beforeEach(function () {
    config([
        'mpesa.environment' => 'sandbox',
        'mpesa.consumer_key' => 'test-key',
        'mpesa.consumer_secret' => 'test-secret',
        'mpesa.shortcode' => '174379',
        'mpesa.passkey' => 'test-passkey',
        'mpesa.transaction_type' => 'CustomerPayBillOnline',
        'mpesa.callback_url' => 'https://example.test/api/payments/stk/callback',
        'mpesa.timeout' => 30,
        'mpesa.base_urls.sandbox' => 'https://sandbox.safaricom.co.ke',
    ]);

    Cache::forget('mpesa.access_token.sandbox');
});

test('M-PESA service obtains a token and sends an STK push', function () {
    Http::fake([
        'https://sandbox.safaricom.co.ke/oauth/v1/generate*' =>
            Http::response([
                'access_token' => 'test-access-token',
            ], 200),

        'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest' =>
            Http::response([
                'MerchantRequestID' => 'merchant-request-123',
                'CheckoutRequestID' => 'ws_CO_test_123',
                'ResponseCode' => '0',
                'ResponseDescription' => 'Success. Request accepted for processing',
                'CustomerMessage' => 'Success. Request accepted for processing',
            ], 200),
    ]);

    $response = app(MpesaService::class)->stkPush(
        '0712345678',
        100,
        'CH1'
    );

    expect($response['CheckoutRequestID'])
        ->toBe('ws_CO_test_123');

    Http::assertSent(function (Request $request) {
        return str_contains(
            $request->url(),
            '/mpesa/stkpush/v1/processrequest'
        )
            && $request['PartyA'] === '254708374149'
            && $request['PhoneNumber'] === '254708374149'
            && $request['Amount'] === 100;
    });
});

test('M-PESA callback marks a successful contribution paid', function () {
    $fixture = createMpesaFixture();

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-1-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_test_123',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $response = $this->postJson('/api/payments/stk/callback', [
        'Body' => [
            'stkCallback' => [
                'MerchantRequestID' => 'merchant-request-123',
                'CheckoutRequestID' => 'ws_CO_test_123',
                'ResultCode' => 0,
                'ResultDesc' => 'The service request is processed successfully.',
                'CallbackMetadata' => [
                    'Item' => [
                        [
                            'Name' => 'Amount',
                            'Value' => 100,
                        ],
                        [
                            'Name' => 'MpesaReceiptNumber',
                            'Value' => 'TESTRECEIPT123',
                        ],
                        [
                            'Name' => 'PhoneNumber',
                            'Value' => 254712345678,
                        ],
                    ],
                ],
            ],
        ],
    ]);

    $response
        ->assertOk()
        ->assertJson([
            'ResultCode' => 0,
            'ResultDesc' => 'Accepted',
        ]);

    expect($fixture['contribution']->fresh()->status)
        ->toBe('paid');

    expect($fixture['contribution']->fresh()->payment_reference)
        ->toBe('TESTRECEIPT123');

    expect($transaction->fresh()->status)
        ->toBe('completed');

    expect($transaction->fresh()->external_reference)
        ->toBe('TESTRECEIPT123');
});

test('M-PESA failure callback leaves the contribution pending', function () {
    $fixture = createMpesaFixture();

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-2-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_test_failed',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $this->postJson('/api/payments/stk/callback', [
        'Body' => [
            'stkCallback' => [
                'MerchantRequestID' => 'merchant-request-failed',
                'CheckoutRequestID' => 'ws_CO_test_failed',
                'ResultCode' => 1032,
                'ResultDesc' => 'Request cancelled by user',
            ],
        ],
    ])->assertOk();

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending');

    expect($transaction->fresh()->status)
        ->toBe('failed');
});

test('starting an M-PESA contribution creates a pending transaction and stores the checkout request id', function () {
    $fixture = createMpesaFixture();

    MpesaSetting::create([
        'environment' => 'sandbox',
        'consumer_key' => 'test-key',
        'consumer_secret' => 'test-secret',
        'shortcode' => '174379',
        'passkey' => 'test-passkey',
        'transaction_type' => 'CustomerPayBillOnline',
        'callback_url' => 'https://example.test/api/payments/stk/callback',
        'sandbox_phone_number' => '254708374149',
        'enabled' => true,
    ]);

    Http::fake([
        'https://sandbox.safaricom.co.ke/oauth/v1/generate*' =>
            Http::response([
                'access_token' => 'test-access-token',
            ], 200),

        'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest' =>
            Http::response([
                'MerchantRequestID' => 'merchant-request-456',
                'CheckoutRequestID' => 'ws_CO_test_456',
                'ResponseCode' => '0',
                'ResponseDescription' => 'Success. Request accepted for processing',
                'CustomerMessage' => 'Success. Request accepted for processing',
            ], 200),
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution'],
    ])
        ->set('payment_method', 'mpesa')
        ->call('pay')
        ->assertHasNoErrors();

    $transaction = Transaction::query()
        ->where('contribution_id', $fixture['contribution']->id)
        ->where('payment_method', 'mpesa')
        ->first();

    expect($transaction)->not->toBeNull()
        ->and($transaction->status)->toBe('pending')
        ->and($transaction->gateway_reference)->toBe('ws_CO_test_456')
        ->and($fixture['contribution']->fresh()->status)->toBe('pending');
});

test('M-PESA service returns a graceful failure when an STK query transaction does not exist', function () {
    Http::fake([
        'https://sandbox.safaricom.co.ke/oauth/v1/generate*' =>
            Http::response([
                'access_token' => 'test-access-token',
            ], 200),

        'https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query' =>
            Http::response([
                'requestId' => 'ws_CO_test_missing',
                'errorCode' => '500.001.1001',
                'errorMessage' => 'The transaction does not Exist',
            ], 500),
    ]);

    $response = app(MpesaService::class)->queryStkPush(
        'ws_CO_test_missing'
    );

    expect($response['success'])
        ->toBeFalse()
        ->and($response['http_status'])
        ->toBe(500)
        ->and($response['request_id'])
        ->toBe('ws_CO_test_missing')
        ->and($response['error_code'])
        ->toBe('500.001.1001')
        ->and($response['error_message'])
        ->toBe('The transaction does not Exist')
        ->and($response['checkout_request_id'])
        ->toBe('ws_CO_test_missing');

    Http::assertSent(function (Request $request) {
        return str_contains(
            $request->url(),
            '/mpesa/stkpushquery/v1/query'
        )
            && $request['BusinessShortCode'] === '174379'
            && $request['CheckoutRequestID'] === 'ws_CO_test_missing'
            && isset($request['Password'])
            && $request['Password'] !== ''
            && isset($request['Timestamp'])
            && $request['Timestamp'] !== '';
    });
});

test('M-PESA status refresh cannot overwrite an already paid contribution', function () {
    $fixture = createMpesaFixture();

    $fixture['contribution']->update([
        'status' => 'paid',
        'paid_at' => now(),
        'payment_reference' => 'MANUAL-TEST-123',
        'payment_method' => 'manual',
    ]);

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-PAID-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_test_paid',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    Http::fake([
        'https://sandbox.safaricom.co.ke/oauth/v1/generate*' =>
            Http::response([
                'access_token' => 'test-access-token',
            ], 200),

        'https://sandbox.safaricom.co.ke/mpesa/stkpushquery/v1/query' =>
            Http::response([
                'ResponseCode' => '0',
                'ResponseDescription' => 'The service request has been accepted successfully',
                'MerchantRequestID' => 'merchant-request-paid',
                'CheckoutRequestID' => 'ws_CO_test_paid',
                'ResultCode' => '0',
                'ResultDesc' => 'The service request is processed successfully.',
                'CallbackMetadata' => [
                    'Item' => [
                        [
                            'Name' => 'Amount',
                            'Value' => 100,
                        ],
                        [
                            'Name' => 'MpesaReceiptNumber',
                            'Value' => 'MPESARECEIPT999',
                        ],
                        [
                            'Name' => 'PhoneNumber',
                            'Value' => 254712345678,
                        ],
                    ],
                ],
            ], 200),
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasNoErrors();

    expect($fixture['contribution']->fresh()->status)
        ->toBe('paid')
        ->and($fixture['contribution']->fresh()->payment_method)
        ->toBe('manual')
        ->and($fixture['contribution']->fresh()->payment_reference)
        ->toBe('MANUAL-TEST-123')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA payment abandoned because the contribution was already paid.');
});

test('M-PESA STK push is blocked when M-PESA is disabled in backend settings', function () {
    MpesaSetting::create([
        'environment' => 'sandbox',
        'consumer_key' => 'test-key',
        'consumer_secret' => 'test-secret',
        'shortcode' => '174379',
        'passkey' => 'test-passkey',
        'transaction_type' => 'CustomerPayBillOnline',
        'callback_url' => 'https://example.test/api/payments/stk/callback',
        'sandbox_phone_number' => '254708374149',
        'enabled' => false,
    ]);

    Http::fake();

    expect(fn () => app(MpesaService::class)->stkPush(
        '0712345678',
        100,
        'CH_DISABLED'
    ))
        ->toThrow(
            RuntimeException::class,
            'M-PESA payments are currently disabled.'
        );

    Http::assertNothingSent();
});

test('M-PESA callback rejects a receipt already used by another transaction', function () {
    $fixture = createMpesaFixture();

    $otherContribution = Contribution::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'contribution_plan_id' => $fixture['plan']->id,
        'user_id' => $fixture['member']->id,
        'amount' => 100,
        'due_date' => now()->addDay()->toDateString(),
        'status' => 'pending',
    ]);

    $existingTransaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $otherContribution->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-EXISTING-TEST',
        'external_reference' => 'DUPLICATE-RECEIPT-001',
        'gateway_reference' => 'ws_CO_existing_receipt',
        'description' => 'M-PESA contribution payment',
        'processed_at' => now(),
    ]);

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-DUPLICATE-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_duplicate_receipt',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $response = $this->postJson('/api/payments/stk/callback', [
        'Body' => [
            'stkCallback' => [
                'MerchantRequestID' => 'merchant-request-duplicate',
                'CheckoutRequestID' => 'ws_CO_duplicate_receipt',
                'ResultCode' => 0,
                'ResultDesc' => 'The service request is processed successfully.',
                'CallbackMetadata' => [
                    'Item' => [
                        [
                            'Name' => 'Amount',
                            'Value' => 100,
                        ],
                        [
                            'Name' => 'MpesaReceiptNumber',
                            'Value' => 'DUPLICATE-RECEIPT-001',
                        ],
                        [
                            'Name' => 'PhoneNumber',
                            'Value' => 254712345678,
                        ],
                    ],
                ],
            ],
        ],
    ]);

    $response
        ->assertOk()
        ->assertJson([
            'ResultCode' => 0,
            'ResultDesc' => 'Accepted',
        ]);

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending');

    expect($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA receipt number has already been used by another transaction.');

    expect($existingTransaction->fresh()->status)
        ->toBe('completed');
});

test('M-PESA successful callback without an amount is rejected', function () {
    $fixture = createMpesaFixture();

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-NO-AMOUNT-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_no_amount',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $response = $this->postJson('/api/payments/stk/callback', [
        'Body' => [
            'stkCallback' => [
                'MerchantRequestID' => 'merchant-request-no-amount',
                'CheckoutRequestID' => 'ws_CO_no_amount',
                'ResultCode' => 0,
                'ResultDesc' => 'The service request is processed successfully.',
                'CallbackMetadata' => [
                    'Item' => [
                        [
                            'Name' => 'MpesaReceiptNumber',
                            'Value' => 'NO-AMOUNT-RECEIPT',
                        ],
                        [
                            'Name' => 'PhoneNumber',
                            'Value' => 254712345678,
                        ],
                    ],
                ],
            ],
        ],
    ]);

    $response
        ->assertOk()
        ->assertJson([
            'ResultCode' => 0,
            'ResultDesc' => 'Accepted',
        ]);

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending');

    expect($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA successful callback did not contain an amount.');
});

test('M-PESA callback rejects a transaction linked to a contribution in another fund', function () {
    $fixture = createMpesaFixture();

    $otherFund = ChamaFund::create([
        'chama_id' => $fixture['chama']->id,
        'name' => 'Other Savings Fund',
        'slug' => 'other-savings-fund-' . uniqid(),
        'description' => 'Integrity test fund.',
        'opening_balance' => 0,
        'currency' => 'KES',
        'status' => 'active',
    ]);

    $otherContribution = Contribution::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $otherFund->id,
        'contribution_plan_id' => $fixture['plan']->id,
        'user_id' => $fixture['member']->id,
        'amount' => 100,
        'due_date' => now()->addDay()->toDateString(),
        'status' => 'pending',
    ]);

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $otherContribution->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-FUND-MISMATCH-TEST',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_fund_mismatch',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $response = $this->postJson('/api/payments/stk/callback', [
        'Body' => [
            'stkCallback' => [
                'MerchantRequestID' => 'merchant-request-fund-mismatch',
                'CheckoutRequestID' => 'ws_CO_fund_mismatch',
                'ResultCode' => 0,
                'ResultDesc' => 'The service request is processed successfully.',
                'CallbackMetadata' => [
                    'Item' => [
                        [
                            'Name' => 'Amount',
                            'Value' => 100,
                        ],
                        [
                            'Name' => 'MpesaReceiptNumber',
                            'Value' => 'FUND-MISMATCH-001',
                        ],
                        [
                            'Name' => 'PhoneNumber',
                            'Value' => 254712345678,
                        ],
                    ],
                ],
            ],
        ],
    ]);

    $response
        ->assertOk()
        ->assertJson([
            'ResultCode' => 0,
            'ResultDesc' => 'Accepted',
        ]);

    expect($otherContribution->fresh()->status)
        ->toBe('pending');

    expect($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA transaction and contribution fund mismatch.');
});

/*
|--------------------------------------------------------------------------
| M-PESA status-query reconciliation tests
|--------------------------------------------------------------------------
*/

test('M-PESA status refresh marks a successful payment as paid', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_success'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-success',
        'CheckoutRequestID' => 'ws_CO_status_success',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-RECEIPT-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasNoErrors();

    expect($fixture['contribution']->fresh()->status)
        ->toBe('paid')
        ->and($fixture['contribution']->fresh()->payment_method)
        ->toBe('mpesa')
        ->and($fixture['contribution']->fresh()->payment_reference)
        ->toBe('STATUS-RECEIPT-001')
        ->and($transaction->fresh()->status)
        ->toBe('completed')
        ->and($transaction->fresh()->external_reference)
        ->toBe('STATUS-RECEIPT-001');
});

test('M-PESA status refresh rejects a payment with the wrong amount', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_wrong_amount'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-wrong-amount',
        'CheckoutRequestID' => 'ws_CO_status_wrong_amount',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 50,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-WRONG-AMOUNT-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA payment amount did not match the contribution amount.');
});

test('M-PESA status refresh rejects a successful response without an amount', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_no_amount'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-no-amount',
        'CheckoutRequestID' => 'ws_CO_status_no_amount',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-NO-AMOUNT-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA successful status query did not contain an amount.');
});

test('M-PESA status refresh rejects a successful response without a receipt', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_no_receipt'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-no-receipt',
        'CheckoutRequestID' => 'ws_CO_status_no_receipt',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA status query did not contain a receipt number.');
});

test('M-PESA status refresh rejects a receipt already used by another transaction', function () {
    $fixture = createMpesaFixture();

    $otherContribution = Contribution::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'contribution_plan_id' => $fixture['plan']->id,
        'user_id' => $fixture['member']->id,
        'amount' => 100,
        'due_date' => now()->addDay()->toDateString(),
        'status' => 'pending',
    ]);

    Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $otherContribution->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'completed',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-OTHER-RECEIPT',
        'external_reference' => 'STATUS-DUPLICATE-001',
        'gateway_reference' => 'ws_CO_other_receipt',
        'description' => 'M-PESA contribution payment',
        'processed_at' => now(),
    ]);

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_duplicate_receipt'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-duplicate-receipt',
        'CheckoutRequestID' => 'ws_CO_status_duplicate_receipt',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-DUPLICATE-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA receipt number has already been used by another transaction.');
});

test('M-PESA status refresh rejects a transaction linked to a contribution in another fund', function () {
    $fixture = createMpesaFixture();

    $otherFund = ChamaFund::create([
        'chama_id' => $fixture['chama']->id,
        'name' => 'Status Query Other Fund',
        'slug' => 'status-query-other-fund-' . uniqid(),
        'description' => 'Status query fund integrity test.',
        'opening_balance' => 0,
        'currency' => 'KES',
        'status' => 'active',
    ]);

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $otherFund->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-STATUS-FUND-MISMATCH',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_status_fund_mismatch',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-fund-mismatch',
        'CheckoutRequestID' => 'ws_CO_status_fund_mismatch',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-FUND-MISMATCH-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA transaction and contribution fund mismatch.');
});

test('M-PESA status refresh rejects a transaction linked to a contribution in another chama', function () {
    $fixture = createMpesaFixture();

    $transaction = Transaction::create([
        'chama_id' => $fixture['chama']->id,
        'chama_fund_id' => $fixture['fund']->id,
        'user_id' => $fixture['member']->id,
        'contribution_id' => $fixture['contribution']->id,
        'type' => 'contribution',
        'amount' => 100,
        'currency' => 'KES',
        'status' => 'pending',
        'payment_method' => 'mpesa',
        'reference' => 'MPESA-STATUS-CHAMA-MISMATCH',
        'external_reference' => null,
        'gateway_reference' => 'ws_CO_status_chama_mismatch',
        'description' => 'M-PESA STK payment',
        'processed_at' => null,
    ]);

    $otherChama = Chama::create([
        'name' => 'Other M-PESA Chama',
        'slug' => 'other-mpesa-chama-' . uniqid(),
        'description' => 'Chama integrity test.',
        'status' => 'active',
        'currency' => 'KES',
        'created_by' => $fixture['creator']->id,
    ]);

    $otherFund = ChamaFund::create([
        'chama_id' => $otherChama->id,
        'name' => 'Other Chama Fund',
        'slug' => 'other-chama-fund-' . uniqid(),
        'description' => 'Other chama fund.',
        'opening_balance' => 0,
        'currency' => 'KES',
        'status' => 'active',
    ]);

    $transaction->update([
        'chama_id' => $otherChama->id,
        'chama_fund_id' => $otherFund->id,
    ]);

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-chama-mismatch',
        'CheckoutRequestID' => 'ws_CO_status_chama_mismatch',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'STATUS-CHAMA-MISMATCH-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('failed')
        ->and($transaction->fresh()->description)
        ->toBe('M-PESA transaction and contribution chama mismatch.');
});

test('M-PESA status refresh keeps a non-final payment pending', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_status_pending'
    );

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-status-pending',
        'CheckoutRequestID' => 'ws_CO_status_pending',
        'ResultCode' => '1037',
        'ResultDesc' => 'DS timeout user cannot be reached',
    ]);

    $this->actingAs($fixture['member']);

    Livewire::test('contribution-pay', [
        'contribution' => $fixture['contribution']->fresh(),
    ])
        ->call('refreshMpesaStatus')
        ->assertHasErrors('payment_method');

    expect($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('pending');
});
test('M-PESA reconciliation completes a stale successful payment', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_reconcile_success'
    );

    $transaction->update([
        'created_at' => now()->subMinutes(45),
    ]);

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-reconcile-success',
        'CheckoutRequestID' => 'ws_CO_reconcile_success',
        'ResultCode' => '0',
        'ResultDesc' => 'The service request is processed successfully.',
        'CallbackMetadata' => [
            'Item' => [
                [
                    'Name' => 'Amount',
                    'Value' => 100,
                ],
                [
                    'Name' => 'MpesaReceiptNumber',
                    'Value' => 'RECONCILE-SUCCESS-001',
                ],
                [
                    'Name' => 'PhoneNumber',
                    'Value' => 254712345678,
                ],
            ],
        ],
    ]);

    $result = app(MpesaPaymentReconciliationService::class)
        ->reconcile($transaction);

    expect($result)
        ->toBe('completed')
        ->and($fixture['contribution']->fresh()->status)
        ->toBe('paid')
        ->and($fixture['contribution']->fresh()->payment_reference)
        ->toBe('RECONCILE-SUCCESS-001')
        ->and($transaction->fresh()->status)
        ->toBe('completed')
        ->and($transaction->fresh()->external_reference)
        ->toBe('RECONCILE-SUCCESS-001');
});

test('M-PESA reconciliation keeps a stale non-final payment pending', function () {
    $fixture = createMpesaFixture();

    $transaction = createPendingMpesaTransaction(
        $fixture,
        'ws_CO_reconcile_pending'
    );

    $transaction->update([
        'created_at' => now()->subMinutes(45),
    ]);

    fakeMpesaStatusQuery([
        'ResponseCode' => '0',
        'ResponseDescription' => 'The service request has been accepted successfully',
        'MerchantRequestID' => 'merchant-request-reconcile-pending',
        'CheckoutRequestID' => 'ws_CO_reconcile_pending',
        'ResultCode' => '1037',
        'ResultDesc' => 'DS timeout user cannot be reached',
    ]);

    $result = app(MpesaPaymentReconciliationService::class)
        ->reconcile($transaction);

    expect($result)
        ->toBe('pending')
        ->and($fixture['contribution']->fresh()->status)
        ->toBe('pending')
        ->and($transaction->fresh()->status)
        ->toBe('pending');
});

