<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Business;
use App\Models\Collateral;
use App\Models\CollateralCategory;
use App\Models\Customer;
use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanEarlySettlement;
use App\Models\LoanProduct;
use App\Models\LoanRestructuring;
use App\Models\Role;
use App\Models\User;
use App\Services\LoanReminderService;
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 LoanAdjustmentWorkflowTest extends TestCase
{
    use RefreshDatabase;

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

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

    public function test_early_settlement_posts_finance_closes_loan_and_marks_collateral_ready_for_release(): void
    {
        [$branch,$loan,$account,$manager,$collateral] = $this->fixture('settle');

        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'0.00','reference'=>'EARLY-001',
        ])->assertRedirect();

        $settlement = LoanEarlySettlement::firstOrFail();
        $this->assertMatchesRegularExpression('/^STL-[A-Z0-9]+-2026-\d{6}$/',$settlement->settlement_number);
        $this->assertSame('60000.00',$settlement->principal_amount);
        $this->assertSame('5000.00',$settlement->interest_amount);
        $this->assertSame('2500.00',$settlement->fees_amount);
        $this->assertSame('0.00',$settlement->penalties_amount);
        $this->assertSame('67500.00',$settlement->final_amount);
        $this->assertSame('77500.00',$account->fresh()->current_balance);
        $this->assertSame('fully_paid',$loan->fresh()->status);
        $this->assertSame('0.00',$loan->fresh()->total_outstanding);
        $this->assertSame('ready_for_release',$collateral->fresh()->status);
        $this->assertDatabaseHas('collateral_movements',['collateral_id'=>$collateral->id,'action'=>'loan_settled_ready_for_release','to_status'=>'ready_for_release']);
        $this->assertDatabaseHas('financial_account_transactions',['related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,'type'=>'loan_early_settlement','money_in'=>67500]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,'category'=>'assets','debit'=>67500,'credit'=>0]);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,'category'=>'principal','debit'=>0,'credit'=>60000]);
        $this->assertDatabaseHas('customer_statement_entries',['loan_id'=>$loan->id,'type'=>'loan_early_settlement','credit'=>67500,'running_balance'=>0]);
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$loan->id,'from_status'=>'active','to_status'=>'fully_paid']);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_early_settlement.executed','auditable_id'=>$settlement->id,'branch_id'=>$branch->id]);
    }

    public function test_settlement_adjustment_is_permissioned_principal_safe_and_balanced(): void
    {
        [, $loan,$account,$manager] = $this->fixture('adjust');
        $cashier = $this->staff('cashier',$loan->branch,'adjust_cashier');

        $this->actingAs($cashier)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'-5000.00','adjustment_reason'=>'Approved loyalty discount',
        ])->assertSessionHasErrors('settlement_adjustment');
        $this->assertSame(0,LoanEarlySettlement::count());
        $this->assertSame('10000.00',$account->fresh()->current_balance);

        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'-8000.00','adjustment_reason'=>'Too large',
        ])->assertSessionHasErrors('settlement_adjustment');
        $this->assertSame(0,LoanEarlySettlement::count());

        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'-5000.00','adjustment_reason'=>'Approved non-principal settlement reduction',
        ])->assertRedirect();
        $settlement=LoanEarlySettlement::firstOrFail();
        $this->assertSame('-5000.00',$settlement->settlement_adjustment);
        $this->assertSame('62500.00',$settlement->final_amount);
        $this->assertSame('72500.00',$account->fresh()->current_balance);
        $this->assertDatabaseHas('accounting_entries',['related_type'=>LoanEarlySettlement::class,'related_id'=>$settlement->id,'category'=>'settlement_adjustment','debit'=>5000,'credit'=>0]);
        $debit = AccountingEntry::where('related_type',LoanEarlySettlement::class)->where('related_id',$settlement->id)->sum('debit');
        $credit = AccountingEntry::where('related_type',LoanEarlySettlement::class)->where('related_id',$settlement->id)->sum('credit');
        $this->assertEquals($debit,$credit);
    }

    public function test_early_settlement_assesses_due_penalty_and_duplicate_execution_is_atomic(): void
    {
        [, $loan,$account,$manager] = $this->fixture('penalty','2026-08-01');
        $loan->update(['late_payment_penalty_type'=>'fixed','late_payment_penalty_value'=>'1000.000000']);

        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'0.00',
        ])->assertRedirect();
        $settlement=LoanEarlySettlement::firstOrFail();
        $this->assertSame('1000.00',$settlement->penalties_amount);
        $this->assertSame('68500.00',$settlement->final_amount);
        $this->assertDatabaseHas('loan_penalty_charges',['loan_id'=>$loan->id,'amount'=>1000]);

        $balance=$account->fresh()->current_balance;
        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan),[
            'financial_account_id'=>$account->id,'date'=>'2026-08-12','settlement_adjustment'=>'0.00',
        ])->assertSessionHasErrors('loan');
        $this->assertSame(1,LoanEarlySettlement::count());
        $this->assertSame($balance,$account->fresh()->current_balance);
    }

    public function test_restructuring_preserves_old_schedule_documents_and_only_activates_new_version_after_approval(): void
    {
        Storage::fake('local');
        [$branch,$loan,$account,$manager] = $this->fixture('restructure');
        $officer=$this->staff('loan-officer',$branch,'restructure_officer');
        $inspector=$this->staff('loan-inspector',$branch,'restructure_inspector');
        $this->actingAs($officer);
        $oldId=$loan->installments()->firstOrFail()->id;
        $schedule="2026-09-30 | 30000.00\n2026-10-31 | 37500.00";

        $this->actingAs($officer)->post(route('loan-restructurings.store',$loan),[
            'new_schedule'=>$schedule,'reason'=>'Customer cash flow moved to month end.',
            'documents'=>[UploadedFile::fake()->create('request.pdf',100,'application/pdf')],
        ])->assertRedirect();
        $record=LoanRestructuring::firstOrFail();
        $this->assertSame('pending',$record->status);
        $this->assertSame(1,$record->previous_schedule_version);
        $this->assertSame(2,$record->new_schedule_version);
        $this->assertCount(1,$record->previous_schedule);
        $this->assertCount(2,$record->new_schedule);
        $this->assertTrue($loan->fresh()->installments()->whereKey($oldId)->exists());
        $document=$record->documents()->firstOrFail();
        Storage::disk('local')->assertExists($document->file_path);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_restructuring.requested','auditable_id'=>$record->id,'branch_id'=>$branch->id]);

        $this->actingAs($inspector)->post(route('loan-restructurings.decision',$record),['decision'=>'approve','comments'=>'Verified with customer.'])->assertRedirect();
        $record->refresh(); $loan->refresh();
        $this->assertSame('approved',$record->status);
        $this->assertSame($inspector->id,$record->approved_by);
        $this->assertSame(2,$loan->current_schedule_version);
        $this->assertFalse((bool)$loan->allInstallments()->whereKey($oldId)->firstOrFail()->is_active);
        $this->assertSame(2,$loan->installments()->count());
        $this->assertSame(3,$loan->allInstallments()->count());
        $this->assertSame([2],$loan->installments()->pluck('schedule_version')->unique()->values()->all());
        $this->assertSame('active',$loan->status);
        $this->assertDatabaseHas('loan_status_histories',['loan_id'=>$loan->id,'from_status'=>'active','to_status'=>'active']);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_restructuring.approved','auditable_id'=>$record->id,'branch_id'=>$branch->id]);
        $this->assertSame('10000.00',$account->fresh()->current_balance);
    }

    public function test_stale_restructuring_cannot_overwrite_an_intervening_balance_change(): void
    {
        [$branch,$loan] = $this->fixture('stale');
        $officer=$this->staff('loan-officer',$branch,'stale_officer');
        $inspector=$this->staff('loan-inspector',$branch,'stale_inspector');
        $this->actingAs($officer)->post(route('loan-restructurings.store',$loan),[
            'new_schedule'=>"2026-09-30 | 30000.00\n2026-10-31 | 37500.00",'reason'=>'Temporary income reduction.',
        ])->assertRedirect();
        $record=LoanRestructuring::firstOrFail();
        $oldId=$loan->installments()->firstOrFail()->id;

        $loan->update(['principal_outstanding'=>'59000.00','total_outstanding'=>'66500.00','total_paid'=>'48500.00']);
        $loan->installments()->firstOrFail()->update(['principal_paid'=>'41000.00','amount_paid'=>'48500.00','remaining'=>'66500.00']);
        $this->actingAs($inspector)->post(route('loan-restructurings.decision',$record),['decision'=>'approve'])->assertSessionHasErrors('restructuring');
        $this->assertSame('pending',$record->fresh()->status);
        $this->assertSame(1,$loan->fresh()->current_schedule_version);
        $this->assertTrue((bool)$loan->allInstallments()->whereKey($oldId)->firstOrFail()->is_active);
        $this->assertSame(1,$loan->fresh()->installments()->count());
    }

    public function test_restructuring_rejection_preserves_current_schedule_and_records_decision(): void
    {
        [$branch,$loan] = $this->fixture('reject');
        $officer=$this->staff('loan-officer',$branch,'reject_officer');
        $inspector=$this->staff('loan-inspector',$branch,'reject_inspector');
        $this->actingAs($officer);
        $oldId=$loan->installments()->firstOrFail()->id;
        $this->actingAs($officer)->post(route('loan-restructurings.store',$loan),[
            'new_schedule'=>"2026-09-30 | 67500.00",'reason'=>'Requested longer repayment date.',
        ])->assertRedirect();
        $record=LoanRestructuring::firstOrFail();
        $this->actingAs($inspector)->post(route('loan-restructurings.decision',$record),[
            'decision'=>'reject','comments'=>'Income evidence does not support the request.',
        ])->assertRedirect();
        $record->refresh();
        $this->assertSame('rejected',$record->status);
        $this->assertSame($inspector->id,$record->rejected_by);
        $this->assertSame('Income evidence does not support the request.',$record->decision_comments);
        $this->assertSame(1,$loan->fresh()->current_schedule_version);
        $this->assertTrue((bool)$loan->allInstallments()->whereKey($oldId)->firstOrFail()->is_active);
        $this->assertDatabaseHas('audit_logs',['action'=>'loan_restructuring.rejected','auditable_id'=>$record->id]);
    }

    public function test_permissions_branch_isolation_ui_and_retired_schedules_do_not_generate_reminders(): void
    {
        [$branchA,$loanA] = $this->fixture('branch-a','2026-08-13');
        [$branchB,$loanB] = $this->fixture('branch-b','2026-08-13');
        $managerA=$this->staff('branch-manager',$branchA,'adjust_manager_a');
        $officerA=$this->staff('loan-officer',$branchA,'adjust_officer_a');
        $inspectorA=$this->staff('loan-inspector',$branchA,'adjust_inspector_a');
        $auditorA=$this->staff('auditor',$branchA,'adjust_auditor_a');

        $this->actingAs($auditorA)->get(route('loan-adjustments.index'))->assertOk()->assertSee('data-standard-table',false)->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($auditorA)->get(route('loan-early-settlements.create',$loanA))->assertForbidden();
        $this->actingAs($auditorA)->get(route('loan-restructurings.create',$loanA))->assertForbidden();
        $this->actingAs($officerA)->get(route('loan-restructurings.create',$loanA))->assertOk()->assertSee('data-ui="form-shell"',false);
        $this->actingAs($officerA)->get(route('loan-early-settlements.create',$loanA))->assertForbidden();
        $this->actingAs($managerA)->get(route('loan-restructurings.create',$loanB))->assertForbidden();

        $this->actingAs($officerA)->post(route('loan-restructurings.store',$loanA),[
            'new_schedule'=>"2026-09-30 | 67500.00",'reason'=>'Move the remaining obligation to month end.',
        ])->assertRedirect();
        $record=LoanRestructuring::firstOrFail();
        $retiredId=$loanA->installments()->firstOrFail()->id;
        $this->actingAs($inspectorA)->post(route('loan-restructurings.decision',$record),['decision'=>'approve'])->assertRedirect();
        $branchA->business->update(['reminders_enabled'=>true,'reminder_pre_due_days'=>[1],'reminder_due_date_enabled'=>true,'reminder_overdue_enabled'=>true,'reminder_channels'=>['in_system']]);
        app(LoanReminderService::class)->dispatch('2026-08-12');
        $this->assertDatabaseMissing('notification_logs',['loan_installment_id'=>$retiredId]);
    }

    private function fixture(string $tag, string $dueDate='2026-10-01'): array
    {
        $business=Business::firstOrFail();
        $branch=Branch::create(['business_id'=>$business->id,'name'=>'Adjustment '.str($tag)->headline(),'status'=>'active']);
        $customer=Customer::create([
            'branch_id'=>$branch->id,'customer_number'=>'CUS-ADJ-'.strtoupper(substr(md5($tag),0,8)),'identity_mode'=>'without_nin',
            'first_name'=>'Neema','last_name'=>str($tag)->headline()->toString(),'phone'=>'0744'.str_pad((string)$branch->id,6,'0',STR_PAD_LEFT),'status'=>'active',
        ]);
        $product=LoanProduct::create([
            'business_id'=>$business->id,'name'=>'Adjustment '.$tag,'minimum_amount'=>'1000.00','maximum_amount'=>'500000.00','duration'=>1,'duration_unit'=>'months',
            'interest_rate'=>'10.000000','interest_calculation_method'=>'flat','fixed_interest_amount'=>null,'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'=>'5000.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'=>2026,
            'loan_number'=>'LN-ADJ-'.strtoupper(substr(md5($tag),0,8)),'principal'=>'100000.00','interest'=>'10000.00','processing_fee'=>'5000.00','insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>'5000.00','total_repayment'=>'115000.00',
            'principal_outstanding'=>'60000.00','interest_outstanding'=>'5000.00','fees_outstanding'=>'2500.00','penalties_outstanding'=>'0.00','total_outstanding'=>'67500.00','total_paid'=>'47500.00',
            'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0.000000','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.00','interest'=>'10000.00','fees'=>'5000.00','amount_due'=>'115000.00','due_date'=>$dueDate,
            'principal_paid'=>'40000.00','interest_paid'=>'5000.00','fees_paid'=>'2500.00','penalty_due'=>'0.00','penalty_paid'=>'0.00','amount_paid'=>'47500.00','remaining'=>'67500.00','status'=>$dueDate<'2026-08-12'?'overdue':'partial',
        ]);
        CustomerStatementEntry::create([
            'customer_id'=>$customer->id,'loan_id'=>$loan->id,'branch_id'=>$branch->id,'entry_date'=>'2026-07-01','reference'=>'DSB-'.$tag,'type'=>'loan_disbursement','description'=>'Original loan obligation','debit'=>'115000.00','credit'=>'0.00','running_balance'=>'115000.00',
        ]);
        CustomerStatementEntry::create([
            'customer_id'=>$customer->id,'loan_id'=>$loan->id,'branch_id'=>$branch->id,'entry_date'=>'2026-08-01','reference'=>'PAY-'.$tag,'type'=>'loan_payment','description'=>'Payments to date','debit'=>'0.00','credit'=>'47500.00','running_balance'=>'67500.00',
        ]);
        $account=FinancialAccount::create(['branch_id'=>$branch->id,'name'=>'Adjustment Cash '.$tag,'account_type'=>'cash','opening_balance'=>'10000.00','current_balance'=>'10000.00','status'=>'active']);
        $manager=$this->staff('branch-manager',$branch,'adjust_manager_'.$tag);
        $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-ADJ-'.strtoupper(substr(md5($tag),0,8)),
            'item'=>'Television','estimated_value'=>'120000.00','accepted_value'=>'90000.00','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,$product,$customer];
    }

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