<?php

namespace Tests\Feature\Billing;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use App\Domain\Refunds\Enums\RefundStatus;
use App\Domain\Refunds\Models\Refund;
use Database\Seeders\PermissionSeeder;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use RuntimeException;
use Tests\TestCase;

class RefundTest extends TestCase
{
    use RefreshDatabase;

    private Company $company;

    private Branch $branch;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed([PermissionSeeder::class, RoleSeeder::class]);

        $this->company = Company::factory()->create();
        $this->branch = Branch::factory()->for($this->company)->create();
    }

    private function user(string $role): User
    {
        return tap(User::factory()->create([
            'company_id' => $this->company->id,
            'branch_id' => $this->branch->id,
        ]))->assignRole($role);
    }

    private function applicant(): Applicant
    {
        return Applicant::factory()->for($this->company)->create(['branch_id' => $this->branch->id]);
    }

    /** Takes 20,000 in cash so there is something to give back. */
    private function collect(Applicant $applicant, int $amount = 20000): string
    {
        return $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => $amount,
                'currency' => 'BDT',
                'method' => 'cash',
            ])
            ->assertCreated()
            ->json('data.id');
    }

    public function test_an_agency_cannot_refund_money_it_never_took(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant, 5000);

        $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 9000,
                'currency' => 'BDT',
                'reason' => 'Visa refused.',
            ])
            ->assertStatus(422);
    }

    public function test_a_second_request_cannot_claim_money_the_first_already_claimed(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant, 10000);
        $cashier = $this->user('cashier');

        $this->actingAs($cashier)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 8000,
                'currency' => 'BDT',
                'reason' => 'Visa refused.',
            ])->assertCreated();

        $this->actingAs($cashier)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 5000,
                'currency' => 'BDT',
                'reason' => 'Second bite.',
            ])->assertStatus(422);

        $this->actingAs($cashier)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 2000,
                'currency' => 'BDT',
                'reason' => 'The rest.',
            ])->assertCreated();
    }

    public function test_a_refund_cannot_be_approved_by_whoever_requested_it(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);
        $officer = $this->user('accounts-officer');

        $refund = $this->actingAs($officer)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 5000,
                'currency' => 'BDT',
                'reason' => 'Medically unfit.',
                'reason_code' => 'medical_unfit',
            ])->assertCreated()->json('data.id');

        $this->actingAs($officer)
            ->postJson("/api/v1/refunds/{$refund}/approve")
            ->assertStatus(422);

        $this->actingAs($this->user('accounts-manager'))
            ->postJson("/api/v1/refunds/{$refund}/approve")
            ->assertOk()
            ->assertJsonPath('data.status', RefundStatus::Approved->value)
            ->assertJsonPath('data.payable_amount', 5000);
    }

    public function test_more_cannot_be_approved_than_was_requested(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);

        $refund = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 5000,
                'currency' => 'BDT',
                'reason' => 'Overpayment.',
            ])->assertCreated()->json('data.id');

        $this->actingAs($this->user('accounts-manager'))
            ->postJson("/api/v1/refunds/{$refund}/approve", ['approved_amount' => 9000])
            ->assertStatus(422);
    }

    public function test_a_deduction_reduces_what_is_paid_out(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);
        $manager = $this->user('accounts-manager');

        $refund = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 10000,
                'currency' => 'BDT',
                'reason' => 'Applicant withdrew.',
            ])->assertCreated()->json('data.id');

        $this->actingAs($manager)
            ->postJson("/api/v1/refunds/{$refund}/approve", [
                'approved_amount' => 10000,
                'deduction_amount' => 2500,
                'deduction_reason' => 'Medical already paid to the centre.',
            ])
            ->assertOk()
            ->assertJsonPath('data.payable_amount', 7500);

        $this->actingAs($manager)
            ->postJson("/api/v1/refunds/{$refund}/pay", [
                'payout_method' => 'bank_transfer',
                'beneficiary_name' => 'Rahim Uddin',
                'transaction_reference' => 'RF-1001',
            ])
            ->assertOk()
            ->assertJsonPath('data.status', RefundStatus::Paid->value)
            ->assertJsonPath('data.paid_amount', 7500);
    }

    public function test_a_payout_needs_a_beneficiary_and_a_method(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);
        $manager = $this->user('accounts-manager');

        $refund = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 5000,
                'currency' => 'BDT',
                'reason' => 'Duplicate payment.',
            ])->assertCreated()->json('data.id');

        $this->actingAs($manager)->postJson("/api/v1/refunds/{$refund}/approve")->assertOk();

        $this->actingAs($manager)
            ->postJson("/api/v1/refunds/{$refund}/pay", ['payout_method' => 'cash'])
            ->assertStatus(422);
    }

    public function test_a_payout_reference_cannot_be_reused(): void
    {
        $manager = $this->user('accounts-manager');
        $cashier = $this->user('cashier');

        $paid = [];

        foreach (range(1, 2) as $index) {
            $applicant = $this->applicant();
            $this->collect($applicant);

            $refund = $this->actingAs($cashier)
                ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                    'requested_amount' => 3000,
                    'currency' => 'BDT',
                    'reason' => "Case {$index}.",
                ])->assertCreated()->json('data.id');

            $this->actingAs($manager)->postJson("/api/v1/refunds/{$refund}/approve")->assertOk();
            $paid[] = $refund;
        }

        $payload = [
            'payout_method' => 'bank_transfer',
            'beneficiary_name' => 'Somebody',
            'transaction_reference' => 'RF-SAME',
        ];

        $this->actingAs($manager)->postJson("/api/v1/refunds/{$paid[0]}/pay", $payload)->assertOk();
        $this->actingAs($manager)->postJson("/api/v1/refunds/{$paid[1]}/pay", $payload)->assertStatus(422);
    }

    public function test_a_paid_refund_cannot_be_edited_or_deleted(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);
        $manager = $this->user('accounts-manager');

        $uuid = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 4000,
                'currency' => 'BDT',
                'reason' => 'Demand cancelled.',
            ])->assertCreated()->json('data.id');

        $this->actingAs($manager)->postJson("/api/v1/refunds/{$uuid}/approve")->assertOk();
        $this->actingAs($manager)->postJson("/api/v1/refunds/{$uuid}/pay", [
            'payout_method' => 'cash',
            'beneficiary_name' => 'Rahim Uddin',
        ])->assertOk();

        $refund = Refund::where('uuid', $uuid)->firstOrFail();

        $this->expectException(RuntimeException::class);

        $refund->paid_amount = 1;
        $refund->save();
    }

    public function test_a_paid_refund_cannot_be_paid_again(): void
    {
        $applicant = $this->applicant();
        $this->collect($applicant);
        $manager = $this->user('accounts-manager');

        $uuid = $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 4000,
                'currency' => 'BDT',
                'reason' => 'Demand cancelled.',
            ])->assertCreated()->json('data.id');

        $this->actingAs($manager)->postJson("/api/v1/refunds/{$uuid}/approve")->assertOk();

        $payload = ['payout_method' => 'cash', 'beneficiary_name' => 'Rahim Uddin'];

        $this->actingAs($manager)->postJson("/api/v1/refunds/{$uuid}/pay", $payload)->assertOk();
        $this->actingAs($manager)->postJson("/api/v1/refunds/{$uuid}/pay", $payload)->assertStatus(422);
    }

    public function test_only_a_confirmed_payment_can_be_refunded(): void
    {
        $applicant = $this->applicant();
        $cashier = $this->user('cashier');

        $pending = $this->actingAs($cashier)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/payments", [
                'amount' => 9000,
                'currency' => 'BDT',
                'method' => 'bank_deposit',
                'transaction_reference' => 'DEP-REF-1',
            ])->assertCreated()->json('data.id');

        $this->actingAs($cashier)
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 1000,
                'currency' => 'BDT',
                'reason' => 'Too soon.',
                'payment_id' => $pending,
            ])
            ->assertStatus(422);
    }

    public function test_a_refund_against_a_receipt_is_capped_by_that_receipt(): void
    {
        $applicant = $this->applicant();
        $small = $this->collect($applicant, 3000);
        $this->collect($applicant, 20000);

        $this->actingAs($this->user('cashier'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 5000,
                'currency' => 'BDT',
                'reason' => 'Wrong receipt.',
                'payment_id' => $small,
            ])
            ->assertStatus(422);
    }

    public function test_a_recruiter_cannot_raise_a_refund(): void
    {
        $applicant = $this->applicant();

        $this->actingAs($this->user('data-entry-officer'))
            ->postJson("/api/v1/applicants/{$applicant->uuid}/refunds", [
                'requested_amount' => 1000,
                'currency' => 'BDT',
                'reason' => 'Not my call.',
            ])
            ->assertForbidden();
    }
}
