<?php

namespace Tests\Feature\Simulation;

use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\Loan;
use App\Models\LoanEarlySettlement;
use App\Models\LoanRestructuring;
use App\Models\LoanTopUp;
use App\Models\LoanWriteOff;
use App\Models\LoanWriteOffRecovery;
use App\Support\Simulation\SimulationLoanAdjustmentExpectations;
use Database\Seeders\SimulationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SimulationLoanAdjustmentTest extends TestCase
{
    use RefreshDatabase;

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

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

        $topUp = $this->systemWide(fn () => LoanTopUp::query()->firstOrFail());
        $base = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['top_up']['base_purpose'])->firstOrFail());
        $replacement = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['top_up']['replacement_purpose'])->firstOrFail());
        $this->assertSame('closed_through_top_up',$base->status);
        $this->assertSame('active',$replacement->status);
        $this->assertTrue((bool) $replacement->top_up_override_used);
        $this->assertSame($scenarios['top_up']['internal_settlement'],$topUp->amount_settled_internally);
        $this->assertSame($scenarios['top_up']['cash_to_customer'],$topUp->cash_given_to_customer);
        $this->assertSame($scenarios['top_up']['new_principal'],$topUp->new_principal);
        $this->assertSame($scenarios['top_up']['new_total_repayment'],$replacement->total_repayment);

        $settlement = $this->systemWide(fn () => LoanEarlySettlement::query()->firstOrFail());
        $settledLoan = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['early_settlement']['purpose'])->firstOrFail());
        $this->assertSame('fully_paid',$settledLoan->status);
        $this->assertSame('0.00',$settledLoan->total_outstanding);
        $this->assertSame($scenarios['early_settlement']['subtotal'],number_format(
            (float) $settlement->principal_amount + (float) $settlement->interest_amount + (float) $settlement->fees_amount + (float) $settlement->penalties_amount,
            2,'.',''
        ));
        $this->assertSame($scenarios['early_settlement']['adjustment'],$settlement->settlement_adjustment);
        $this->assertSame($scenarios['early_settlement']['final_amount'],$settlement->final_amount);

        $approved = $this->systemWide(fn () => LoanRestructuring::query()->where('status','approved')->firstOrFail());
        $approvedLoan = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['restructure_approved']['purpose'])->firstOrFail());
        $this->assertSame(2,(int) $approvedLoan->current_schedule_version);
        $this->assertSame($scenarios['restructure_approved']['new_installments'],$this->systemWide(
            fn () => $approvedLoan->installments()->where('is_active',true)->count()
        ));
        $this->assertSame($scenarios['restructure_approved']['total_outstanding'],$approvedLoan->total_outstanding);
        $this->assertNotSame((int) $approved->requested_by,(int) $approved->approved_by);

        $rejected = $this->systemWide(fn () => LoanRestructuring::query()->where('status','rejected')->firstOrFail());
        $rejectedLoan = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['restructure_rejected']['purpose'])->firstOrFail());
        $this->assertSame('active',$rejectedLoan->status);
        $this->assertSame(1,(int) $rejectedLoan->current_schedule_version);
        $this->assertSame($scenarios['restructure_rejected']['total_outstanding'],$rejectedLoan->total_outstanding);
        $this->assertNotSame((int) $rejected->requested_by,(int) $rejected->rejected_by);

        $writeOff = $this->systemWide(fn () => LoanWriteOff::query()->where('status','approved')->firstOrFail());
        $writtenOffLoan = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['write_off_recovery']['purpose'])->firstOrFail());
        $this->assertSame('written_off',$writtenOffLoan->status);
        $this->assertSame($scenarios['write_off_recovery']['write_off_amount'],$writeOff->write_off_amount);
        $this->assertSame($scenarios['write_off_recovery']['recovery_amount'],$writeOff->recovered_amount);
        $this->assertSame($scenarios['write_off_recovery']['recoverable_balance'],$writeOff->recoverable_balance);
        $this->assertSame(1,$this->systemWide(fn () => LoanWriteOffRecovery::query()->count()));
        $this->assertNotSame((int) $writeOff->requested_by,(int) $writeOff->approved_by);

        $rejectedWriteOff = $this->systemWide(fn () => LoanWriteOff::query()->where('status','rejected')->firstOrFail());
        $rejectedWriteOffLoan = $this->systemWide(fn () => Loan::query()->where('purpose',$scenarios['write_off_rejected']['purpose'])->firstOrFail());
        $this->assertSame('active',$rejectedWriteOffLoan->status);
        $this->assertSame($scenarios['write_off_rejected']['write_off_amount'],$rejectedWriteOff->write_off_amount);
    }

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

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

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

        $topUp = $this->systemWide(fn () => FinancialAccountTransaction::query()->where('type','loan_top_up')->firstOrFail());
        $this->assertSame('245000.00',$topUp->money_out);
        $this->assertSame('0.00',$topUp->money_in);

        $settlement = $this->systemWide(fn () => FinancialAccountTransaction::query()->where('type','loan_early_settlement')->firstOrFail());
        $this->assertSame('783000.00',$settlement->money_in);
        $this->assertSame('0.00',$settlement->money_out);

        $recovery = $this->systemWide(fn () => FinancialAccountTransaction::query()->where('type','loan_write_off_recovery')->firstOrFail());
        $this->assertSame($controls['recovery_cash'],$recovery->money_in);
        $this->assertSame('0.00',$recovery->money_out);
    }

    public function test_adjustment_record_counts_and_decision_paths_are_controlled(): void
    {
        $this->seed(SimulationSeeder::class);
        $controls = SimulationLoanAdjustmentExpectations::aggregateControls();
        $scenarios = SimulationLoanAdjustmentExpectations::scenarios();

        $basePurposes = [
            $scenarios['top_up']['base_purpose'],
            $scenarios['early_settlement']['purpose'],
            $scenarios['restructure_approved']['purpose'],
            $scenarios['restructure_rejected']['purpose'],
            $scenarios['write_off_recovery']['purpose'],
            $scenarios['write_off_rejected']['purpose'],
        ];

        $this->assertSame($controls['base_adjustment_loans'],$this->systemWide(fn () => Loan::query()->whereIn('purpose',$basePurposes)->count()));
        $this->assertSame($controls['top_up_records'],$this->systemWide(fn () => LoanTopUp::query()->count()));
        $this->assertSame($controls['early_settlements'],$this->systemWide(fn () => LoanEarlySettlement::query()->count()));
        $this->assertSame($controls['restructurings'],$this->systemWide(fn () => LoanRestructuring::query()->count()));
        $this->assertSame($controls['approved_restructurings'],$this->systemWide(fn () => LoanRestructuring::query()->where('status','approved')->count()));
        $this->assertSame($controls['rejected_restructurings'],$this->systemWide(fn () => LoanRestructuring::query()->where('status','rejected')->count()));
        $this->assertSame($controls['write_offs'],$this->systemWide(fn () => LoanWriteOff::query()->count()));
        $this->assertSame($controls['approved_write_offs'],$this->systemWide(fn () => LoanWriteOff::query()->where('status','approved')->count()));
        $this->assertSame($controls['rejected_write_offs'],$this->systemWide(fn () => LoanWriteOff::query()->where('status','rejected')->count()));
        $this->assertSame($controls['write_off_recoveries'],$this->systemWide(fn () => LoanWriteOffRecovery::query()->count()));
    }

    public function test_full_adjustment_simulation_is_idempotent(): void
    {
        $this->seed(SimulationSeeder::class);
        $before = $this->snapshot();

        $this->seed(SimulationSeeder::class);
        $after = $this->snapshot();

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

    /** @return array<string, int|string> */
    private function snapshot(): array
    {
        return $this->systemWide(fn () => [
            'adjustment_loans'=>Loan::query()->where('purpose','like','SIM-ADJ-%')->count(),
            'top_ups'=>LoanTopUp::query()->count(),
            'settlements'=>LoanEarlySettlement::query()->count(),
            'restructurings'=>LoanRestructuring::query()->count(),
            'write_offs'=>LoanWriteOff::query()->count(),
            'recoveries'=>LoanWriteOffRecovery::query()->count(),
            'adjustment_transactions'=>FinancialAccountTransaction::query()->whereIn('type',[
                'loan_top_up','loan_early_settlement','loan_write_off_recovery'
            ])->count(),
            'account_total'=>$this->money(FinancialAccount::query()->where('name','like','SIM %')->sum('current_balance')),
        ]);
    }

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