<?php

namespace Tests\Feature;

use App\Models\Branch;
use App\Models\Business;
use App\Models\CashReconciliation;
use App\Models\DailyClosing;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\Role;
use App\Models\User;
use Carbon\CarbonImmutable;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;

class CashReconciliationDailyClosingTest extends TestCase
{
    use RefreshDatabase;

    private int $transactionSequence = 0;

    protected function setUp(): void
    {
        parent::setUp();
        CarbonImmutable::setTestNow('2026-08-12 17:45:00');
        $this->seed(DatabaseSeeder::class);
    }

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

    public function test_cash_reconciliation_calculates_expected_cash_requires_variance_explanation_and_preserves_recount_history(): void
    {
        [$branch, $cash, $cashier] = $this->fixture('recount');
        $this->tx($cash, '2026-08-11 09:00:00', 'opening_balance', '1000.00', '0.00', $cashier);
        $this->tx($cash, '2026-08-12 10:00:00', 'loan_payment', '200.00', '0.00', $cashier);
        $this->tx($cash, '2026-08-12 15:00:00', 'expense', '0.00', '50.00', $cashier);

        $payload = [
            'branch_id' => $branch->id,
            'financial_account_id' => $cash->id,
            'date' => '2026-08-12',
            'physical_cash' => '1140.00',
        ];
        $this->actingAs($cashier)->post(route('cash-closing.reconcile'), $payload)
            ->assertSessionHasErrors('variance_explanation');
        $this->assertSame(0, CashReconciliation::count());

        $payload['variance_explanation'] = 'Ten shillings short at physical count.';
        $this->actingAs($cashier)->post(route('cash-closing.reconcile'), $payload)->assertRedirect();
        $first = CashReconciliation::firstOrFail();
        $this->assertSame('1000.00', $first->opening_cash);
        $this->assertSame('200.00', $first->cash_in);
        $this->assertSame('50.00', $first->cash_out);
        $this->assertSame('1150.00', $first->expected_cash);
        $this->assertSame('1140.00', $first->physical_cash);
        $this->assertSame('-10.00', $first->variance);
        $this->assertSame(1, $first->version);
        $this->assertDatabaseHas('audit_logs', ['action' => 'cash_reconciliation.created', 'auditable_id' => $first->id]);

        $this->actingAs($cashier)->post(route('cash-closing.reconcile'), [
            'branch_id' => $branch->id,
            'financial_account_id' => $cash->id,
            'date' => '2026-08-12',
            'physical_cash' => '1150.00',
        ])->assertRedirect();

        $second = CashReconciliation::where('status', 'active')->firstOrFail();
        $this->assertSame(2, $second->version);
        $this->assertSame('0.00', $second->variance);
        $this->assertSame('superseded', $first->fresh()->status);
        $this->assertSame($second->id, $first->fresh()->superseded_by_id);
        $this->assertSame(2, CashReconciliation::count());
    }

    public function test_daily_closing_report_classifies_transaction_ledger_and_reconciles_exactly(): void
    {
        [$branch, $cash, , $accountant] = $this->fixture('report');
        $mobile = $this->account($branch, 'Report Mobile', 'mobile_money');
        $bank = $this->account($branch, 'Report Bank', 'bank');

        $this->tx($cash, '2026-08-11 08:00:00', 'opening_balance', '1000.00', '0.00', $accountant);
        $this->tx($mobile, '2026-08-11 08:05:00', 'opening_balance', '1000.00', '0.00', $accountant);
        $this->tx($bank, '2026-08-11 08:10:00', 'opening_balance', '1000.00', '0.00', $accountant);

        $this->tx($cash, '2026-08-12 09:00:00', 'loan_payment', '100.00', '0.00', $accountant);
        $this->tx($mobile, '2026-08-12 09:10:00', 'loan_payment', '200.00', '0.00', $accountant);
        $this->tx($bank, '2026-08-12 09:20:00', 'loan_early_settlement', '300.00', '0.00', $accountant);
        $this->tx($cash, '2026-08-12 09:30:00', 'loan_write_off_recovery', '50.00', '0.00', $accountant);
        $this->tx($cash, '2026-08-12 10:00:00', 'loan_disbursement', '0.00', '400.00', $accountant);
        $this->tx($bank, '2026-08-12 10:10:00', 'loan_top_up', '0.00', '100.00', $accountant);
        $this->tx($bank, '2026-08-12 11:00:00', 'expense', '0.00', '75.00', $accountant);
        $this->tx($cash, '2026-08-12 11:10:00', 'payroll', '0.00', '25.00', $accountant);
        $this->tx($cash, '2026-08-12 12:00:00', 'transfer_out', '0.00', '50.00', $accountant);
        $this->tx($bank, '2026-08-12 12:00:01', 'transfer_in', '50.00', '0.00', $accountant);
        $this->tx($mobile, '2026-08-12 13:00:00', 'capital_injection', '500.00', '0.00', $accountant);
        $this->tx($bank, '2026-08-12 14:00:00', 'manual_adjustment_in', '20.00', '0.00', $accountant);
        $this->tx($mobile, '2026-08-12 14:10:00', 'manual_adjustment_out', '0.00', '10.00', $accountant);

        $this->reconcile($accountant, $branch, $cash, '675.00');
        $this->reconcile($accountant, $branch, $mobile, '1690.00');
        $this->reconcile($accountant, $branch, $bank, '1195.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id,
            'date' => '2026-08-12',
        ])->assertRedirect();

