<?php

namespace Tests\Feature;

use App\Models\AccountingEntry;
use App\Models\AuditLog;
use App\Models\Branch;
use App\Models\Business;
use App\Models\CashReconciliation;
use App\Models\Collateral;
use App\Models\CollateralCategory;
use App\Models\Customer;
use App\Models\CustomerStatementEntry;
use App\Models\DailyClosing;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanProduct;
use App\Models\LoanRestructuring;
use App\Models\LoanTopUp;
use App\Models\LoanWriteOff;
use App\Models\LoanWriteOffRecovery;
use App\Models\PayrollEntry;
use App\Models\PayrollPeriod;
use App\Models\RemotePaymentSubmission;
use App\Models\ReminderNotificationLog;
use App\Models\Role;
use App\Models\User;
use App\Services\FinancialReportService;
use App\Services\ManagementDashboardService;
use App\Services\MoneyMath;
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 CompleteIntegrationWorkflowTest extends TestCase
{
    use RefreshDatabase;

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

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

    public function test_complete_microfinance_system_operates_and_reconciles_end_to_end(): void
    {
        $business = Business::firstOrFail();
        $business->update([
            'name' => 'Step 26 Integration Microfinance',
            'loan_approval_mode' => 'approval_required',
            'maker_checker_enabled' => true,
            'require_signed_contract_before_disbursement' => true,
            'top_up_eligibility_percentage' => '70.00',
            'reminders_enabled' => true,
            'reminder_pre_due_days' => [1],
            'reminder_due_date_enabled' => false,
            'reminder_overdue_enabled' => true,
            'reminder_overdue_frequency' => 'daily',
            'reminder_channels' => ['in_system'],
        ]);

        $admin = User::where('username', 'admin')->firstOrFail();
        $branchA = Branch::create(['business_id'=>$business->id,'name'=>'Step 26 Dodoma','location'=>'Dodoma','status'=>'active']);
        $branchB = Branch::create(['business_id'=>$business->id,'name'=>'Step 26 Arusha','location'=>'Arusha','status'=>'active']);
        $officer = $this->staff('loan-officer', $branchA, 'step26_officer');
        $inspector = $this->staff('loan-inspector', $branchA, 'step26_inspector');
        $cashier = $this->staff('cashier', $branchA, 'step26_cashier');
        $accountant = $this->staff('accountant', $branchA, 'step26_accountant');
        $manager = $this->staff('branch-manager', $branchA, 'step26_manager');
        $checker = $this->staff('branch-manager', $branchA, 'step26_checker');
        $hr = $this->staff('hr-payroll-officer', $branchA, 'step26_hr');
        $otherManager = $this->staff('branch-manager', $branchB, 'step26_other_manager');
        $this->assertSame(2, Branch::whereIn('id', [$branchA->id, $branchB->id])->count());
        $this->assertGreaterThanOrEqual(6, Role::count());

        foreach ([
            ['Step 26 Main Cash','cash','800000.00'],
            ['Step 26 M-Pesa','mobile_money','200000.00'],
            ['Step 26 Bank','bank','400000.00'],
        ] as [$name,$type,$opening]) {
            $this->actingAs($admin)->post(route('accounts.store'), [
                'branch_id'=>$branchA->id,'name'=>$name,'account_type'=>$type,'opening_balance'=>$opening,
                'status'=>'active','reference_details'=>'Step 26 integration account',
            ])->assertRedirect();
        }
        $cash = FinancialAccount::where('branch_id',$branchA->id)->where('account_type','cash')->firstOrFail();
        $mpesa = FinancialAccount::where('branch_id',$branchA->id)->where('account_type','mobile_money')->firstOrFail();
        $bank = FinancialAccount::where('branch_id',$branchA->id)->where('account_type','bank')->firstOrFail();
        $this->actingAs($admin)->post(route('capital-injections.store'), [
            'account_id'=>$bank->id,'amount'=>'800000.00','date'=>'2026-08-11','reference'=>'STEP26-CAPITAL',
            'notes'=>'Owner capital for complete integration test.',
        ])->assertRedirect();
        $this->assertSame('1200000.00', $bank->fresh()->current_balance);

        CarbonImmutable::setTestNow('2026-08-12 08:00:00');
        $this->actingAs($officer)->post(route('customers.store'), [
            'branch_id'=>$branchA->id,'identity_mode'=>'with_nin','first_name'=>'Amina','last_name'=>'Mashauri',
            'nin'=>'19900101-12345-00001-00','phone'=>'0712345626','status'=>'active','region'=>'Dodoma','district'=>'Dodoma City',
            'next_of_kin_name'=>'Juma Mashauri','next_of_kin_phone'=>'0755000026',
        ])->assertRedirect();
        $customer = Customer::where('phone','0712345626')->firstOrFail();
        $this->actingAs($officer)->post(route('customers.documents.store',$customer), [
            'document_type'=>'National ID','document'=>UploadedFile::fake()->create('step26-national-id.pdf',80,'application/pdf'),
        ])->assertRedirect();
        Storage::disk('local')->assertExists($customer->documents()->firstOrFail()->file_path);

        $category = CollateralCategory::where('business_id',$business->id)->firstOrFail();
        $this->actingAs($admin)->post(route('collateral.store'), [
            'customer_id'=>$customer->id,'branch_id'=>$branchA->id,'category_id'=>$category->id,'item'=>'55-inch Smart TV',
            'brand'=>'Samsung','serial_number'=>'STEP26-TV-001','estimated_value'=>'900000.00','accepted_value'=>'750000.00',
            'condition'=>'Good','received_at'=>'2026-08-12','store_room'=>'Vault A','shelf'=>'A-01','status'=>'pending_inspection',
            'photos'=>[UploadedFile::fake()->image('step26-tv.jpg')],
        ])->assertRedirect();
        $collateral = Collateral::where('serial_number','STEP26-TV-001')->firstOrFail();
        $this->actingAs($admin)->post(route('collateral.move',$collateral), [
            'status'=>'accepted','store_room'=>'Vault A','shelf'=>'A-01','notes'=>'Inspection completed.',
        ])->assertRedirect();

        $this->actingAs($admin)->post(route('loan-products.store'), [
            'name'=>'Step 26 Business Loan','minimum_amount'=>'100000.00','maximum_amount'=>'1000000.00','duration'=>2,'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'=>'fixed','late_payment_penalty_value'=>'5000.000000',
            'processing_fee_type'=>'fixed','processing_fee_value'=>'5000.000000','insurance_fee_type'=>'fixed','insurance_fee_value'=>'0.000000',
            'other_charges'=>'1000.00','collateral_required'=>'1','guarantor_required'=>'0','status'=>'active',
        ])->assertRedirect();
        $product = LoanProduct::where('name','Step 26 Business Loan')->firstOrFail();

        $this->actingAs($officer)->post(route('loans.store'), [
            'branch_id'=>$branchA->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,'principal'=>'500000.00',
            'purpose'=>'Purchase wholesale inventory','schedule_start_date'=>'2026-08-13','collateral_ids'=>[$collateral->id],
            'supporting_documents'=>[UploadedFile::fake()->create('step26-application.pdf',50,'application/pdf')],
        ])->assertRedirect();
        $loan = Loan::where('customer_id',$customer->id)->where('loan_product_id',$product->id)->firstOrFail();
        $this->assertSame('draft',$loan->status);
        $this->assertSame('556000.00',$loan->total_repayment);
        $this->assertSame(2,$loan->installments()->count());

        $contract = $this->actingAs($officer)->post(route('loans.contract',$loan));
        $contract->assertOk()->assertHeader('content-type','application/pdf');
        $this->assertStringStartsWith('%PDF-1.4',$contract->getContent());
        $this->actingAs($officer)->post(route('loans.signed-contract',$loan), [
            'signed_contract'=>UploadedFile::fake()->create('step26-signed-agreement.pdf',90,'application/pdf'),
        ])->assertRedirect();
        $this->actingAs($officer)->post(route('loans.transition',$loan), [
            'status'=>'under_review','notes'=>'KYC, collateral and signed agreement complete.',
        ])->assertRedirect();
        $this->actingAs($inspector)->post(route('loans.approvals.decide',$loan), [
            'decision'=>'approve','comments'=>'Identity, capacity and collateral verified.',
        ])->assertRedirect();
        $this->assertSame('ready_for_disbursement',$loan->fresh()->status);

        $this->actingAs($cashier)->post(route('loan-disbursements.store',$loan), [
            'amount'=>'500000.00','financial_account_id'=>$cash->id,'date'=>'2026-08-12','reference'=>'STEP26-DSB-001',
        ])->assertRedirect();
        $loan->refresh();
        $this->assertSame('active',$loan->status);
        $this->assertSame('300000.00',$cash->fresh()->current_balance);
        $this->assertTrue($loan->installments->every(fn($row)=>$row->status==='upcoming'));

        $loan->installments()->orderBy('installment_number')->firstOrFail()->update(['due_date'=>'2026-08-13']);
        $this->artisan('notifications:dispatch-reminders',['--date'=>'2026-08-12'])->assertSuccessful();
        $reminder = ReminderNotificationLog::where('loan_id',$loan->id)->firstOrFail();
        $this->assertSame('pre_due',$reminder->reminder_type);
        $this->assertSame('delivered',$reminder->status);
        $cashBefore = $cash->fresh()->current_balance;
        $this->actingAs($cashier)->post(route('loan-payments.store',$loan), [
            'amount'=>'50000.00','date'=>'2026-08-12','method'=>'cash','financial_account_id'=>$cash->id,
            'reference'=>'STEP26-CASH-PAY-001','notes'=>'Partial branch repayment.',
        ])->assertRedirect();
        $partial = LoanPayment::where('reference','STEP26-CASH-PAY-001')->firstOrFail();
        $money = app(MoneyMath::class);
        $this->assertGreaterThan(0,(float)$partial->principal_amount);
        $this->assertGreaterThan(0,(float)$partial->interest_amount);
        $this->assertGreaterThan(0,(float)$partial->fees_amount);
        $this->assertSame(5000000,
            $money->toMinor($partial->principal_amount)+$money->toMinor($partial->interest_amount)
            +$money->toMinor($partial->fees_amount)+$money->toMinor($partial->penalties_amount));
        $this->assertSame($money->toMinor($cashBefore)+5000000,$money->toMinor($cash->fresh()->current_balance));
        $this->actingAs($cashier)->get(route('loan-payments.receipt',$partial))->assertOk()->assertSee($partial->receipt_number);

        $mpesaBefore = $mpesa->fresh()->current_balance;
        $this->post(route('remote-payments.public.store'), [
            'loan_number'=>$loan->loan_number,'customer_phone'=>$customer->phone,'amount'=>'40000.00','payment_channel'=>'mpesa',
            'transaction_reference'=>'STEP26-MPESA-001','proof'=>UploadedFile::fake()->create('step26-mpesa-proof.pdf',40,'application/pdf'),
        ])->assertRedirect();
        $remote = RemotePaymentSubmission::where('transaction_reference','STEP26-MPESA-001')->firstOrFail();
        $this->actingAs($cashier)->post(route('remote-payments.approve',$remote), [
            'financial_account_id'=>$mpesa->id,'payment_date'=>'2026-08-12','verification_notes'=>'Provider receipt verified.',
        ])->assertRedirect();
        $this->assertSame('approved',$remote->fresh()->status);
        $this->assertSame($money->toMinor($mpesaBefore)+4000000,$money->toMinor($mpesa->fresh()->current_balance));

        $overdue = $loan->fresh()->installments()->where('remaining','>',0)->orderBy('installment_number')->firstOrFail();
        $overdue->update(['due_date'=>'2026-08-01']);
        $this->artisan('loans:update-overdue',['--date'=>'2026-08-12'])->assertSuccessful();
        $this->assertGreaterThan(0,$loan->fresh()->days_overdue);
        $this->assertSame('5000.00',$loan->fresh()->penalties_outstanding);
        $this->actingAs($manager)->post(route('arrears.follow-ups.store',$loan), [
            'action_type'=>'promise_to_pay','promise_date'=>'2026-08-15','notes'=>'Customer promised settlement.',
        ])->assertRedirect();

        $bankBefore = $bank->fresh()->current_balance;
        $this->actingAs($manager)->post(route('loan-early-settlements.store',$loan), [
            'financial_account_id'=>$bank->id,'date'=>'2026-08-12','settlement_adjustment'=>'0.00','reference'=>'STEP26-EARLY-001',
        ])->assertRedirect();
        $this->assertSame('fully_paid',$loan->fresh()->status);
        $this->assertSame('0.00',$loan->fresh()->total_outstanding);
        $this->assertSame('ready_for_release',$collateral->fresh()->status);
        $this->assertGreaterThan($money->toMinor($bankBefore),$money->toMinor($bank->fresh()->current_balance));
        $this->actingAs($admin)->post(route('collateral.move',$collateral), [
            'status'=>'released','store_room'=>null,'shelf'=>null,'notes'=>'Released after full settlement.',
        ])->assertRedirect();
        $this->actingAs($admin)->get(route('collateral.release-acknowledgement',$collateral))
            ->assertOk()->assertSee('Collateral Release Acknowledgement');

        $topOldProduct = $this->simpleProduct($business,'Step 26 Prior Loan','flat','0.000000');
        $topProduct = $this->simpleProduct($business,'Step 26 Top-up Product','fixed_amount','0.000000','10000.00');
        $topOld = $this->siblingLoan($branchA,$customer,$topOldProduct,'STEP26-TOP-OLD','100000.00','0.00','0.00','20000.00','80000.00','2026-12-01','20000.00');
        $this->actingAs($manager)->post(route('loan-top-ups.store',$topOld), [
            'loan_product_id'=>$topProduct->id,'principal'=>'120000.00','purpose'=>'Increase stock','schedule_start_date'=>'2026-09-01',
        ])->assertRedirect();
        $replacement = Loan::where('top_up_source_loan_id',$topOld->id)->firstOrFail();
        $this->markReplacementReady($replacement,$checker);
        $this->actingAs($manager)->post(route('loan-top-ups.execute',$replacement), [
            'financial_account_id'=>$bank->id,'date'=>'2026-08-12','reference'=>'STEP26-TOPUP-001',
        ])->assertRedirect()->assertSessionHasNoErrors();
        $topUp = LoanTopUp::where('new_loan_id',$replacement->id)->firstOrFail();
        $this->assertSame('100000.00',$topUp->cash_given_to_customer);
        $this->assertSame('closed_through_top_up',$topOld->fresh()->status);
        $this->assertSame('active',$replacement->fresh()->status);

        $adjustProduct = $this->simpleProduct($business,'Step 26 Adjustment Product','flat','10.000000',null,'5000.00');
        $restructureLoan = $this->siblingLoan($branchA,$customer,$adjustProduct,'STEP26-RESTRUCTURE',
            '100000.00','10000.00','5000.00','67500.00','47500.00','2026-10-01','60000.00','5000.00','2500.00');
        $this->actingAs($officer)->post(route('loan-restructurings.store',$restructureLoan), [
            'new_schedule'=>"2026-09-30 | 30000.00\n2026-10-31 | 37500.00",'reason'=>'Cash flow moved to month end.',
            'documents'=>[UploadedFile::fake()->create('step26-restructure.pdf',60,'application/pdf')],
        ])->assertRedirect();
        $restructuring = LoanRestructuring::where('loan_id',$restructureLoan->id)->firstOrFail();
        $this->actingAs($inspector)->post(route('loan-restructurings.decision',$restructuring), [
            'decision'=>'approve','comments'=>'New dates verified.',
        ])->assertRedirect();
        $this->assertSame('approved',$restructuring->fresh()->status);
        $this->assertSame(2,$restructureLoan->fresh()->current_schedule_version);

        $writeOffLoan = $this->siblingLoan($branchA,$customer,$adjustProduct,'STEP26-WRITEOFF',
            '100000.00','10000.00','5000.00','67500.00','47500.00','2026-08-01','60000.00','5000.00','2500.00');
        $this->actingAs($manager)->post(route('loan-write-offs.store',$writeOffLoan), [
            'write_off_amount'=>'67500.00','reason'=>'Documented collection attempts exhausted.',
            'documents'=>[UploadedFile::fake()->create('step26-writeoff.pdf',70,'application/pdf')],
        ])->assertRedirect();
        $writeOff = LoanWriteOff::where('loan_id',$writeOffLoan->id)->firstOrFail();
        $this->actingAs($checker)->post(route('loan-write-offs.decision',$writeOff), [
            'decision'=>'approve','comments'=>'Evidence independently reviewed.',
        ])->assertRedirect();
        $this->assertSame('written_off',$writeOffLoan->fresh()->status);
        $cashBefore = $cash->fresh()->current_balance;
        $this->actingAs($cashier)->post(route('loan-write-offs.recoveries.store',$writeOff), [
            'financial_account_id'=>$cash->id,'amount'=>'10000.00','date'=>'2026-08-12','method'=>'cash','reference'=>'STEP26-WOR-001',
        ])->assertRedirect();
        $recovery = LoanWriteOffRecovery::where('loan_write_off_id',$writeOff->id)->firstOrFail();
        $this->assertSame('10000.00',$recovery->amount);
        $this->assertSame($money->toMinor($cashBefore)+1000000,$money->toMinor($cash->fresh()->current_balance));

        $expenseCategory = ExpenseCategory::where('business_id',$business->id)->where('name','Rent')->firstOrFail();
        $this->actingAs($accountant)->post(route('expenses.store'), [
            'branch_id'=>$branchA->id,'expense_category_id'=>$expenseCategory->id,'financial_account_id'=>$bank->id,
            'description'=>'Step 26 office rent','amount'=>'25000.00','expense_date'=>'2026-08-12','supplier'=>'Step 26 Landlord',
            'receipt'=>UploadedFile::fake()->create('step26-rent.pdf',50,'application/pdf'),
        ])->assertRedirect();
        $expense = Expense::where('description','Step 26 office rent')->firstOrFail();
        $this->actingAs($manager)->post(route('expenses.decision',$expense), [
            'decision'=>'approve','comments'=>'Receipt and budget verified.',
        ])->assertRedirect();
        $this->assertSame('approved',$expense->fresh()->status);

        $employee = User::factory()->create([
            'name'=>'Step 26 Operations Employee','username'=>'step26_employee','position'=>'Operations Officer','status'=>'active','primary_branch_id'=>$branchA->id,
        ]);
        $employee->branches()->attach($branchA->id,['is_primary'=>true]);
        $this->actingAs($hr)->put(route('payroll.compensations.update',$employee), [
            'basic_salary'=>'1000.00','allowances'=>'100.00','bonus'=>'50.00','commission'=>'25.00','overtime'=>'75.00',
            'deductions'=>'30.00','advances'=>'20.00','other_deductions'=>'10.00','effective_from'=>'2026-08-01','status'=>'active',
        ])->assertRedirect();
        $this->actingAs($hr)->post(route('payroll.store'), [
            'branch_id'=>$branchA->id,'name'=>'Step 26 August Payroll','start_date'=>'2026-08-01','end_date'=>'2026-08-12','pay_date'=>'2026-08-12',
        ])->assertRedirect();
        $payroll = PayrollPeriod::where('name','Step 26 August Payroll')->firstOrFail();
        $this->actingAs($manager)->post(route('payroll.decision',$payroll), ['decision'=>'approve','comments'=>'Payroll verified.'])->assertRedirect();
        $this->actingAs($accountant)->post(route('payroll.pay',$payroll), ['financial_account_id'=>$bank->id])->assertRedirect();
        $payrollEntry = PayrollEntry::where('payroll_period_id',$payroll->id)->where('user_id',$employee->id)->firstOrFail();
        $this->assertSame('paid',$payroll->fresh()->status);
        $this->assertSame('1190.00',$payrollEntry->fresh()->net);

        $mpesaBefore = $mpesa->fresh()->current_balance;
        $bankBefore = $bank->fresh()->current_balance;
        $this->actingAs($accountant)->post(route('account-transfers.store'), [
            'source_account_id'=>$mpesa->id,'destination_account_id'=>$bank->id,'amount'=>'10000.00','date'=>'2026-08-12','reference'=>'STEP26-TRANSFER-001',
        ])->assertRedirect();
        $this->assertSame($money->toMinor($mpesaBefore)-1000000,$money->toMinor($mpesa->fresh()->current_balance));
        $this->assertSame($money->toMinor($bankBefore)+1000000,$money->toMinor($bank->fresh()->current_balance));

        CarbonImmutable::setTestNow('2026-08-12 17:30:00');
        $physicalCash = $cash->fresh()->current_balance;
        $this->actingAs($accountant)->post(route('cash-closing.reconcile'), [
            'branch_id'=>$branchA->id,'financial_account_id'=>$cash->id,'date'=>'2026-08-12','physical_cash'=>$physicalCash,
        ])->assertRedirect();
        $reconciliation = CashReconciliation::where('branch_id',$branchA->id)->where('status','active')->firstOrFail();
        $this->assertSame('0.00',$reconciliation->variance);
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), ['branch_id'=>$branchA->id,'date'=>'2026-08-12'])->assertRedirect();
        $closing = DailyClosing::where('branch_id',$branchA->id)->whereDate('closing_date','2026-08-12')->firstOrFail();
        $this->assertSame($closing->expected_cash,$closing->actual_cash);
        $this->actingAs($manager)->post(route('cash-closing.confirm',$closing), [
            'manager_comments'=>'Step 26 ledger and physical cash reconciled.',
        ])->assertRedirect();
        $this->assertSame('confirmed',$closing->fresh()->status);

        $reports = app(FinancialReportService::class);
        $pnl = $reports->generate('profit-loss',[$branchA->id],'2026-08-12','2026-08-12');
        $cashFlow = $reports->generate('cash-flow',[$branchA->id],'2026-08-12','2026-08-12');
        $collections = $reports->generate('collections',[$branchA->id],'2026-08-12','2026-08-12');
        $disbursements = $reports->generate('disbursements',[$branchA->id],'2026-08-12','2026-08-12');
        $expenseReport = $reports->generate('expenses',[$branchA->id],'2026-08-12','2026-08-12');
        $payrollReport = $reports->generate('payroll',[$branchA->id],'2026-08-12','2026-08-12');
        $entries = AccountingEntry::where('branch_id',$branchA->id)->whereDate('entry_date','2026-08-12')->get();
        $incomeMinor = $entries->whereIn('category',['interest','fees','penalties','write_off_recoveries','service_income','other_income','settlement_adjustment'])
            ->sum(fn($e)=>$money->toMinor((string)$e->credit));
        $expenseMinor = $entries->whereIn('category',['expenses','payroll','write_offs','settlement_adjustment','other_expenses'])
            ->sum(fn($e)=>$money->toMinor((string)$e->debit));
        $principalMinor = $entries->where('category','principal')->sum(fn($e)=>$money->toMinor((string)$e->credit));
        $this->assertSame($money->fromMinor($incomeMinor),$pnl['total_income']);
        $this->assertSame($money->fromMinor($expenseMinor),$pnl['total_expenses']);
        $this->assertSame($money->fromSignedMinor($incomeMinor-$expenseMinor),$pnl['net_profit']);
        $this->assertSame($money->fromMinor($principalMinor),$pnl['principal_activity_excluded']);
        $txs = FinancialAccountTransaction::where('branch_id',$branchA->id)->whereDate('transaction_date','2026-08-12')->get();
        $inMinor = $txs->sum(fn($t)=>$money->toMinor((string)$t->money_in));
        $outMinor = $txs->sum(fn($t)=>$money->toMinor((string)$t->money_out));
        $this->assertSame($money->fromMinor($inMinor),$cashFlow['money_in']);
        $this->assertSame($money->fromMinor($outMinor),$cashFlow['money_out']);
        $this->assertSame($money->fromSignedMinor($inMinor-$outMinor),$cashFlow['net_cash_flow']);
        $this->assertSame('25000.00',$expenseReport['total_expenses']);
        $this->assertSame('1190.00',$payrollReport['total_net']);

        $dashboard = app(ManagementDashboardService::class)->generate([$branchA->id],'2026-08-12','2026-08-12');
        $this->assertSame($collections['total_collections'],$dashboard['metrics']['collections']);
        $this->assertSame($disbursements['total_disbursements'],$dashboard['metrics']['disbursements']);
        $this->assertSame($expenseReport['total_expenses'],$dashboard['metrics']['expenses']);
        $this->actingAs($manager)->get(route('dashboard',[
            'period'=>'custom','from'=>'2026-08-12','to'=>'2026-08-12','branch_ids'=>[$branchA->id],
        ]))->assertOk()->assertSee('Management Dashboard')->assertSee($branchA->name);
        $this->actingAs($manager)->get(route('reports.index',[
            'report'=>'profit-loss','branch_ids'=>[$branchA->id],'from'=>'2026-08-12','to'=>'2026-08-12',
        ]))->assertOk()->assertSee('Profit &amp; Loss',false);

        $this->actingAs($otherManager)->get(route('customers.show',$customer))->assertForbidden();
        $this->actingAs($otherManager)->get(route('loans.show',$loan))->assertForbidden();
        $this->actingAs($otherManager)->get(route('accounts.show',$cash))->assertForbidden();
        $this->actingAs($otherManager)->get(route('reports.index',[
            'report'=>'profit-loss','branch_ids'=>[$branchA->id],'from'=>'2026-08-12','to'=>'2026-08-12',
        ]))->assertForbidden();
        $this->actingAs($otherManager)->get(route('dashboard',['branch_ids'=>[$branchA->id]]))->assertForbidden();

        $this->assertDatabaseHas('audit_logs', [
            'action'=>'loan_product.created',
            'auditable_id'=>$product->id,
        ]);
        foreach ([
            'customer.created','collateral.created','collateral.custody_changed','loan_application.created',
            'loan_approval.approve','loan.disbursed','loan.payment_received','remote_payment.approved','collections.follow_up_created',
            'loan_top_up.executed','loan_restructuring.approved','expense.created','financial_transfer.posted','daily_closing.confirmed',
        ] as $action) {
            $this->assertTrue(AuditLog::where('action',$action)->where('branch_id',$branchA->id)->exists(),"Missing audit action {$action}");
        }
        $this->assertDatabaseHas('financial_account_transactions',['branch_id'=>$branchA->id,'type'=>'loan_write_off_recovery']);
        $this->assertDatabaseHas('accounting_entries',['branch_id'=>$branchA->id,'category'=>'write_offs']);
        $this->assertDatabaseHas('accounting_entries',['branch_id'=>$branchA->id,'category'=>'write_off_recoveries']);
    }

    private function staff(string $roleName, 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',$roleName)->firstOrFail());
        $user->branches()->attach($branch->id,['is_primary'=>true]);
        return $user;
    }

    private function simpleProduct(Business $business, string $name, string $method, string $rate, ?string $fixedInterest=null, string $fee='0.00'): 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'=>$fee,
            'insurance_fee_type'=>'fixed','insurance_fee_value'=>'0.000000','other_charges'=>'0.00','collateral_required'=>false,'guarantor_required'=>false,'status'=>'active',
        ]);
    }

    private function siblingLoan(
        Branch $branch, Customer $customer, LoanProduct $product, string $tag,
        string $principal, string $originalInterest, string $originalFees,
        string $totalOutstanding, string $totalPaid, string $dueDate,
        string $principalOutstanding, string $interestOutstanding='0.00', string $feesOutstanding='0.00'
    ): Loan {
        $money = app(MoneyMath::class);
        $totalRepayment = $money->fromMinor($money->toMinor($principal)+$money->toMinor($originalInterest)+$money->toMinor($originalFees));
        $principalPaid = $money->fromMinor(max(0,$money->toMinor($principal)-$money->toMinor($principalOutstanding)));
        $interestPaid = $money->fromMinor(max(0,$money->toMinor($originalInterest)-$money->toMinor($interestOutstanding)));
        $feesPaid = $money->fromMinor(max(0,$money->toMinor($originalFees)-$money->toMinor($feesOutstanding)));
        $loan = Loan::create([
            'branch_id'=>$branch->id,'customer_id'=>$customer->id,'loan_product_id'=>$product->id,'sequence_number'=>Loan::where('branch_id',$branch->id)->count()+100,
            'sequence_year'=>2026,'loan_number'=>'LN-'.$tag,'principal'=>$principal,'interest'=>$originalInterest,'processing_fee'=>$originalFees,
            'insurance_fee'=>'0.00','other_charges'=>'0.00','fees'=>$originalFees,'total_repayment'=>$totalRepayment,
            'principal_outstanding'=>$principalOutstanding,'interest_outstanding'=>$interestOutstanding,'fees_outstanding'=>$feesOutstanding,
            'penalties_outstanding'=>'0.00','total_outstanding'=>$totalOutstanding,'total_paid'=>$totalPaid,
            'late_payment_penalty_type'=>'none','late_payment_penalty_value'=>'0.000000','purpose'=>'Step 26 sibling lifecycle',
            'schedule_start_date'=>'2026-06-01','status'=>'active','disbursed_at'=>'2026-06-01','activated_at'=>'2026-06-01','schedule_activated_at'=>'2026-06-01',
            'current_schedule_version'=>1,
        ]);
        $loan->allInstallments()->create([
            'schedule_version'=>1,'is_active'=>true,'installment_number'=>1,'principal'=>$principal,'interest'=>$originalInterest,'fees'=>$originalFees,
            'amount_due'=>$totalRepayment,'due_date'=>$dueDate,'principal_paid'=>$principalPaid,'interest_paid'=>$interestPaid,'fees_paid'=>$feesPaid,
            'penalty_due'=>'0.00','penalty_paid'=>'0.00','amount_paid'=>$totalPaid,'remaining'=>$totalOutstanding,'status'=>'partial',
        ]);
        CustomerStatementEntry::create([
            'customer_id'=>$customer->id,'loan_id'=>$loan->id,'branch_id'=>$branch->id,'entry_date'=>'2026-06-01','reference'=>'DSB-'.$tag,
            'type'=>'loan_disbursement','description'=>'Historical obligation','debit'=>$totalRepayment,'credit'=>'0.00','running_balance'=>$totalRepayment,
        ]);
        if ($money->toMinor($totalPaid)>0) {
            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'=>'Prior repayments','debit'=>'0.00','credit'=>$totalPaid,'running_balance'=>$totalOutstanding,
            ]);
        }
        return $loan;
    }

    private function markReplacementReady(Loan $loan, User $user): void
    {
        $loan->documents()->create([
            'kind'=>'signed_contract','document_type'=>'loan_contract','original_name'=>'step26-topup-signed.pdf',
            'file_path'=>'tests/step26-topup-'.$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,
        ]);
    }
}