<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Customer;
use App\Models\DailyCollection;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanProduct;
use App\Models\Role;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class LoanPaymentWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(DatabaseSeeder::class);
    }

    public function test_partial_payment_updates_account_schedule_loan_statement_ledger_and_daily_collection(): void
    {
        [$branch,$loan,$account,$cashier]=$this->fixture('partial');
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'30000.00'))->assertRedirect();
        $payment=LoanPayment::where('loan_id',$loan->id)->firstOrFail();
        $this->assertMatchesRegularExpression('/^REC-[A-Z0-9]+-\d{4}-\d{6}$/',$payment->receipt_number);
        $this->assertSame('23000.00',$payment->principal_amount); $this->assertSame('5000.00',$payment->interest_amount); $this->assertSame('2000.00',$payment->fees_amount); $this->assertSame('0.00',$payment->penalties_amount);
        $loan->refresh(); $account->refresh(); $first=$loan->installments()->firstOrFail();
        $this->assertSame('1030000.00',$account->current_balance); $this->assertSame('77000.00',$loan->principal_outstanding); $this->assertSame('5000.00',$loan->interest_outstanding); $this->assertSame('2000.00',$loan->fees_outstanding); $this->assertSame('84000.00',$loan->total_outstanding); $this->assertSame('30000.00',$loan->total_paid);
        $this->assertSame('partial',$first->status); $this->assertSame('23000.00',$first->principal_paid); $this->assertSame('5000.00',$first->interest_paid); $this->assertSame('2000.00',$first->fees_paid); $this->assertSame('27000.00',$first->remaining);
        $this->assertDatabaseHas('financial_account_transactions',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'type'=>'loan_payment','money_in'=>30000]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'assets','debit'=>30000,'credit'=>0]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'principal','debit'=>0,'credit'=>23000]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'interest','credit'=>5000]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'fees','credit'=>2000]);
        $this->assertDatabaseMissing('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'capital']);
        $this->assertDatabaseHas('customer_statement_entries',['loan_id'=>$loan->id,'related_type'=>LoanPayment::class,'related_id'=>$payment->id,'credit'=>30000,'running_balance'=>84000]);
        $this->assertDatabaseHas('daily_collections',['branch_id'=>$branch->id,'financial_account_id'=>$account->id,'amount'=>30000,'principal'=>23000,'interest'=>5000,'fees'=>2000,'payments_count'=>1]);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan.payment_received','auditable_id'=>$payment->id]);
    }

    public function test_exact_and_multiple_installment_payments_reconcile_and_complete_loan(): void
    {
        [, $loan,$account,$cashier]=$this->fixture('complete');
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'57000.00'))->assertRedirect();
        $loan->refresh(); $this->assertSame('paid',$loan->installments()->orderBy('installment_number')->first()->status); $this->assertSame('active',$loan->status); $this->assertSame('57000.00',$loan->total_outstanding);
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'57000.00'))->assertRedirect();
        $loan->refresh(); $this->assertSame('completed',$loan->status); $this->assertSame('0.00',$loan->total_outstanding); $this->assertSame('114000.00',$loan->total_paid); $this->assertNotNull($loan->completed_at); $this->assertSame(2,$loan->installments()->where('status','paid')->count());
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$loan->id,'from_status'=>'active','to_status'=>'completed']);
        $daily=DailyCollection::firstOrFail(); $this->assertSame('114000.00',$daily->amount); $this->assertSame(2,$daily->payments_count);
    }

    public function test_larger_than_installment_payment_spans_installments_with_permanent_allocations(): void
    {
        [, $loan,$account,$cashier]=$this->fixture('multi');
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'80000.00'))->assertRedirect();
        $payment=LoanPayment::firstOrFail(); $this->assertSame(2,$payment->allocations()->count()); $this->assertSame('66000.00',$payment->principal_amount); $this->assertSame('10000.00',$payment->interest_amount); $this->assertSame('4000.00',$payment->fees_amount);
        $installments=$loan->fresh()->installments()->orderBy('installment_number')->get(); $this->assertSame('paid',$installments[0]->status); $this->assertSame('partial',$installments[1]->status); $this->assertSame('34000.00',$installments[1]->remaining); $this->assertSame('34000.00',$loan->fresh()->total_outstanding);
    }

    public function test_penalties_are_allocated_before_fees_interest_and_principal(): void
    {
        [, $loan,$account,$cashier]=$this->fixture('penalty');
        $this->actingAs($cashier);
        $first=$loan->installments()->firstOrFail(); $first->update(['penalty_due'=>'3000.00','remaining'=>'60000.00']); $loan->update(['penalties_outstanding'=>'3000.00','total_outstanding'=>'117000.00']);
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'4000.00'))->assertRedirect();
        $payment=LoanPayment::firstOrFail(); $this->assertSame('3000.00',$payment->penalties_amount); $this->assertSame('1000.00',$payment->fees_amount); $this->assertSame('0.00',$payment->interest_amount); $this->assertSame('0.00',$payment->principal_amount);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanPayment::class,'related_id'=>$payment->id,'category'=>'penalties','credit'=>3000]);
    }

    public function test_overpayment_is_rejected_atomically(): void
    {
        [, $loan,$account,$cashier]=$this->fixture('overpay'); $before=$account->current_balance;
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'114000.01'))->assertSessionHasErrors('amount');
        $this->assertSame(0,LoanPayment::count()); $this->assertSame($before,$account->fresh()->current_balance); $this->assertSame('114000.00',$loan->fresh()->total_outstanding); $this->assertSame(0,AccountingEntry::where('event_type','loan_payment')->count());
    }

    public function test_payment_search_permission_and_branch_isolation_are_backend_enforced(): void
    {
        [$branch,$loan,$account,$cashier,$officer]=$this->fixture('security');
        $business=$branch->business; $otherBranch=Branch::create(['business_id'=>$business->id,'name'=>'Other Payments','status'=>'active']); $otherCashier=$this->staff('cashier',$otherBranch,'other_cashier');
        $this->actingAs($officer)->get(route('loan-payments.index'))->assertForbidden();
        $this->actingAs($cashier)->get(route('loan-payments.index',['search'=>$loan->loan_number]))->assertOk()->assertSee($loan->loan_number)->assertDontSee('No active loan matched this search.');
        $this->actingAs($otherCashier)->get(route('loan-payments.index',['search'=>$loan->loan_number]))->assertOk()->assertSee('No active loan matched this search.');
        $this->actingAs($otherCashier)->post(route('loan-payments.store',$loan),$this->payload($account,'1000.00'))->assertForbidden();
        $this->assertSame(0,LoanPayment::count());
    }

    public function test_payment_receipt_statement_and_form_use_real_standardized_data(): void
    {
        [, $loan,$account,$cashier]=$this->fixture('ui');
        $this->actingAs($cashier)->get(route('loan-payments.create',$loan))->assertOk()->assertSee('data-ui="form-shell"',false)->assertSee('data-ui="form-section"',false)->assertSee('data-ui="form-actions"',false);
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan),$this->payload($account,'30000.00'))->assertRedirect();
        $payment=LoanPayment::firstOrFail();
        $this->actingAs($cashier)->get(route('loan-payments.receipt',$payment))->assertOk()->assertSee($payment->receipt_number)->assertSee('Principal')->assertSee('Remaining loan balance');
        $this->actingAs($cashier)->get(route('loan-payments.statement',$loan))->assertOk()->assertSee('Customer statement')->assertSee($payment->receipt_number)->assertSee('Repayment schedule');
    }

    private function fixture(string $tag): array
    {
        $business=Business::firstOrFail(); $branch=Branch::create(['business_id'=>$business->id,'name'=>'Payment '.$tag,'status'=>'active']);
        $customer=Customer::create(['branch_id'=>$branch->id,'customer_number'=>'CUS-PAY-'.strtoupper($tag),'identity_mode'=>'without_nin','first_name'=>'Asha','last_name'=>'Musa','phone'=>'0755'.str_pad((string)$branch->id,6,'0',STR_PAD_LEFT),'status'=>'active']);
        $product=LoanProduct::create(['business_id'=>$business->id,'name'=>'Payment Product '.$tag,'minimum_amount'=>'10000.00','maximum_amount'=>'500000.00','duration'=>2,'duration_unit'=>'months','interest_rate'=>'10.000000','interest_calculation_method'=>'flat','repayment_frequency'=>'monthly','grace_period_days'=>0,'late_payment_penalty_type'=>'fixed','late_payment_penalty_value'=>'0.000000','processing_fee_type'=>'fixed','processing_fee_value'=>'4000.000000','insurance_fee_type'=>'fixed','insurance_fee_value'=>'0.000000','other_charges'=>'0.00','collateral_required'=>false,'guarantor_required'=>false,'status'=>'active']);
        $loan=Loan::create(['branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,'sequence_number'=>1,'sequence_year'=>(int)now()->format('Y'),'loan_number'=>'LN-PAY-'.strtoupper($tag),'principal'=>'100000.00','interest'=>'10000.00','processing_fee'=>'4000.00','insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>'4000.00','total_repayment'=>'114000.00','principal_outstanding'=>'100000.00','interest_outstanding'=>'10000.00','fees_outstanding'=>'4000.00','penalties_outstanding'=>'0.00','total_outstanding'=>'114000.00','total_paid'=>'0.00','purpose'=>'Working capital','schedule_start_date'=>now()->format('Y-m-d'),'status'=>'active','disbursed_at'=>now(),'activated_at'=>now(),'schedule_activated_at'=>now()]);
        foreach([1,2] as $number) $loan->installments()->create(['installment_number'=>$number,'principal'=>'50000.00','interest'=>'5000.00','fees'=>'2000.00','amount_due'=>'57000.00','due_date'=>now()->addMonths($number)->format('Y-m-d'),'amount_paid'=>'0.00','remaining'=>'57000.00','status'=>'upcoming','principal_paid'=>'0.00','interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00']);
        $account=FinancialAccount::create(['branch_id'=>$branch->id,'name'=>'Cash Till '.$tag,'account_type'=>'cash','opening_balance'=>'1000000.00','current_balance'=>'1000000.00','status'=>'active']);
        $cashier=$this->staff('cashier',$branch,'cashier_'.$tag); $officer=$this->staff('loan-officer',$branch,'officer_'.$tag); return [$branch,$loan,$account,$cashier,$officer];
    }

    private function staff(string $role, Branch $branch, string $username): User
    {
        $user=User::factory()->create(['name'=>str($username)->replace('_',' ')->title()->toString(),'username'=>$username,'status'=>'active','primary_branch_id'=>$branch->id]);
        $user->roles()->attach(Role::where('name',$role)->firstOrFail()); $user->branches()->attach($branch->id,['is_primary'=>true]); return $user;
    }

    private function payload(FinancialAccount $account, string $amount): array
    {
        return ['amount'=>$amount,'date'=>now()->format('Y-m-d'),'method'=>'cash','financial_account_id'=>$account->id,'reference'=>'REF-'.uniqid(),'notes'=>'Office collection'];
    }
}
