<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Customer;
use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanProduct;
use App\Models\LoanTopUp;
use App\Models\Role;
use App\Models\User;
use App\Services\MoneyMath;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class LoanTopUpWorkflowTest extends TestCase
{
    use RefreshDatabase;

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

    public function test_example_top_up_settles_old_loan_and_disburses_only_net_cash_with_balanced_accounting(): void
    {
        [$branch,$old,$product,$account,$manager] = $this->fixture('example','200000.00','20000.00');

        $this->actingAs($manager)->post(route('loan-top-ups.store',$old),$this->applicationPayload($product,'120000.00'))
            ->assertRedirect();
        $new = Loan::where('top_up_source_loan_id',$old->id)->firstOrFail();
        $this->assertSame('draft',$new->status);
        $this->assertSame('120000.00',$new->principal);
        $this->assertSame('10000.00',$new->interest);
        $this->assertSame('130000.00',$new->total_repayment);
        $this->assertSame('80.00',$new->top_up_paid_percentage_at_application);
        $this->assertSame('70.00',$new->top_up_eligibility_percentage);
        $this->assertFalse($new->top_up_override_used);

        $this->markReplacementReady($new,$manager);
        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','reference'=>'TOPUP-EXAMPLE','notes'=>'Approved top-up',
        ])->assertRedirect()->assertSessionHasNoErrors();

        $topUp = LoanTopUp::firstOrFail();
        $this->assertMatchesRegularExpression('/^TOP-[A-Z0-9]+-2026-\d{6}$/',$topUp->top_up_number);
        $this->assertSame('20000.00',$topUp->previous_outstanding);
        $this->assertSame('20000.00',$topUp->amount_settled_internally);
        $this->assertSame('120000.00',$topUp->new_principal);
        $this->assertSame('10000.00',$topUp->new_interest);
        $this->assertSame('130000.00',$topUp->new_total_repayment);
        $this->assertSame('100000.00',$topUp->cash_given_to_customer);
        $this->assertSame('100000.00',$account->fresh()->current_balance);

        $old->refresh(); $new->refresh();
        $this->assertSame('closed_through_top_up',$old->status);
        $this->assertSame('0.00',$old->total_outstanding);
        $this->assertSame('100000.00',$old->total_paid);
        $this->assertSame('paid',$old->installments()->firstOrFail()->status);
        $this->assertSame('0.00',$old->installments()->firstOrFail()->remaining);
        $this->assertSame('active',$new->status);
        $this->assertSame('120000.00',$new->principal_outstanding);
        $this->assertSame('10000.00',$new->interest_outstanding);
        $this->assertSame('130000.00',$new->total_outstanding);

        $this->assertDatabaseHas('financial_account_transactions',[
            'related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,'type'=>'loan_top_up','money_out'=>100000,'money_in'=>0,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,'event_type'=>'loan_top_up','category'=>'principal','debit'=>120000,'credit'=>0,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,'event_type'=>'loan_top_up','category'=>'principal','debit'=>0,'credit'=>20000,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanTopUp::class,'related_id'=>$topUp->id,'event_type'=>'loan_top_up','category'=>'assets','debit'=>0,'credit'=>100000,
        ]);
        $money = app(MoneyMath::class);
        $debits = AccountingEntry::where('related_type',LoanTopUp::class)->where('related_id',$topUp->id)->sum('debit');
        $credits = AccountingEntry::where('related_type',LoanTopUp::class)->where('related_id',$topUp->id)->sum('credit');
        $this->assertSame(12000000,$money->toMinor((string)$debits));
        $this->assertSame(12000000,$money->toMinor((string)$credits));

        $this->assertDatabaseHas('customer_statement_entries',['loan_id'=>$old->id,'type'=>'top_up_internal_settlement','credit'=>20000,'running_balance'=>0]);
        $this->assertDatabaseHas('customer_statement_entries',['loan_id'=>$new->id,'type'=>'loan_top_up','debit'=>130000,'running_balance'=>130000]);
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$old->id,'from_status'=>'active','to_status'=>'closed_through_top_up']);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_top_up.executed','auditable_id'=>$topUp->id,'branch_id'=>$branch->id]);
    }

    public function test_configured_threshold_blocks_ineligible_user_and_authorized_override_is_permanently_recorded(): void
    {
        [$branch,$old,$product,,$manager,$officer] = $this->fixture('override','200000.00','30000.00');
        $branch->business->update(['top_up_eligibility_percentage'=>'80.00']);

        $this->actingAs($officer)->post(route('loan-top-ups.store',$old),$this->applicationPayload($product,'120000.00'))
            ->assertSessionHasErrors('eligibility');
        $this->assertSame(0,Loan::where('top_up_source_loan_id',$old->id)->count());

        $payload = $this->applicationPayload($product,'120000.00') + ['override_eligibility'=>1,'override_reason'=>'Strong payment history verified by manager.'];
        $this->actingAs($officer)->post(route('loan-top-ups.store',$old),$payload)->assertSessionHasErrors('override_eligibility');
        $this->assertSame(0,Loan::where('top_up_source_loan_id',$old->id)->count());

        $this->actingAs($manager)->post(route('loan-top-ups.store',$old),$payload)->assertRedirect();
        $new = Loan::where('top_up_source_loan_id',$old->id)->firstOrFail();
        $this->assertSame('70.00',$new->top_up_paid_percentage_at_application);
        $this->assertSame('80.00',$new->top_up_eligibility_percentage);
        $this->assertTrue($new->top_up_override_used);
        $this->assertSame($manager->id,$new->top_up_override_by);
        $this->assertSame('Strong payment history verified by manager.',$new->top_up_override_reason);
        $this->assertNotNull($new->top_up_override_at);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_top_up.application_created','auditable_id'=>$new->id,'branch_id'=>$branch->id]);
    }

    public function test_execution_recalculates_current_old_balance_instead_of_using_stale_application_quote(): void
    {
        [, $old,$product,$account,$manager] = $this->fixture('recalc','200000.00','20000.00');
        $this->actingAs($manager)->post(route('loan-top-ups.store',$old),$this->applicationPayload($product,'120000.00'))->assertRedirect();
        $new = Loan::where('top_up_source_loan_id',$old->id)->firstOrFail();

        $oldInstallment = $old->installments()->firstOrFail();
        $oldInstallment->update(['principal_paid'=>'90000.00','amount_paid'=>'90000.00','remaining'=>'10000.00','status'=>'partial']);
        $old->update(['principal_outstanding'=>'10000.00','total_outstanding'=>'10000.00','total_paid'=>'90000.00']);
        CustomerStatementEntry::create([
            'customer_id'=>$old->customer_id,'loan_id'=>$old->id,'branch_id'=>$old->branch_id,'entry_date'=>'2026-08-11','reference'=>'PAY-LATE',
            'type'=>'loan_payment','description'=>'Payment after top-up application','debit'=>'0.00','credit'=>'10000.00','running_balance'=>'10000.00',
        ]);
        $this->markReplacementReady($new,$manager);

        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertRedirect()->assertSessionHasNoErrors();
        $topUp = LoanTopUp::firstOrFail();
        $this->assertSame('10000.00',$topUp->previous_outstanding);
        $this->assertSame('110000.00',$topUp->cash_given_to_customer);
        $this->assertSame('90000.00',$account->fresh()->current_balance);
    }

    public function test_duplicate_execution_and_normal_disbursement_of_top_up_replacement_are_blocked(): void
    {
        [, $old,$product,$account,$manager] = $this->fixture('duplicate','200000.00','20000.00');
        $this->actingAs($manager)->post(route('loan-top-ups.store',$old),$this->applicationPayload($product,'120000.00'))->assertRedirect();
        $new = Loan::where('top_up_source_loan_id',$old->id)->firstOrFail();
        $this->markReplacementReady($new,$manager);

        $this->actingAs($manager)->get(route('loan-disbursements.create',$new))->assertStatus(409);
        $this->actingAs($manager)->post(route('loan-disbursements.store',$new),[
            'amount'=>'120000.00','financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('loan');
        $this->assertSame('200000.00',$account->fresh()->current_balance);

        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertRedirect()->assertSessionHasNoErrors();
        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('loan');
        $this->assertSame(1,LoanTopUp::count());
        $this->assertSame('100000.00',$account->fresh()->current_balance);
    }

    public function test_signed_contract_requirement_and_insufficient_net_funds_abort_atomically(): void
    {
        [$branch,$old,$product,$account,$manager] = $this->fixture('guard','90000.00','20000.00');
        $this->actingAs($manager)->post(route('loan-top-ups.store',$old),$this->applicationPayload($product,'120000.00'))->assertRedirect();
        $new = Loan::where('top_up_source_loan_id',$old->id)->firstOrFail();
        $new->update(['status'=>'ready_for_disbursement']);
        $branch->business->update(['require_signed_contract_before_disbursement'=>true]);

        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('loan');
        $this->assertSame(0,LoanTopUp::count());
        $this->assertSame('active',$old->fresh()->status);
        $this->assertSame('90000.00',$account->fresh()->current_balance);

        $branch->business->update(['require_signed_contract_before_disbursement'=>false]);
        $account->update(['current_balance'=>'50000.00']);
        $this->actingAs($manager)->post(route('loan-top-ups.execute',$new),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12',
        ])->assertSessionHasErrors('financial_account_id');
        $this->assertSame(0,LoanTopUp::count());
        $this->assertSame('active',$old->fresh()->status);
        $this->assertSame('ready_for_disbursement',$new->fresh()->status);
        $this->assertSame('50000.00',$account->fresh()->current_balance);
    }

    public function test_top_up_pages_permissions_and_branch_isolation_are_backend_enforced(): void
    {
        [$branchA,$loanA,$productA] = $this->fixture('branch-a','200000.00','20000.00');
        [$branchB,$loanB] = $this->fixture('branch-b','200000.00','20000.00');
        $managerA = $this->staff('branch-manager',$branchA,'topup_manager_a');
        $managerB = $this->staff('branch-manager',$branchB,'topup_manager_b');
        $auditorA = $this->staff('auditor',$branchA,'topup_auditor_a');

        $this->actingAs($managerA)->get(route('loan-top-ups.index'))->assertOk()->assertSee('data-standard-table',false)->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($managerA)->get(route('loan-top-ups.create',$loanB))->assertForbidden();
        $this->actingAs($managerA)->post(route('loan-top-ups.store',$loanB),$this->applicationPayload($productA,'120000.00'))->assertForbidden();
        $this->actingAs($auditorA)->get(route('loan-top-ups.index'))->assertOk();
        $this->actingAs($auditorA)->get(route('loan-top-ups.create',$loanA))->assertForbidden();
        $this->actingAs($managerB)->get(route('loan-top-ups.create',$loanB))->assertOk()->assertSee('data-ui="form-shell"',false)->assertSee('Backend top-up preview');
    }

    public function test_business_setting_renders_and_custom_threshold_is_used(): void
    {
        $admin = User::where('username','admin')->firstOrFail();
        $business = Business::firstOrFail();
        $business->update(['top_up_eligibility_percentage'=>'75.50']);
        $this->actingAs($admin)->get(route('admin.business-settings.edit'))
            ->assertOk()->assertSee('Loan top-up eligibility')->assertSee('75.50');

        [, $old] = $this->fixture('custom-threshold','200000.00','24000.00');
        $old->branch->business->update(['top_up_eligibility_percentage'=>'75.50']);
        $manager = $this->staff('branch-manager',$old->branch,'custom_threshold_manager');
        $this->actingAs($manager)->get(route('loan-top-ups.create',$old))
            ->assertOk()->assertSee('76.00%')->assertSee('75.50%')->assertSee('Eligible for top-up');
    }

    private function fixture(string $tag, string $accountBalance, string $oldOutstanding): array
    {
        $business = Business::firstOrFail();
        $branch = Branch::create(['business_id'=>$business->id,'name'=>'Top Up '.str($tag)->headline(),'status'=>'active']);
        $customer = Customer::create([
            'branch_id'=>$branch->id,'customer_number'=>'CUS-TOP-'.strtoupper(substr(md5($tag),0,8)),'identity_mode'=>'without_nin',
            'first_name'=>'Asha','last_name'=>str($tag)->headline()->toString(),'phone'=>'0733'.str_pad((string)$branch->id,6,'0',STR_PAD_LEFT),'status'=>'active',
        ]);
        $oldProduct = $this->product($business,'Old '.$tag,'flat','0.000000',null);
        $newProduct = $this->product($business,'Topup '.$tag,'fixed_amount','0.000000','10000.00');
        $money = app(MoneyMath::class);
        $outstandingMinor = $money->toMinor($oldOutstanding);
        $paid = $money->fromMinor($money->toMinor('100000.00')-$outstandingMinor);
        $old = Loan::create([
            'branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$oldProduct->id,'sequence_number'=>1,'sequence_year'=>2026,
            'loan_number'=>'LN-TOP-OLD-'.strtoupper(substr(md5($tag),0,8)),'principal'=>'100000.00','interest'=>'0.00','processing_fee'=>'0.00','insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>'0.00','total_repayment'=>'100000.00',
            'principal_outstanding'=>$oldOutstanding,'interest_outstanding'=>'0.00','fees_outstanding'=>'0.00','penalties_outstanding'=>'0.00','total_outstanding'=>$oldOutstanding,'total_paid'=>$paid,
            'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0.000000','purpose'=>'Original loan','schedule_start_date'=>'2026-06-01','status'=>'active','disbursed_at'=>'2026-06-01','activated_at'=>'2026-06-01','schedule_activated_at'=>'2026-06-01',
        ]);
        $old->installments()->create([
            'installment_number'=>1,'principal'=>'100000.00','interest'=>'0.00','fees'=>'0.00','amount_due'=>'100000.00','due_date'=>'2026-12-01',
            'principal_paid'=>$paid,'interest_paid'=>'0.00','fees_paid'=>'0.00','penalty_due'=>'0.00','penalty_paid'=>'0.00','amount_paid'=>$paid,'remaining'=>$oldOutstanding,'status'=>'partial',
        ]);
        CustomerStatementEntry::create([
            'customer_id'=>$customer->id,'loan_id'=>$old->id,'branch_id'=>$branch->id,'entry_date'=>'2026-06-01','reference'=>'DSB-'.$tag,'type'=>'loan_disbursement','description'=>'Original obligation','debit'=>'100000.00','credit'=>'0.00','running_balance'=>'100000.00',
        ]);
        if ($paid !== '0.00') CustomerStatementEntry::create([
            'customer_id'=>$customer->id,'loan_id'=>$old->id,'branch_id'=>$branch->id,'entry_date'=>'2026-08-01','reference'=>'PAY-'.$tag,'type'=>'loan_payment','description'=>'Prior repayments','debit'=>'0.00','credit'=>$paid,'running_balance'=>$oldOutstanding,
        ]);
        $account = FinancialAccount::create(['branch_id'=>$branch->id,'name'=>'Top Up Cash '.$tag,'account_type'=>'cash','opening_balance'=>$accountBalance,'current_balance'=>$accountBalance,'status'=>'active']);
        $manager = $this->staff('branch-manager',$branch,'topup_manager_'.$tag);
        $officer = $this->staff('loan-officer',$branch,'topup_officer_'.$tag);
        return [$branch,$old,$newProduct,$account,$manager,$officer];
    }

    private function product(Business $business, string $name, string $method, string $rate, ?string $fixedInterest): LoanProduct
    {
        return LoanProduct::create([
            'business_id'=>$business->id,'name'=>$name,'minimum_amount'=>'1000.00','maximum_amount'=>'500000.00','duration'=>1,'duration_unit'=>'months',
            'interest_rate'=>$rate,'interest_calculation_method'=>$method,'fixed_interest_amount'=>$fixedInterest,'repayment_frequency'=>'monthly','grace_period_days'=>0,
            'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0.000000','processing_fee_type'=>'fixed','processing_fee_value'=>'0.000000',
            'insurance_fee_type'=>'fixed','insurance_fee_value'=>'0.000000','other_charges'=>'0.00','collateral_required'=>false,'guarantor_required'=>false,'status'=>'active',
        ]);
    }

    private function applicationPayload(LoanProduct $product, string $principal): array
    {
        return ['loan_product_id'=>$product->id,'principal'=>$principal,'purpose'=>'Business stock top-up','schedule_start_date'=>'2026-09-01'];
    }

    private function markReplacementReady(Loan $loan, User $user): void
    {
        $loan->documents()->create([
            'kind'=>'signed_contract','document_type'=>'loan_contract','original_name'=>'signed-contract.pdf',
            'file_path'=>'tests/signed-contract-'.$loan->id.'.pdf','mime_type'=>'application/pdf','file_size'=>1,'uploaded_by'=>$user->id,
        ]);
        $loan->update([
            'status'=>'ready_for_disbursement','contract_signed_at'=>now(),'contract_signed_uploaded_by'=>$user->id,
            'approved_at'=>now(),'approved_by'=>$user->id,
        ]);
    }

    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;
    }
}
