<?php

namespace Tests\Feature;

use App\Enums\PaymentStatus;
use App\Models\Payment;
use App\Notifications\PaymentReceived;
use App\Services\Payments\PaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class PaymentTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_a_payment_is_created_for_an_application_fee(): void
    {
        $application = $this->startApplication($this->applicant());

        $payment = app(PaymentService::class)->forApplication($application);

        $this->assertSame(PaymentStatus::Pending, $payment->status);
        $this->assertSame('UGX', $payment->currency);
        $this->assertSame('application_fee', $payment->purpose);
        $this->assertStringStartsWith('PAY-', $payment->reference);
    }

    public function test_requesting_a_payment_twice_reuses_the_open_record(): void
    {
        $application = $this->startApplication($this->applicant());
        $service = app(PaymentService::class);

        $first = $service->forApplication($application);
        $second = $service->forApplication($application);

        $this->assertTrue($first->is($second));
        $this->assertSame(1, Payment::where('application_id', $application->id)->count());
    }

    public function test_settling_a_payment_notifies_the_applicant_and_writes_an_audit_record(): void
    {
        Notification::fake();

        $application = $this->startApplication($this->applicant());
        $service = app(PaymentService::class);
        $payment = $service->forApplication($application);

        $service->markPaid($payment, 'PROV-123');

        $payment->refresh();

        $this->assertSame(PaymentStatus::Paid, $payment->status);
        $this->assertNotNull($payment->paid_at);
        $this->assertSame('PROV-123', $payment->provider_reference);
        $this->assertTrue($application->fresh()->isPaid());

        $this->assertDatabaseHas('audit_logs', ['action' => 'payment.received']);

        Notification::assertSentTo($application->applicant->user, PaymentReceived::class);
    }

    public function test_settling_an_already_paid_payment_is_a_no_op(): void
    {
        Notification::fake();

        $application = $this->startApplication($this->applicant());
        $service = app(PaymentService::class);
        $payment = $service->forApplication($application);

        $service->markPaid($payment);
        $paidAt = $payment->fresh()->paid_at;

        $service->markPaid($payment->fresh());

        $this->assertEquals($paidAt, $payment->fresh()->paid_at);
        Notification::assertSentTimes(PaymentReceived::class, 1);
    }

    public function test_finance_verification_settles_a_manual_payment(): void
    {
        $application = $this->startApplication($this->applicant());
        $payment = app(PaymentService::class)->forApplication($application);
        $finance = $this->staff('finance-officer');

        app(PaymentService::class)->verifyManually($payment, $finance, 'Matched bank statement 12 Aug');

        $payment->refresh();

        $this->assertTrue($payment->isPaid());
        $this->assertSame($finance->id, $payment->verified_by);
        $this->assertNotNull($payment->verified_at);
        $this->assertDatabaseHas('audit_logs', ['action' => 'payment.verified']);
    }

    public function test_a_failed_payment_is_recorded_with_its_reason(): void
    {
        $application = $this->startApplication($this->applicant());
        $payment = app(PaymentService::class)->forApplication($application);

        app(PaymentService::class)->markFailed($payment, 'Insufficient funds');

        $payment->refresh();

        $this->assertSame(PaymentStatus::Failed, $payment->status);
        $this->assertSame('Insufficient funds', $payment->failure_reason);
        $this->assertNotNull($payment->failed_at);
        $this->assertFalse($application->fresh()->isPaid());
    }

    public function test_a_webhook_with_an_invalid_signature_is_rejected(): void
    {
        config(['kcu.payments.webhook_secret' => 'test-secret']);

        $this->postJson('/webhooks/payments/mobile_money', ['data' => ['tx_ref' => 'PAY-1']], [
            'X-Payment-Signature' => 'not-the-right-signature',
        ])->assertStatus(400);

        $this->assertDatabaseCount('payment_events', 0);
    }

    public function test_an_unknown_provider_is_rejected(): void
    {
        $this->postJson('/webhooks/payments/not-a-provider', [])->assertStatus(404);
    }

    public function test_a_valid_webhook_settles_the_payment(): void
    {
        Notification::fake();
        config(['kcu.payments.webhook_secret' => 'test-secret']);

        $application = $this->startApplication($this->applicant());
        $payment = app(PaymentService::class)->forApplication($application, 'mobile_money');

        $response = $this->sendWebhook($payment, (float) $payment->amount, 'evt-1');

        $response->assertOk();

        $this->assertTrue($payment->fresh()->isPaid());
        $this->assertDatabaseHas('payment_events', ['event_id' => 'evt-1', 'processed' => true]);
    }

    public function test_a_repeated_webhook_cannot_settle_a_payment_twice(): void
    {
        Notification::fake();
        config(['kcu.payments.webhook_secret' => 'test-secret']);

        $application = $this->startApplication($this->applicant());
        $payment = app(PaymentService::class)->forApplication($application, 'mobile_money');

        $this->sendWebhook($payment, (float) $payment->amount, 'evt-dup')->assertOk();
        $this->sendWebhook($payment, (float) $payment->amount, 'evt-dup')->assertOk();

        // The duplicate is ignored rather than reprocessed.
        $this->assertSame(1, \App\Models\PaymentEvent::where('event_id', 'evt-dup')->count());
        Notification::assertSentTimes(PaymentReceived::class, 1);
    }

    public function test_a_webhook_claiming_less_than_the_fee_does_not_settle_the_payment(): void
    {
        config(['kcu.payments.webhook_secret' => 'test-secret']);

        $application = $this->startApplication($this->applicant());
        $payment = app(PaymentService::class)->forApplication($application, 'mobile_money');

        $this->sendWebhook($payment, 1000.00, 'evt-short')->assertOk();

        $this->assertFalse($payment->fresh()->isPaid());
        $this->assertDatabaseHas('payment_events', ['event_id' => 'evt-short']);
    }

    public function test_an_applicant_cannot_view_another_applicants_payment(): void
    {
        $mine = $this->startApplication($this->applicant());
        $theirs = $this->startApplication($this->applicant());

        $theirPayment = app(PaymentService::class)->forApplication($theirs);

        $this->actingAs($mine->applicant->user)
            ->get(route('applicant.payments.show', $theirs))
            ->assertForbidden();

        $this->actingAs($mine->applicant->user)
            ->get(route('applicant.payments.callback', $theirPayment))
            ->assertForbidden();
    }

    /** Post a correctly signed webhook payload for the given payment. */
    protected function sendWebhook(Payment $payment, float $amount, string $eventId)
    {
        $payload = [
            'event' => 'charge.completed',
            'data' => [
                'id' => $eventId,
                'tx_ref' => $payment->reference,
                'status' => 'successful',
                'amount' => $amount,
            ],
        ];

        $body = json_encode($payload);

        return $this->call(
            'POST',
            '/webhooks/payments/mobile_money',
            [],
            [],
            [],
            [
                'CONTENT_TYPE' => 'application/json',
                'HTTP_ACCEPT' => 'application/json',
                'HTTP_X_PAYMENT_SIGNATURE' => hash_hmac('sha256', $body, 'test-secret'),
            ],
            $body,
        );
    }
}
