<?php

namespace Tests\Feature\Simulation;

use App\Models\Collateral;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\Loan;
use App\Models\LoanDisbursement;
use App\Models\LoanPayment;
use App\Models\LoanPenaltyCharge;
use App\Models\RemotePaymentSubmission;
use App\Models\User;
use App\Services\BranchAccessService;
use App\Support\Simulation\SimulationLoanLifecycleExpectations;
use Database\Seeders\SimulationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SimulationLoanLifecycleTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Storage::fake('local');
        config([
            'simulation.enabled' => true,
            'simulation.phase' => 'loan-lifecycle',
            'simulation.anchor_date' => '2026-08-01',
            'simulation.as_of_date' => SimulationLoanLifecycleExpectations::AS_OF_DATE,
            'simulation.user_password' => 'Simulation@2026',
        ]);
    }

    public function test_lifecycle_scenarios_cover_required_operational_states(): void
    {
        $this->seed(SimulationSeeder::class);

        $loans = $this->systemWide(
            fn () => Loan::query()->where('purpose', 'like', 'SIM-LC-%')->with(['collaterals', 'guarantor'])->get()->keyBy('purpose')
        );

        $this->assertSame(8, $loans->count());
        foreach (SimulationLoanLifecycleExpectations::scenarios() as $scenario) {
            $this->assertArrayHasKey($scenario['purpose'], $loans);
            $this->assertSame($scenario['expected_status'], $loans[$scenario['purpose']]->status, $scenario['purpose'].' status mismatch.');
        }

        $secured = $loans['SIM-LC-SECURED-READY'];
        $this->assertNotNull($secured->guarantor_id);
        $this->assertSame(1, $secured->collaterals->count());
        $this->assertSame('SIM-COL-MZA-001', $secured->collaterals->first()->code);
        $this->assertNotNull($secured->contract_generated_at);
        $this->assertNotNull($secured->contract_signed_at);

        $remote = $this->systemWide(
            fn () => RemotePaymentSubmission::query()->where('transaction_reference', 'like', 'SIM-RMP-%')->get()
        );
        $this->assertSame(3, $remote->count());
        $this->assertSame(1, $remote->where('status', 'pending_verification')->count());
        $this->assertSame(1, $remote->where('status', 'rejected')->count());
        $this->assertSame(1, $remote->where('status', 'approved')->count());
        $this->assertNotNull($remote->firstWhere('status', 'approved')?->loan_payment_id);
    }

    public function test_lifecycle_money_movements_reconcile_to_independent_controls(): void
    {
        $this->seed(SimulationSeeder::class);
        $controls = SimulationLoanLifecycleExpectations::aggregateControls();

        $this->assertSame(
            $controls['loan_disbursements'],
            $this->systemWide(fn () => LoanDisbursement::query()->where('reference', 'like', 'SIM-DSB-%')->count()),
        );
        $this->assertSame(
            $controls['customer_disbursements'],
            $this->money($this->systemWide(fn () => LoanDisbursement::query()->where('reference', 'like', 'SIM-DSB-%')->sum('amount'))),
        );
        $this->assertSame(
            $controls['loan_payments'],
            $this->systemWide(fn () => LoanPayment::query()->where(function ($query): void {
                $query->where('reference', 'like', 'SIM-PAY-%')->orWhere('reference', 'SIM-RMP-APPROVED-001');
            })->count()),
        );
        $this->assertSame(
            $controls['loan_collections'],
            $this->money($this->systemWide(fn () => LoanPayment::query()->where(function ($query): void {
                $query->where('reference', 'like', 'SIM-PAY-%')->orWhere('reference', 'SIM-RMP-APPROVED-001');
            })->sum('amount'))),
        );
        $this->assertSame(
            $controls['cash_processing_fees'],
            $this->money($this->systemWide(fn () => FinancialAccountTransaction::query()->where('type', 'loan_processing_fee')->sum('money_in'))),
        );
        $this->assertSame(
            $controls['penalty_charges'],
            $this->systemWide(fn () => LoanPenaltyCharge::query()->count()),
        );

        foreach (SimulationLoanLifecycleExpectations::expectedAccountBalances() as $accountName => $expected) {
            $actual = $this->systemWide(
                fn () => FinancialAccount::query()->where('name', $accountName)->firstOrFail()->current_balance
            );
            $this->assertSame($expected, $actual, $accountName.' balance mismatch.');
        }

        $accountTotal = $this->systemWide(fn () => FinancialAccount::query()->where('name', 'like', 'SIM %')->sum('current_balance'));
        $this->assertSame($controls['expected_account_total'], $this->money($accountTotal));
    }

    public function test_key_loan_balances_and_arrears_match_controls(): void
    {
        $this->seed(SimulationSeeder::class);
        $scenarios = SimulationLoanLifecycleExpectations::scenarios();

        foreach (['active_partial', 'completed', 'overdue', 'remote'] as $key) {
            $expected = $scenarios[$key];
            $loan = $this->systemWide(fn () => Loan::query()->where('purpose', $expected['purpose'])->firstOrFail());

            $this->assertSame($expected['expected_total_repayment'], $loan->total_repayment, $key.' total repayment mismatch.');
            $this->assertSame($expected['expected_total_outstanding'], $loan->total_outstanding, $key.' outstanding mismatch.');
            if (isset($expected['expected_total_paid'])) {
                $this->assertSame($expected['expected_total_paid'], $loan->total_paid, $key.' paid mismatch.');
            }
        }

        $overdue = $this->systemWide(fn () => Loan::query()->where('purpose', 'SIM-LC-OVERDUE')->firstOrFail());
        $this->assertSame($scenarios['overdue']['expected_penalties_outstanding'], $overdue->penalties_outstanding);
        $this->assertSame($scenarios['overdue']['expected_total_overdue'], $overdue->total_overdue);
        $this->assertSame($scenarios['overdue']['expected_days_overdue'], $overdue->days_overdue);
        $this->assertSame('31-60', $overdue->arrears_bucket);
    }

    public function test_full_simulation_rerun_is_idempotent_and_preserves_transaction_balances(): void
    {
        $this->seed(SimulationSeeder::class);

        $before = $this->systemWide(fn () => [
            'loans' => Loan::query()->where('purpose', 'like', 'SIM-LC-%')->count(),
            'disbursements' => LoanDisbursement::query()->where('reference', 'like', 'SIM-DSB-%')->count(),
            'payments' => LoanPayment::query()->where(function ($query): void {
                $query->where('reference', 'like', 'SIM-PAY-%')->orWhere('reference', 'SIM-RMP-APPROVED-001');
            })->count(),
            'remote' => RemotePaymentSubmission::query()->where('transaction_reference', 'like', 'SIM-RMP-%')->count(),
            'penalties' => LoanPenaltyCharge::query()->count(),
            'account_total' => $this->money(FinancialAccount::query()->where('name', 'like', 'SIM %')->sum('current_balance')),
        ]);

        $this->seed(SimulationSeeder::class);

        $after = $this->systemWide(fn () => [
            'loans' => Loan::query()->where('purpose', 'like', 'SIM-LC-%')->count(),
            'disbursements' => LoanDisbursement::query()->where('reference', 'like', 'SIM-DSB-%')->count(),
            'payments' => LoanPayment::query()->where(function ($query): void {
                $query->where('reference', 'like', 'SIM-PAY-%')->orWhere('reference', 'SIM-RMP-APPROVED-001');
            })->count(),
            'remote' => RemotePaymentSubmission::query()->where('transaction_reference', 'like', 'SIM-RMP-%')->count(),
            'penalties' => LoanPenaltyCharge::query()->count(),
            'account_total' => $this->money(FinancialAccount::query()->where('name', 'like', 'SIM %')->sum('current_balance')),
        ]);

        $this->assertSame($before, $after);
        $this->assertSame(SimulationLoanLifecycleExpectations::aggregateControls()['expected_account_total'], $after['account_total']);
    }

    public function test_simulated_cross_branch_authorization_remains_denied(): void
    {
        $this->seed(SimulationSeeder::class);

        $darCashier = User::query()->where('username', 'sim.dar.cashier')->firstOrFail();
        $mwanzaLoan = $this->systemWide(fn () => Loan::query()->where('purpose', 'SIM-LC-REMOTE')->firstOrFail());
        $access = app(BranchAccessService::class);

        $this->assertFalse($access->canAccessBranch($darCashier, (int) $mwanzaLoan->branch_id));
        $this->assertFalse(Gate::forUser($darCashier)->allows('receivePayment', $mwanzaLoan));
        $this->assertFalse(Gate::forUser($darCashier)->allows('disburse', $mwanzaLoan));

        $this->assertSame(1, $this->systemWide(fn () => Collateral::query()->where('code', 'SIM-COL-MZA-001')->count()));
    }

    private function money(mixed $value): string
    {
        return number_format((float) $value, 2, '.', '');
    }
}
