<?php

namespace Tests\Feature\Simulation;

use App\Models\AccountingEntry;
use App\Models\CapitalInjection;
use App\Models\CashReconciliation;
use App\Models\DailyClosing;
use App\Models\Expense;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\FinancialTransfer;
use App\Models\FixedAsset;
use App\Models\FixedAssetDepreciation;
use App\Models\FixedAssetMaintenance;
use App\Models\PayrollPeriod;
use App\Models\RecurringExpense;
use App\Support\Simulation\SimulationOperationalFinanceExpectations;
use Database\Seeders\SimulationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class SimulationOperationalFinanceTest extends TestCase
{
    use RefreshDatabase;

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

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

        $opening = $this->systemWide(fn () => FinancialAccountTransaction::query()
            ->where('type','opening_balance')
            ->where('transaction_number','like','SIM-OPEN-%')
            ->get());

        $this->assertCount($controls['opening_ledger_transactions'],$opening);
        $this->assertSame($controls['opening_ledger_total'],$this->money($opening->sum('money_in')));
        $this->assertSame('0.00',$this->money($opening->sum('money_out')));

        $openingJournal = $this->systemWide(fn () => AccountingEntry::query()
            ->where('event_type','opening_balance')
            ->where('journal_number','like','JRN-SIM-OPEN-%')
            ->get());
        $this->assertCount($controls['opening_ledger_transactions'] * 2,$openingJournal);
        $this->assertSame($controls['opening_ledger_total'],$this->money($openingJournal->sum('debit')));
        $this->assertSame($controls['opening_ledger_total'],$this->money($openingJournal->sum('credit')));
    }

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

        $capital = $this->systemWide(fn () => CapitalInjection::query()->where('reference','SIM-OP-CAPITAL')->firstOrFail());
        $this->assertSame($controls['capital_injection'],$capital->amount);

        $transfer = $this->systemWide(fn () => FinancialTransfer::query()->where('reference','SIM-OP-TRANSFER')->firstOrFail());
        $this->assertSame($controls['internal_transfer'],$transfer->amount);

        $approvedExpense = $this->systemWide(fn () => Expense::query()->where('description','SIM-OP-APPROVED-EXPENSE')->firstOrFail());
        $this->assertSame('approved',$approvedExpense->status);
        $this->assertSame($controls['approved_expense'],$approvedExpense->amount);
        $this->assertNotSame((int) $approvedExpense->entered_by,(int) $approvedExpense->approved_by);

        $rejectedExpense = $this->systemWide(fn () => Expense::query()->where('description','SIM-OP-REJECTED-EXPENSE')->firstOrFail());
        $this->assertSame('rejected',$rejectedExpense->status);
        $this->assertSame($controls['rejected_expense'],$rejectedExpense->amount);

        $recurring = $this->systemWide(fn () => RecurringExpense::query()->where('description','SIM-OP-RECURRING-INTERNET')->firstOrFail());
        $recurringExpense = $this->systemWide(fn () => Expense::query()->where('recurring_expense_id',$recurring->id)->firstOrFail());
        $this->assertSame('pending',$recurringExpense->status);
        $this->assertSame($controls['recurring_pending_expense'],$recurringExpense->amount);

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

        $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_payroll_fixed_assets_and_daily_closing_reconcile(): void
    {
        $this->seed(SimulationSeeder::class);
        $controls = SimulationOperationalFinanceExpectations::aggregateControls();

        $payroll = $this->systemWide(fn () => PayrollPeriod::query()->where('name','SIM August 2026 Payroll')->firstOrFail());
        $this->assertSame('paid',$payroll->status);
        $this->assertSame($controls['payroll_total'],$payroll->total_net);
        $this->assertNotSame((int) $payroll->created_by,(int) $payroll->approved_by);
        $this->assertNotSame((int) $payroll->created_by,(int) $payroll->paid_by);

        $asset = $this->systemWide(fn () => FixedAsset::query()->where('name','SIM Arusha Office Equipment')->firstOrFail());
        $this->assertSame($controls['fixed_asset_purchase'],$asset->acquisition_cost);
        $this->assertSame('2400000.00',$asset->book_value);
        $this->assertSame('100000.00',$asset->accumulated_depreciation);

        $depreciation = $this->systemWide(fn () => FixedAssetDepreciation::query()->where('fixed_asset_id',$asset->id)->firstOrFail());
        $this->assertSame($controls['fixed_asset_depreciation'],$depreciation->amount);

        $maintenance = $this->systemWide(fn () => FixedAssetMaintenance::query()->where('fixed_asset_id',$asset->id)->where('reference','SIM-OP-MAINTENANCE')->firstOrFail());
        $this->assertSame($controls['fixed_asset_maintenance'],$maintenance->cost);

        $closing = $this->systemWide(fn () => DailyClosing::query()->whereDate('closing_date',SimulationOperationalFinanceExpectations::AS_OF_DATE)->firstOrFail());
        $this->assertSame('confirmed',$closing->status);
        $this->assertSame($controls['dar_closing_expected_cash'],$closing->expected_cash);
        $this->assertSame($controls['dar_closing_expected_cash'],$closing->actual_cash);
        $this->assertSame('0.00',$closing->variance);
        $this->assertNotSame((int) $closing->prepared_by,(int) $closing->manager_confirmed_by);

        $reconciliations = $this->systemWide(fn () => CashReconciliation::query()
            ->whereDate('reconciliation_date',SimulationOperationalFinanceExpectations::AS_OF_DATE)
            ->where('status','active')
            ->get());
        $this->assertCount(3,$reconciliations);
        $this->assertTrue($reconciliations->every(fn (CashReconciliation $item): bool => (string) $item->variance === '0.00'));
    }

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

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

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

    /** @return array<string, int|string> */
    private function snapshot(): array
    {
        return $this->systemWide(fn () => [
            'opening_transactions'=>FinancialAccountTransaction::query()->where('transaction_number','like','SIM-OPEN-%')->count(),
            'capital'=>CapitalInjection::query()->where('reference','SIM-OP-CAPITAL')->count(),
            'transfers'=>FinancialTransfer::query()->where('reference','SIM-OP-TRANSFER')->count(),
            'approved_expenses'=>Expense::query()->where('description','SIM-OP-APPROVED-EXPENSE')->count(),
            'rejected_expenses'=>Expense::query()->where('description','SIM-OP-REJECTED-EXPENSE')->count(),
            'recurring_expenses'=>Expense::query()->whereNotNull('recurring_expense_id')->where('description','SIM-OP-RECURRING-INTERNET')->count(),
            'payrolls'=>PayrollPeriod::query()->where('name','SIM August 2026 Payroll')->count(),
            'assets'=>FixedAsset::query()->where('name','SIM Arusha Office Equipment')->count(),
            'depreciations'=>FixedAssetDepreciation::query()->count(),
            'maintenance'=>FixedAssetMaintenance::query()->where('reference','SIM-OP-MAINTENANCE')->count(),
            'closings'=>DailyClosing::query()->whereDate('closing_date',SimulationOperationalFinanceExpectations::AS_OF_DATE)->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,'.','');
    }
}
