<?php

namespace Tests\Feature\Simulation;

use App\Models\Branch;
use App\Models\User;
use App\Services\AdvancedFinancialReportService;
use App\Services\FinancialReportService;
use App\Services\LoanBranchReportService;
use App\Services\ManagementDashboardService;
use App\Services\MoneyMath;
use App\Support\Simulation\SimulationReportReconciliationExpectations;
use Carbon\CarbonImmutable;
use Database\Seeders\SimulationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SimulationReportReconciliationTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Storage::fake('local');
        CarbonImmutable::setTestNow('2026-08-15 12:00:00');
        config([
            'simulation.enabled' => true,
            'simulation.phase' => 'report-reconciliation',
            'simulation.anchor_date' => '2026-08-01',
            'simulation.as_of_date' => SimulationReportReconciliationExpectations::AS_OF_DATE,
            'simulation.user_password' => 'Simulation@2026',
        ]);
    }

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

    public function test_financial_reports_reconcile_to_independent_cash_and_profit_controls(): void
    {
        $this->seed(SimulationSeeder::class);
        $branchIds = $this->simulationBranchIds();
        $financial = app(FinancialReportService::class);
        $money = app(MoneyMath::class);
        $cash = SimulationReportReconciliationExpectations::financialControls();
        $pnlControl = SimulationReportReconciliationExpectations::profitLossControls();

        $collections = $financial->generate('collections', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $disbursements = $financial->generate('disbursements', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $expenses = $financial->generate('expenses', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $payroll = $financial->generate('payroll', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $pnl = $financial->generate('profit-loss', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);

        $this->assertSame($cash['collections'], $collections['total_collections']);
        $this->assertSame($cash['disbursements'], $disbursements['total_disbursements']);
        $this->assertSame($cash['approved_expenses'], $expenses['total_expenses']);
        $this->assertSame($cash['payroll'], $payroll['total_net']);
        $this->assertSame($pnlControl['total_income'], $pnl['total_income']);
        $this->assertSame($pnlControl['total_expenses'], $pnl['total_expenses']);
        $this->assertSame($pnlControl['net_profit'], $pnl['net_profit']);

        $income = collect($pnl['income_rows'])->keyBy('category');
        $expense = collect($pnl['expense_rows'])->keyBy('category');
        $this->assertSame($pnlControl['interest_income'], $income['interest']['amount']);
        $this->assertSame($pnlControl['fee_income'], $income['fees']['amount']);
        $this->assertSame($pnlControl['write_off_recovery_income'], $income['write_off_recoveries']['amount']);
        $this->assertSame($pnlControl['operating_expenses'], $expense['expenses']['amount']);
        $this->assertSame($pnlControl['payroll_expense'], $expense['payroll']['amount']);
        $this->assertSame($pnlControl['write_off_expense'], $expense['write_offs']['amount']);
        $this->assertSame($pnlControl['depreciation_expense'], $expense['depreciation_expense']['amount']);
        $this->assertSame($pnlControl['asset_maintenance_expense'], $expense['asset_maintenance']['amount']);
        $this->assertSame($pnlControl['settlement_adjustment_expense'], $expense['other_expenses']['amount']);

        $cashFlow = $financial->generate('cash-flow', $branchIds, SimulationReportReconciliationExpectations::ALL_TIME_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $capital = $financial->generate('capital', $branchIds, SimulationReportReconciliationExpectations::ALL_TIME_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $statement = $financial->generate('account-statement', $branchIds, SimulationReportReconciliationExpectations::ALL_TIME_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);

        $this->assertSame($cash['all_time_money_in'], $cashFlow['money_in']);
        $this->assertSame($cash['all_time_money_out'], $cashFlow['money_out']);
        $this->assertSame($cash['all_time_net_cash_flow'], $cashFlow['net_cash_flow']);
        $this->assertSame($cash['collections'], $cashFlow['collections_in']);
        $this->assertSame($cash['cash_flow_operating_out'], $cashFlow['operating_out']);
        $this->assertSame($cash['cash_flow_loan_disbursements_out'], $cashFlow['loan_disbursements_out']);
        $this->assertSame($cash['capital'], $cashFlow['capital_in']);
        $this->assertSame($cash['cash_flow_net_transfers'], $cashFlow['net_transfers']);
        $this->assertSame($cash['capital'], $capital['total_capital']);
        $this->assertSame($cash['opening_funds'], $capital['opening_funds']);
        $this->assertSame($cash['capital_injections'], $capital['capital_injections']);

        $closingMinor = collect($statement['summaries'])->sum(fn (array $row): int => $money->toSignedMinor((string) $row['closing_balance']));
        $this->assertSame($cash['account_balance'], $money->fromSignedMinor($closingMinor));
    }

    public function test_advanced_and_loan_reports_reconcile_portfolio_and_ledger_controls(): void
    {
        $this->seed(SimulationSeeder::class);
        $branchIds = $this->simulationBranchIds();
        $advanced = app(AdvancedFinancialReportService::class);
        $loans = app(LoanBranchReportService::class);
        $portfolio = SimulationReportReconciliationExpectations::portfolioControls();
        $pnl = SimulationReportReconciliationExpectations::profitLossControls();

        $income = $advanced->generate('income-statement', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $trialBalance = $advanced->generate('trial-balance', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $position = $advanced->generate('statement-of-financial-position', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);
        $portfolioReport = $advanced->generate('portfolio-summary', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO);

        $this->assertSame($pnl['total_income'], $income['total_income']);
        $this->assertSame($pnl['total_expenses'], $income['total_expenses']);
        $this->assertSame($pnl['net_profit'], $income['net_profit']);
        $this->assertTrue($trialBalance['balanced']);
        $this->assertSame('0.00', $trialBalance['difference']);
        $this->assertTrue($position['balanced']);
        $this->assertSame('0.00', $position['difference']);
        $this->assertSame($pnl['statement_assets'], $position['total_assets']);
        $this->assertSame($pnl['statement_equity'], $position['total_equity']);

        $this->assertSame($portfolio['active_loans'], $portfolioReport['active_loans']);
        $this->assertSame($portfolio['overdue_loans'], $portfolioReport['overdue_loans']);
        $this->assertSame($portfolio['written_off_loans'], $portfolioReport['written_off_loans']);
        $this->assertSame($portfolio['principal_outstanding'], $portfolioReport['principal_outstanding']);
        $this->assertSame($portfolio['interest_outstanding'], $portfolioReport['interest_outstanding']);
        $this->assertSame($portfolio['fees_outstanding'], $portfolioReport['fees_outstanding']);
        $this->assertSame($portfolio['penalties_outstanding'], $portfolioReport['penalties_outstanding']);
        $this->assertSame($portfolio['total_outstanding'], $portfolioReport['total_outstanding']);
        $this->assertSame($portfolio['overdue_total'], $portfolioReport['total_overdue']);
        $this->assertSame($portfolio['portfolio_at_risk_percent'], $portfolioReport['portfolio_at_risk_percent']);

        $active = $this->systemWide(fn () => $loans->generate('active-loan-portfolio', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $principal = $this->systemWide(fn () => $loans->generate('outstanding-principal', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $interest = $this->systemWide(fn () => $loans->generate('outstanding-interest', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $overdue = $this->systemWide(fn () => $loans->generate('overdue-loans', $branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $completed = $this->systemWide(fn () => $loans->generate('completed-loans', $branchIds, SimulationReportReconciliationExpectations::ALL_TIME_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $writtenOff = $this->systemWide(fn () => $loans->generate('written-off-loans', $branchIds, SimulationReportReconciliationExpectations::ALL_TIME_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));

        $this->assertSame((string) $portfolio['active_loans'], $this->summary($active, 'Active loans'));
        $this->assertSame($portfolio['original_principal'], $this->summary($active, 'Original principal'));
        $this->assertSame($portfolio['principal_outstanding'], $this->summary($principal, 'Outstanding principal'));
        $this->assertSame($portfolio['interest_outstanding'], $this->summary($interest, 'Outstanding interest'));
        $this->assertSame((string) $portfolio['overdue_loans'], $this->summary($overdue, 'Overdue loans'));
        $this->assertSame($portfolio['overdue_principal'], $this->summary($overdue, 'Overdue principal'));
        $this->assertSame($portfolio['overdue_interest'], $this->summary($overdue, 'Overdue interest'));
        $this->assertSame($portfolio['overdue_total'], $this->summary($overdue, 'Total overdue'));
        $this->assertSame((string) $portfolio['completed_loans'], $this->summary($completed, 'Completed loans'));
        $this->assertSame((string) $portfolio['written_off_loans'], $this->summary($writtenOff, 'Written-off loans'));
        $this->assertSame($portfolio['recoverable_balance'], $this->summary($writtenOff, 'Recoverable balance'));

        $branchRows = collect($portfolioReport['branch_rows'])->keyBy(fn (array $row): string => $row['branch']->code);
        foreach (SimulationReportReconciliationExpectations::branchControls() as $code => $expected) {
            $this->assertSame($expected['active_loans'], $branchRows[$code]['active_loans']);
            $this->assertSame($expected['principal_outstanding'], $branchRows[$code]['principal_outstanding']);
            $this->assertSame($expected['total_outstanding'], $branchRows[$code]['total_outstanding']);
            $this->assertSame($expected['overdue_total'], $branchRows[$code]['overdue']);
        }
    }

    public function test_dashboard_reconciles_to_reports_for_company_and_each_branch(): void
    {
        $this->seed(SimulationSeeder::class);
        $dashboard = app(ManagementDashboardService::class);
        $money = app(MoneyMath::class);
        $branchIds = $this->simulationBranchIds();
        $cash = SimulationReportReconciliationExpectations::financialControls();
        $portfolio = SimulationReportReconciliationExpectations::portfolioControls();
        $pnl = SimulationReportReconciliationExpectations::profitLossControls();

        $data = $this->systemWide(fn () => $dashboard->generate($branchIds, SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
        $this->assertSame($cash['account_balance'], $data['metrics']['available_funds']);
        $this->assertSame($portfolio['principal_outstanding'], $data['metrics']['outstanding_principal']);
        $this->assertSame($portfolio['interest_outstanding'], $data['metrics']['expected_interest']);
        $this->assertSame($cash['collections'], $data['metrics']['collections']);
        $this->assertSame($cash['disbursements'], $data['metrics']['disbursements']);
        $this->assertSame($cash['approved_expenses'], $data['metrics']['expenses']);
        $this->assertSame($portfolio['overdue_total'], $data['metrics']['overdue_amount']);
        $this->assertSame($pnl['net_profit'], $data['metrics']['period_profit']);
        $this->assertSame($pnl['month_net_profit'], $data['metrics']['profit_this_month']);
        $this->assertSame((string) $portfolio['active_loans'], $data['metrics']['active_loans']);
        $this->assertSame((string) $portfolio['completed_loans'], $data['metrics']['completed_loans']);
        $this->assertSame($portfolio['recoverable_balance'], $data['metrics']['written_off_portfolio']);

        $this->assertSame($cash['cash_balance'], $data['account_balances']['cash']);
        $this->assertSame($cash['bank_balance'], $data['account_balances']['bank']);
        $this->assertSame($cash['mobile_money_balance'], $data['account_balances']['mobile_money']);
        $this->assertSame($cash['other_account_balance'], $data['account_balances']['other']);

        $seriesIncome = collect($data['income_expense_series'])->sum(fn (array $row): int => (int) $row['income_minor']);
        $seriesExpenses = collect($data['income_expense_series'])->sum(fn (array $row): int => (int) $row['expenses_minor']);
        $this->assertSame($pnl['total_income'], $money->fromMinor($seriesIncome));
        $this->assertSame($pnl['total_expenses'], $money->fromMinor($seriesExpenses));

        $flowCollections = collect($data['flow_series'])->sum(fn (array $row): int => (int) $row['collections_minor']);
        $flowDisbursements = collect($data['flow_series'])->sum(fn (array $row): int => (int) $row['disbursements_minor']);
        $this->assertSame($cash['collections'], $money->fromMinor($flowCollections));
        $this->assertSame($cash['disbursements'], $money->fromMinor($flowDisbursements));

        foreach (SimulationReportReconciliationExpectations::branchControls() as $code => $expected) {
            $branch = $this->systemWide(fn () => Branch::query()->where('code', $code)->firstOrFail());
            $branchData = $this->systemWide(fn () => $dashboard->generate([(int) $branch->id], SimulationReportReconciliationExpectations::PERIOD_FROM, SimulationReportReconciliationExpectations::PERIOD_TO));
            $this->assertSame((string) $expected['active_loans'], $branchData['metrics']['active_loans']);
            $this->assertSame($expected['principal_outstanding'], $branchData['metrics']['outstanding_principal']);
            $this->assertSame($expected['overdue_total'], $branchData['metrics']['overdue_amount']);
            $this->assertSame($expected['collections'], $branchData['metrics']['collections']);
            $this->assertSame($expected['cash_disbursements'], $branchData['metrics']['disbursements']);
        }
    }

    public function test_fixed_asset_and_csv_exports_match_reconciled_controls_and_enforce_branch_scope(): void
    {
        $this->seed(SimulationSeeder::class);
        $admin = User::query()->whereHas('roles', fn ($query) => $query->where('name', 'super-administrator'))->firstOrFail();
        $darManager = User::query()->where('username', 'sim.dar.manager')->firstOrFail();
        $dar = $this->systemWide(fn () => Branch::query()->where('code', 'SIM-DAR')->firstOrFail());
        $mwanza = $this->systemWide(fn () => Branch::query()->where('code', 'SIM-MZA')->firstOrFail());
        $arusha = $this->systemWide(fn () => Branch::query()->where('code', 'SIM-ARU')->firstOrFail());
        $asset = SimulationReportReconciliationExpectations::fixedAssetControls();
        $pnl = SimulationReportReconciliationExpectations::profitLossControls();
        $portfolio = SimulationReportReconciliationExpectations::portfolioControls();

        $query = [
            'from' => SimulationReportReconciliationExpectations::PERIOD_FROM,
            'to' => SimulationReportReconciliationExpectations::PERIOD_TO,
            'branch_ids' => [$dar->id, $mwanza->id, $arusha->id],
        ];

        $financialCsv = $this->actingAs($admin)->get(route('reports.export', ['report' => 'profit-loss'] + $query))->assertOk()->streamedContent();
        $this->assertStringContainsString('"Total Income",'.$pnl['total_income'], $financialCsv);
        $this->assertStringContainsString('"Total Expenses",'.$pnl['total_expenses'], $financialCsv);
        $this->assertStringContainsString('"Net Profit / Loss",\''.$pnl['net_profit'], $financialCsv);

        $loanCsv = $this->actingAs($admin)->get(route('loan-branch-reports.export', ['report' => 'active-loan-portfolio'] + $query))->assertOk()->streamedContent();
        $this->assertStringContainsString('"Active loans",'.$portfolio['active_loans'], $loanCsv);
        $this->assertStringContainsString('"Outstanding principal",'.$portfolio['principal_outstanding'], $loanCsv);
        $this->assertStringContainsString('"Total outstanding",'.$portfolio['total_outstanding'], $loanCsv);

        $registerCsv = $this->actingAs($admin)->get(route('fixed-assets.reports.export', [
            'report' => 'register', 'from' => '2026-08-01', 'to' => SimulationReportReconciliationExpectations::PERIOD_TO, 'branch_ids' => [$arusha->id],
        ]))->assertOk()->streamedContent();
        $this->assertStringContainsString('SIM Arusha Office Equipment', $registerCsv);
        $this->assertStringContainsString($asset['total_cost'], $registerCsv);
        $this->assertStringContainsString($asset['accumulated_depreciation'], $registerCsv);
        $this->assertStringContainsString($asset['net_book_value'], $registerCsv);

        $depreciationCsv = $this->actingAs($admin)->get(route('fixed-assets.reports.export', [
            'report' => 'depreciation', 'from' => '2026-08-01', 'to' => SimulationReportReconciliationExpectations::PERIOD_TO, 'branch_ids' => [$arusha->id],
        ]))->assertOk()->streamedContent();
        $this->assertStringContainsString($asset['period_depreciation'], $depreciationCsv);

        $maintenanceCsv = $this->actingAs($admin)->get(route('fixed-assets.reports.export', [
            'report' => 'maintenance', 'from' => '2026-08-01', 'to' => SimulationReportReconciliationExpectations::PERIOD_TO, 'branch_ids' => [$arusha->id],
        ]))->assertOk()->streamedContent();
        $this->assertStringContainsString('SIM-OP-MAINTENANCE', $maintenanceCsv);
        $this->assertStringContainsString($asset['period_maintenance'], $maintenanceCsv);

        $this->actingAs($darManager)->get(route('reports.index', [
            'report' => 'profit-loss', 'from' => SimulationReportReconciliationExpectations::PERIOD_FROM,
            'to' => SimulationReportReconciliationExpectations::PERIOD_TO, 'branch_ids' => [$mwanza->id],
        ]))->assertForbidden();
        $this->actingAs($darManager)->get(route('loan-branch-reports.index', [
            'report' => 'active-loan-portfolio', 'from' => SimulationReportReconciliationExpectations::PERIOD_FROM,
            'to' => SimulationReportReconciliationExpectations::PERIOD_TO, 'branch_ids' => [$mwanza->id],
        ]))->assertForbidden();
    }

    /** @return list<int> */
    private function simulationBranchIds(): array
    {
        return $this->systemWide(fn () => Branch::query()->where('code', 'like', 'SIM-%')->orderBy('code')->pluck('id')->map(fn ($id) => (int) $id)->all());
    }

    private function summary(array $report, string $label): string
    {
        return (string) (collect($report['summary'])->firstWhere('label', $label)['value'] ?? '');
    }
}
