<?php

namespace Tests\Feature;

use App\Models\{AccountingEntry,Branch,Business,Collateral,CollateralCategory,CollectionFollowUp,Customer,FinancialAccount,Loan,LoanPayment,LoanProduct,LoanWriteOff,LoanWriteOffRecovery,Role,User};
use Carbon\CarbonImmutable;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class LoanWriteOffWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        CarbonImmutable::setTestNow('2026-08-12 11:30:00');
        $this->seed(DatabaseSeeder::class);
        Storage::fake('local');
    }

    protected function tearDown(): void
    {
        CarbonImmutable::setTestNow();
        parent::tearDown();
    }

    public function test_request_requires_full_balance_evidence_and_management_permission(): void
    {
        [$branch,$loan,,$manager] = $this->fixture('request');
        $officer = $this->staff('loan-officer',$branch,'wo_officer');

        $this->actingAs($manager)->post(route('loan-write-offs.store',$loan),[
            'write_off_amount'=>'67500.00','reason'=>'Evidence missing.',
        ])->assertSessionHasErrors('documents');

        $this->actingAs($manager)->post(route('loan-write-offs.store',$loan),[
            'write_off_amount'=>'60000.00','reason'=>'Partial write-off.',
            'documents'=>[$this->evidence()],
        ])->assertSessionHasErrors('write_off_amount');

        $this->actingAs($officer)->post(route('loan-write-offs.store',$loan),[
            'write_off_amount'=>'67500.00','reason'=>'Not management.',
            'documents'=>[$this->evidence()],
        ])->assertForbidden();

        $record=$this->requestWriteOff($loan,$manager);
        $this->assertSame('pending',$record->status);
        $this->assertSame('60000.00',$record->principal_outstanding);
        $this->assertSame('5000.00',$record->interest_outstanding);
        $this->assertSame('2500.00',$record->fees_outstanding);
        $this->assertSame('67500.00',$record->write_off_amount);
        $this->assertSame('67500.00',$record->recoverable_balance);
        $this->assertSame('active',$loan->fresh()->status);
        Storage::disk('local')->assertExists($record->documents()->firstOrFail()->file_path);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_write_off.requested','auditable_id'=>$record->id]);
    }

    public function test_approval_preserves_original_history_and_posts_balanced_principal_write_off(): void
    {
        [$branch,$loan,$account,$manager,$collateral,$payment,$followUp] = $this->fixture('approval');
        $approver=$this->staff('branch-manager',$branch,'wo_approver');
        $record=$this->requestWriteOff($loan,$manager);
        $loanSnapshot=$loan->only(['principal_outstanding','interest_outstanding','fees_outstanding','total_outstanding','total_paid']);
        $paymentSnapshot=$payment->only(['amount','principal_amount','interest_amount','fees_amount']);
        $installmentSnapshot=$loan->allInstallments()->firstOrFail()->only(['amount_paid','remaining','status']);

        $this->actingAs($approver)->post(route('loan-write-offs.decision',$record),[
            'decision'=>'approve','comments'=>'Approved after evidence review.',
        ])->assertRedirect();

        $record->refresh(); $loan->refresh();
        $this->assertSame('approved',$record->status);
        $this->assertSame('written_off',$loan->status);
        $this->assertSame($approver->id,$loan->written_off_by);
        $this->assertSame($loanSnapshot,$loan->only(array_keys($loanSnapshot)));
        $this->assertSame($paymentSnapshot,$payment->fresh()->only(array_keys($paymentSnapshot)));
        $this->assertSame($installmentSnapshot,$loan->allInstallments()->firstOrFail()->only(array_keys($installmentSnapshot)));
        $this->assertSame('in_custody',$collateral->fresh()->status);
        $this->assertDatabaseHas('collection_follow_ups',['id'=>$followUp->id]);
        $this->assertSame('10000.00',$account->fresh()->current_balance);

        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanWriteOff::class,'related_id'=>$record->id,'category'=>'write_offs','debit'=>60000,'credit'=>0,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanWriteOff::class,'related_id'=>$record->id,'category'=>'principal','debit'=>0,'credit'=>60000,
        ]);
        $this->assertSame(
            AccountingEntry::where('related_type',LoanWriteOff::class)->where('related_id',$record->id)->sum('debit'),
            AccountingEntry::where('related_type',LoanWriteOff::class)->where('related_id',$record->id)->sum('credit')
        );
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$loan->id,'from_status'=>'active','to_status'=>'written_off']);
        $this->assertDatabaseHas('customer_statement_entries',['loan_id'=>$loan->id,'type'=>'loan_write_off','running_balance'=>67500]);
    }

    public function test_stale_balance_and_maker_checker_controls_block_invalid_approval(): void
    {
        [$branch,$loan,,$manager] = $this->fixture('controls');
        $branch->business->update(['maker_checker_enabled'=>true]);
        $approver=$this->staff('branch-manager',$branch,'wo_checker');
        $record=$this->requestWriteOff($loan,$manager);

        $this->actingAs($manager)->post(route('loan-write-offs.decision',$record),['decision'=>'approve'])
            ->assertSessionHasErrors('write_off');
        $this->assertSame('pending',$record->fresh()->status);

        $loan->update(['principal_outstanding'=>'59000.00','total_outstanding'=>'66500.00','total_paid'=>'48500.00']);
        $this->actingAs($approver)->post(route('loan-write-offs.decision',$record),['decision'=>'approve'])
            ->assertSessionHasErrors('write_off');
        $this->assertSame('pending',$record->fresh()->status);
        $this->assertSame('active',$loan->fresh()->status);
        $this->assertDatabaseMissing('accounting_entries',['related_type'=>LoanWriteOff::class,'related_id'=>$record->id]);
    }

    public function test_recovery_is_separate_cash_and_income_flow_and_cannot_over_recover(): void
    {
        [$branch,$loan,$account,$manager,,$payment] = $this->fixture('recovery');
        $approver=$this->staff('branch-manager',$branch,'wo_recovery_approver');
        $cashier=$this->staff('cashier',$branch,'wo_cashier');
        $record=$this->requestWriteOff($loan,$manager);
        $this->actingAs($approver)->post(route('loan-write-offs.decision',$record),['decision'=>'approve'])->assertRedirect();

        $loanSnapshot=$loan->fresh()->only(['principal_outstanding','interest_outstanding','fees_outstanding','total_outstanding','total_paid']);
        $paymentSnapshot=$payment->fresh()->only(['amount','principal_amount','interest_amount','fees_amount']);
        $paymentCount=LoanPayment::where('loan_id',$loan->id)->count();

        $this->actingAs($cashier)->post(route('loan-write-offs.recoveries.store',$record),[
            'financial_account_id'=>$account->id,'amount'=>'10000.00','date'=>'2026-08-12','method'=>'cash','reference'=>'WOR-001',
        ])->assertRedirect();

        $recovery=LoanWriteOffRecovery::firstOrFail(); $record->refresh();
        $this->assertSame('10000.00',$record->recovered_amount);
        $this->assertSame('57500.00',$record->recoverable_balance);
        $this->assertSame('20000.00',$account->fresh()->current_balance);
        $this->assertSame('written_off',$loan->fresh()->status);
        $this->assertSame($loanSnapshot,$loan->fresh()->only(array_keys($loanSnapshot)));
        $this->assertSame($paymentSnapshot,$payment->fresh()->only(array_keys($paymentSnapshot)));
        $this->assertSame($paymentCount,LoanPayment::where('loan_id',$loan->id)->count());
        $this->assertDatabaseHas('financial_account_transactions',[
            'related_type'=>LoanWriteOffRecovery::class,'related_id'=>$recovery->id,'type'=>'loan_write_off_recovery','money_in'=>10000,
        ]);
        $this->assertDatabaseHas('accounting_entries',[
            'related_type'=>LoanWriteOffRecovery::class,'related_id'=>$recovery->id,'category'=>'write_off_recoveries','credit'=>10000,
        ]);
        $this->assertDatabaseHas('daily_collections',[
            'branch_id'=>$branch->id,'amount'=>10000,'write_off_recoveries'=>10000,'write_off_recoveries_count'=>1,'payments_count'=>0,
        ]);
        $this->assertDatabaseHas('customer_statement_entries',[
            'loan_id'=>$loan->id,'type'=>'loan_write_off_recovery','credit'=>10000,'running_balance'=>57500,
        ]);

        $this->actingAs($cashier)->post(route('loan-write-offs.recoveries.store',$record),[
            'financial_account_id'=>$account->id,'amount'=>'60000.00','date'=>'2026-08-12','method'=>'cash',
        ])->assertSessionHasErrors('amount');
        $this->assertSame(1,LoanWriteOffRecovery::count());
        $this->assertSame('57500.00',$record->fresh()->recoverable_balance);
    }

    public function test_rejection_and_branch_scoping_preserve_active_loan_and_private_evidence(): void
    {
        [$branchA,$loanA,,$managerA]=$this->fixture('branch-a');
        [$branchB,$loanB,,$managerB]=$this->fixture('branch-b');
        $approverA=$this->staff('branch-manager',$branchA,'wo_rejector');
        $auditorA=$this->staff('auditor',$branchA,'wo_auditor');
        $recordA=$this->requestWriteOff($loanA,$managerA);
        $recordB=$this->requestWriteOff($loanB,$managerB);
        $document=$recordA->documents()->firstOrFail();

        $this->actingAs($auditorA)->get(route('loan-write-offs.index'))->assertOk()
            ->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($auditorA)->get(route('loan-write-offs.show',$recordB))->assertForbidden();
        $this->actingAs($auditorA)->get(route('loan-write-offs.documents.download',[$recordA,$document]))->assertOk();

        $this->actingAs($approverA)->post(route('loan-write-offs.decision',$recordA),[
            'decision'=>'reject','comments'=>'Continue collections and collateral enforcement.',
        ])->assertRedirect();
        $this->assertSame('rejected',$recordA->fresh()->status);
        $this->assertSame('active',$loanA->fresh()->status);
        $this->assertSame('67500.00',$loanA->fresh()->total_outstanding);
        $this->assertDatabaseMissing('accounting_entries',['related_type'=>LoanWriteOff::class,'related_id'=>$recordA->id]);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_write_off.rejected','auditable_id'=>$recordA->id]);
    }

    private function requestWriteOff(Loan $loan, User $manager): LoanWriteOff
    {
        $this->actingAs($manager)->post(route('loan-write-offs.store',$loan),[
            'write_off_amount'=>'67500.00','reason'=>'Collection attempts were exhausted and documented.',
            'documents'=>[$this->evidence()],
        ])->assertRedirect();
        return LoanWriteOff::latest('id')->firstOrFail();
    }

    private function evidence(): UploadedFile
    {
        return UploadedFile::fake()->create('write-off-evidence.pdf',100,'application/pdf');
    }

    private function fixture(string $tag): array
    {
        $business=Business::firstOrFail();
        $branch=Branch::create(['business_id'=>$business->id,'name'=>'Write Off '.str($tag)->headline(),'status'=>'active']);
        $customer=Customer::create([
            'branch_id'=>$branch->id,'customer_number'=>'CUS-WO-'.strtoupper(substr(md5($tag),0,8)),
            'identity_mode'=>'without_nin','first_name'=>'Asha','last_name'=>str($tag)->headline()->toString(),
            'phone'=>'0755'.str_pad((string)$branch->id,6,'0',STR_PAD_LEFT),'status'=>'active',
        ]);
        $product=LoanProduct::create([
            'business_id'=>$business->id,'name'=>'Write Off '.$tag,'minimum_amount'=>'1000.00','maximum_amount'=>'500000.00',
            'duration'=>1,'duration_unit'=>'months','interest_rate'=>'10.000000','interest_calculation_method'=>'flat',
            'repayment_frequency'=>'monthly','grace_period_days'=>0,'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0',
            'processing_fee_type'=>'fixed','processing_fee_value'=>'5000','insurance_fee_type'=>'fixed','insurance_fee_value'=>'0',
            'other_charges'=>'0','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'=>2026,
            'loan_number'=>'LN-WO-'.strtoupper(substr(md5($tag),0,8)),'principal'=>'100000','interest'=>'10000',
            'processing_fee'=>'5000','insurance_fee'=>'0','other_charges'=>'0','fees'=>'5000','total_repayment'=>'115000',
            'principal_outstanding'=>'60000','interest_outstanding'=>'5000','fees_outstanding'=>'2500','penalties_outstanding'=>'0',
            'total_outstanding'=>'67500','total_paid'=>'47500','late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0',
            'purpose'=>'Working capital','schedule_start_date'=>'2026-07-01','status'=>'active','disbursed_at'=>'2026-07-01',
            'activated_at'=>'2026-07-01','schedule_activated_at'=>'2026-07-01','current_schedule_version'=>1,
        ]);
        $loan->allInstallments()->create([
            'schedule_version'=>1,'is_active'=>true,'installment_number'=>1,'principal'=>'100000','interest'=>'10000','fees'=>'5000',
            'amount_due'=>'115000','due_date'=>'2026-08-01','principal_paid'=>'40000','interest_paid'=>'5000','fees_paid'=>'2500',
            'penalty_due'=>'0','penalty_paid'=>'0','amount_paid'=>'47500','remaining'=>'67500','status'=>'overdue',
        ]);
        $account=FinancialAccount::create([
            'branch_id'=>$branch->id,'name'=>'Write Off Cash '.$tag,'account_type'=>'cash',
            'opening_balance'=>'10000','current_balance'=>'10000','status'=>'active',
        ]);
        $manager=$this->staff('branch-manager',$branch,'wo_manager_'.str_replace('-','_',$tag));
        $payment=LoanPayment::create([
            'receipt_number'=>'REC-WO-'.strtoupper(substr(md5($tag),0,8)),'loan_id'=>$loan->id,'customer_id'=>$customer->id,
            'branch_id'=>$branch->id,'financial_account_id'=>$account->id,'amount'=>'47500','principal_amount'=>'40000',
            'interest_amount'=>'5000','fees_amount'=>'2500','penalties_amount'=>'0','payment_date'=>'2026-08-01 09:00:00',
            'method'=>'cash','received_by'=>$manager->id,
        ]);
        $followUp=CollectionFollowUp::create([
            'loan_id'=>$loan->id,'customer_id'=>$customer->id,'branch_id'=>$branch->id,'action_type'=>'visit',
            'action_at'=>'2026-08-10 10:00:00','notes'=>'Collection visit.','employee_id'=>$manager->id,
        ]);
        $category=CollateralCategory::where('business_id',$business->id)->firstOrFail();
        $collateral=Collateral::create([
            'customer_id'=>$customer->id,'branch_id'=>$branch->id,'category_id'=>$category->id,'sequence_number'=>1,
            'code'=>'COL-WO-'.strtoupper(substr(md5($tag),0,8)),'item'=>'Television','estimated_value'=>'120000',
            'accepted_value'=>'90000','received_at'=>'2026-07-01','store_room'=>'Main Store','shelf'=>'A1',
            'status'=>'in_custody','created_by'=>$manager->id,'updated_by'=>$manager->id,
        ]);
        $loan->collaterals()->attach($collateral->id);
        return [$branch,$loan,$account,$manager,$collateral,$payment,$followUp];
    }

    private function staff(string $role, Branch $branch, string $username): User
    {
        $user=User::factory()->create(['name'=>str($username)->replace('_',' ')->title(),'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;
    }
}