        $closing = DailyClosing::firstOrFail();
        $this->assertSame('3000.00', $closing->opening_funds);
        $this->assertSame('650.00', $closing->collections);
        $this->assertSame('150.00', $closing->cash_collections);
        $this->assertSame('200.00', $closing->mobile_money_collections);
        $this->assertSame('300.00', $closing->bank_collections);
        $this->assertSame('0.00', $closing->other_collections);
        $this->assertSame('500.00', $closing->disbursements);
        $this->assertSame('100.00', $closing->expenses);
        $this->assertSame('0.00', $closing->transfers);
        $this->assertSame('500.00', $closing->capital_inflows);
        $this->assertSame('20.00', $closing->other_inflows);
        $this->assertSame('10.00', $closing->other_outflows);
        $this->assertSame('3560.00', $closing->closing_funds);
        $this->assertSame('3560.00', $closing->expected_cash);
        $this->assertSame('3560.00', $closing->actual_cash);
        $this->assertSame('0.00', $closing->variance);
        $this->assertNotEmpty($closing->snapshot_fingerprint);
        $this->assertSame(13, $closing->snapshot['transaction_count']);
        $this->assertSame(3, $closing->snapshot['reconciled_account_count']);
    }

    public function test_closing_requires_every_branch_money_account_to_be_reconciled_and_reconciled_again_after_new_movement(): void
    {
        [$branch, $cash, , $accountant] = $this->fixture('stale-reconciliation');
        $safe = $this->account($branch, 'Vault Safe', 'safe');
        $mobile = $this->account($branch, 'Branch Mobile Money', 'mobile_money');
        $bank = $this->account($branch, 'Branch Bank', 'bank');
        $this->tx($cash, '2026-08-11 09:00:00', 'opening_balance', '500.00', '0.00', $accountant);
        $this->tx($safe, '2026-08-11 09:05:00', 'opening_balance', '500.00', '0.00', $accountant);
        $this->tx($mobile, '2026-08-11 09:10:00', 'opening_balance', '500.00', '0.00', $accountant);
        $this->tx($bank, '2026-08-11 09:15:00', 'opening_balance', '500.00', '0.00', $accountant);

        $this->reconcile($accountant, $branch, $cash, '500.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertSessionHasErrors('reconciliation');

        $this->reconcile($accountant, $branch, $safe, '500.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertSessionHasErrors('reconciliation');

        $this->reconcile($accountant, $branch, $mobile, '500.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertSessionHasErrors('reconciliation');

        $this->reconcile($accountant, $branch, $bank, '500.00');
        $this->tx($bank, '2026-08-12 16:00:00', 'loan_payment', '25.00', '0.00', $accountant);
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertSessionHasErrors('reconciliation');

        $this->reconcile($accountant, $branch, $bank, '525.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertRedirect();
        $closing = DailyClosing::firstOrFail();
        $this->assertSame('prepared', $closing->status);
        $this->assertSame('2025.00', $closing->expected_cash);
        $this->assertSame('2025.00', $closing->actual_cash);
        $this->assertSame(2, CashReconciliation::where('financial_account_id', $bank->id)->count());
    }

    public function test_manager_confirmation_enforces_maker_checker_variance_comments_and_closed_books(): void
    {
        [$branch, $cash, , , $manager, $admin] = $this->fixture('confirmation');
        $branch->business->update(['maker_checker_enabled' => true]);
        $this->tx($cash, '2026-08-11 09:00:00', 'opening_balance', '1000.00', '0.00', $admin);
        $this->actingAs($admin)->post(route('cash-closing.reconcile'), [
            'branch_id' => $branch->id,
            'financial_account_id' => $cash->id,
            'date' => '2026-08-12',
            'physical_cash' => '990.00',
            'variance_explanation' => 'Cash count is short by ten.',
        ])->assertRedirect();
        $this->actingAs($admin)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertRedirect();
        $closing = DailyClosing::firstOrFail();

        $this->actingAs($admin)->post(route('cash-closing.confirm', $closing), [])
            ->assertSessionHasErrors('closing');
        $this->assertSame('prepared', $closing->fresh()->status);

        $this->actingAs($manager)->post(route('cash-closing.confirm', $closing), [])
            ->assertSessionHasErrors('manager_comments');
        $this->actingAs($manager)->post(route('cash-closing.confirm', $closing), [
            'manager_comments' => 'Variance reviewed and accepted for investigation.',
        ])->assertRedirect();
        $closing->refresh();
        $this->assertSame('confirmed', $closing->status);
        $this->assertSame($manager->id, $closing->manager_confirmed_by);
        $this->assertDatabaseHas('audit_logs', ['action' => 'daily_closing.confirmed', 'auditable_id' => $closing->id]);

        try {
            $this->tx($cash, '2026-08-12 18:00:00', 'loan_payment', '5.00', '0.00', $manager);
            $this->fail('A manager-confirmed branch day must reject late financial posting.');
        } catch (ValidationException $exception) {
            $this->assertArrayHasKey('transaction_date', $exception->errors());
        }

        $nextDay = $this->tx($cash, '2026-08-13 09:00:00', 'loan_payment', '5.00', '0.00', $manager);
        $this->assertSame('2026-08-13', $nextDay->transaction_date->toDateString());
    }

    public function test_prepared_closing_rejects_stale_snapshot_until_refreshed(): void
    {
        [$branch, $cash, , $accountant, $manager] = $this->fixture('stale-closing');
        $this->tx($cash, '2026-08-11 09:00:00', 'opening_balance', '1000.00', '0.00', $accountant);
        $this->reconcile($accountant, $branch, $cash, '1000.00');
        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertRedirect();
        $closing = DailyClosing::firstOrFail();
        $oldFingerprint = $closing->snapshot_fingerprint;

        $this->tx($cash, '2026-08-12 16:30:00', 'loan_payment', '50.00', '0.00', $accountant);
        $this->reconcile($accountant, $branch, $cash, '1050.00');
        $this->actingAs($manager)->post(route('cash-closing.confirm', $closing), [])
            ->assertSessionHasErrors('closing');
        $this->assertSame('prepared', $closing->fresh()->status);

        $this->actingAs($accountant)->post(route('cash-closing.prepare'), [
            'branch_id' => $branch->id, 'date' => '2026-08-12',
        ])->assertRedirect();
        $closing->refresh();
        $this->assertNotSame($oldFingerprint, $closing->snapshot_fingerprint);
        $this->assertSame('1050.00', $closing->expected_cash);
        $this->actingAs($manager)->post(route('cash-closing.confirm', $closing), [])->assertRedirect();
        $this->assertSame('confirmed', $closing->fresh()->status);
    }

    public function test_permissions_branch_isolation_navigation_and_printable_report_are_backend_enforced(): void
    {
        [$branchA, $cashA, $cashierA, $accountantA, $managerA] = $this->fixture('branch-a');
        [$branchB, $cashB, , $accountantB] = $this->fixture('branch-b');
        $auditorA = $this->staff('auditor', $branchA, 'closing_auditor_a');

        $this->tx($cashA, '2026-08-11 09:00:00', 'opening_balance', '100.00', '0.00', $accountantA);
        $this->tx($cashB, '2026-08-11 09:00:00', 'opening_balance', '200.00', '0.00', $accountantB);
        $this->reconcile($accountantA, $branchA, $cashA, '100.00');
        $this->reconcile($accountantB, $branchB, $cashB, '200.00');
        $this->actingAs($accountantA)->post(route('cash-closing.prepare'), ['branch_id'=>$branchA->id,'date'=>'2026-08-12'])->assertRedirect();
        $closingA = DailyClosing::where('branch_id',$branchA->id)->firstOrFail();
        $this->actingAs($accountantB)->post(route('cash-closing.prepare'), ['branch_id'=>$branchB->id,'date'=>'2026-08-12'])->assertRedirect();
        $closingB = DailyClosing::where('branch_id',$branchB->id)->firstOrFail();

        $this->actingAs($cashierA)->get(route('cash-closing.index', ['branch_id'=>$branchA->id,'date'=>'2026-08-12']))
            ->assertOk()
            ->assertSee('Cash Closing')
            ->assertSee('Branch money accounts')
            ->assertSee('data-standard-table', false)
            ->assertSee('Reconcile again')
            ->assertSee('reconcile-'.$cashA->id, false);
        $this->actingAs($cashierA)->post(route('cash-closing.prepare'), ['branch_id'=>$branchA->id,'date'=>'2026-08-12'])->assertForbidden();
        $this->actingAs($managerA)->post(route('cash-closing.reconcile'), [
            'branch_id'=>$branchA->id,'financial_account_id'=>$cashA->id,'date'=>'2026-08-12','physical_cash'=>'100.00',
        ])->assertForbidden();
        $this->actingAs($accountantA)->post(route('cash-closing.confirm',$closingA), [])->assertForbidden();
        $this->actingAs($auditorA)->get(route('cash-closing.show',$closingA))->assertOk()->assertSee('Print');
        $this->actingAs($auditorA)->post(route('cash-closing.reconcile'), [
            'branch_id'=>$branchA->id,'financial_account_id'=>$cashA->id,'date'=>'2026-08-12','physical_cash'=>'100.00',
        ])->assertForbidden();
        $this->actingAs($cashierA)->get(route('cash-closing.index', ['branch_id'=>$branchB->id,'date'=>'2026-08-12']))->assertForbidden();
        $this->actingAs($auditorA)->get(route('cash-closing.show',$closingB))->assertForbidden();
    }

    private function reconcile(User $user, Branch $branch, FinancialAccount $account, string $actual): void
    {
        $this->actingAs($user)->post(route('cash-closing.reconcile'), [
            'branch_id' => $branch->id,
            'financial_account_id' => $account->id,
            'date' => '2026-08-12',
            'actual_balance' => $actual,
        ])->assertRedirect();
    }

    private function fixture(string $tag): array
    {
        $business = Business::firstOrFail();
        $branch = Branch::create([
            'business_id' => $business->id,
            'name' => 'Closing '.str($tag)->headline(),
            'status' => 'active',
        ]);
        $cash = $this->account($branch, 'Cash '.str($tag)->headline(), 'cash');
        $slug = str_replace('-', '_', $tag);
        $cashier = $this->staff('cashier', $branch, 'closing_cashier_'.$slug);
        $accountant = $this->staff('accountant', $branch, 'closing_accountant_'.$slug);
        $manager = $this->staff('branch-manager', $branch, 'closing_manager_'.$slug);
        $admin = $this->staff('branch-administrator', $branch, 'closing_admin_'.$slug);

        return [$branch, $cash, $cashier, $accountant, $manager, $admin];
    }

    private function account(Branch $branch, string $name, string $type): FinancialAccount
    {
        return FinancialAccount::create([
            'branch_id' => $branch->id,
            'name' => $name,
            'account_type' => $type,
            'opening_balance' => '0.00',
            'current_balance' => '0.00',
            'status' => 'active',
        ]);
    }

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

    private function tx(
        FinancialAccount $account,
        string $date,
        string $type,
        string $moneyIn,
        string $moneyOut,
        User $user
    ): FinancialAccountTransaction {
        $this->transactionSequence++;
        return FinancialAccountTransaction::create([
            'transaction_number' => 'TST-'.str_pad((string) $this->transactionSequence, 6, '0', STR_PAD_LEFT),
            'account_id' => $account->id,
            'branch_id' => $account->branch_id,
            'transaction_date' => $date,
            'reference' => 'TEST-'.$this->transactionSequence,
            'type' => $type,
            'description' => 'Step 18 test '.$type,
            'money_in' => $moneyIn,
            'money_out' => $moneyOut,
            'running_balance' => '0.00',
            'user_id' => $user->id,
        ]);
    }
}
