<?php

namespace Tests\Feature;

use App\Models\Branch;
use App\Models\Business;
use App\Models\Customer;
use App\Models\CustomerStatementEntry;
use App\Models\FinancialAccount;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\LoanPenaltyCharge;
use App\Models\LoanProduct;
use App\Models\Role;
use App\Models\User;
use App\Services\LoanApplicationService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ArrearsPenaltyWorkflowTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(DatabaseSeeder::class);
    }

    public function test_all_four_penalty_methods_are_deterministic_and_do_not_compound(): void
    {
        $target = '2026-08-20';
        [, $fixed] = $this->activeLoan('fixed', 'fixed', '500.000000', 1, $target);
        [, $percentage] = $this->activeLoan('percentage', 'percentage', '10.000000', 1, $target);
        [, $daily] = $this->activeLoan('daily', 'daily_percentage', '1.000000', 3, $target);
        [, $weekly] = $this->activeLoan('weekly', 'weekly_percentage', '2.000000', 15, $target);

        $this->artisan('loans:update-overdue', ['--date' => $target])->assertSuccessful();

        $this->assertPenalty($fixed, 1, '500.00');
        $this->assertPenalty($percentage, 1, '1000.00');
        $this->assertPenalty($daily, 3, '300.00');
        $this->assertPenalty($weekly, 3, '600.00');
        $this->assertSame('10300.00', $this->inBranch($daily->branch_id, fn () => $daily->fresh()->total_overdue));
        $this->assertSame('10300.00', $this->inBranch($daily->branch_id, fn () => $daily->fresh()->total_outstanding));
        $latestStatement = CustomerStatementEntry::query()
            ->where('loan_id', $daily->id)
            ->orderByDesc('entry_date')
            ->orderByDesc('id')
            ->firstOrFail();
        $this->assertSame('10300.00', $latestStatement->running_balance);
        $baseAmounts = $this->inBranch($daily->branch_id, fn () => $daily->penaltyCharges()->pluck('base_amount')->unique()->values()->all());
        $this->assertSame(['10000.00'], $baseAmounts);

        $count = $this->systemWide(fn () => LoanPenaltyCharge::count());
        $this->artisan('loans:update-overdue', ['--date' => $target])->assertSuccessful();
        $this->assertSame($count, $this->systemWide(fn () => LoanPenaltyCharge::count()));
        $this->assertSame('300.00', $this->inBranch($daily->branch_id, fn () => $daily->fresh()->penalties_outstanding));
    }

    public function test_arrears_buckets_match_required_boundaries(): void
    {
        $target = '2026-08-20';
        $expected = [1 => '1-7', 7 => '1-7', 8 => '8-30', 30 => '8-30', 31 => '31-60', 60 => '31-60', 61 => '61-90', 90 => '61-90', 91 => '90+'];
        $loans = [];
        foreach ($expected as $days => $bucket) {
            [, $loan] = $this->activeLoan('bucket-'.$days, 'none', '0.000000', $days, $target);
            $loans[$days] = [$loan, $bucket];
        }

        $this->artisan('loans:update-overdue', ['--date' => $target])->assertSuccessful();

        foreach ($loans as $days => [$loan, $bucket]) {
            $this->inBranch($loan->branch_id, fn () => $loan->refresh());
            $this->assertSame($days, $loan->days_overdue);
            $this->assertSame($bucket, $loan->arrears_bucket);
            $this->assertSame('10000.00', $loan->overdue_principal);
            $this->assertSame('10000.00', $loan->total_overdue);
            $this->assertSame('overdue', $this->inBranch($loan->branch_id, fn () => $loan->installments()->firstOrFail()->status));
        }
    }

    public function test_payment_catches_up_due_penalty_and_refreshes_arrears_immediately(): void
    {
        $target = '2026-08-20';
        [$branch, $loan] = $this->activeLoan('payment-catchup', 'fixed', '500.000000', 1, $target);
        $account = FinancialAccount::create([
            'branch_id' => $branch->id, 'name' => 'Arrears Cash Till', 'account_type' => 'cash',
            'opening_balance' => '0.00', 'current_balance' => '0.00', 'status' => 'active',
        ]);
        $cashier = $this->staff('cashier', $branch, 'arrears_cashier');

        $this->actingAs($cashier)->post(route('loan-payments.store', $loan), [
            'amount' => '500.00', 'date' => $target, 'method' => 'cash',
            'financial_account_id' => $account->id, 'reference' => 'ARREARS-CATCHUP-1', 'notes' => 'Penalty payment',
        ])->assertRedirect();

        $payment = LoanPayment::where('loan_id', $loan->id)->firstOrFail();
        $loan->refresh();
        $this->assertSame(1, LoanPenaltyCharge::where('loan_id', $loan->id)->count());
        $this->assertSame('500.00', $payment->penalties_amount);
        $this->assertSame('0.00', $loan->penalties_outstanding);
        $this->assertSame('10000.00', $loan->total_outstanding);
        $this->assertSame('10000.00', $loan->total_overdue);
        $this->assertSame(1, $loan->days_overdue);
        $this->assertSame('overdue', $loan->installments()->firstOrFail()->status);
        $this->assertDatabaseHas('accounting_entries', [
            'related_type' => LoanPayment::class, 'related_id' => $payment->id, 'category' => 'penalties', 'credit' => 500,
        ]);
    }

    public function test_collection_followups_promises_and_branch_isolation_are_enforced(): void
    {
        $target = '2026-08-20';
        [$branchA, $loanA] = $this->activeLoan('follow-a', 'none', '0.000000', 8, $target);
        [$branchB, $loanB] = $this->activeLoan('follow-b', 'none', '0.000000', 31, $target);
        $this->artisan('loans:update-overdue', ['--date' => $target])->assertSuccessful();
        $managerA = $this->staff('branch-manager', $branchA, 'arrears_manager_a');

        $this->actingAs($managerA)->get(route('arrears.index'))
            ->assertOk()->assertSee('data-standard-table', false)->assertSee($loanA->loan_number)->assertDontSee($loanB->loan_number);
        $this->actingAs($managerA)->get(route('arrears.show', $loanB))->assertForbidden();
        $this->actingAs($managerA)->post(route('arrears.follow-ups.store', $loanB), ['action_type' => 'called'])->assertForbidden();

        $this->actingAs($managerA)->post(route('arrears.follow-ups.store', $loanA), [
            'action_type' => 'promise_to_pay', 'notes' => 'Customer promised to clear arrears.',
        ])->assertSessionHasErrors('promise_date');

        $this->actingAs($managerA)->post(route('arrears.follow-ups.store', $loanA), [
            'action_type' => 'promise_to_pay', 'promise_date' => '2026-08-25', 'notes' => 'Customer promised to clear arrears.',
        ])->assertRedirect();

        $this->assertDatabaseHas('collection_follow_ups', [
            'loan_id' => $loanA->id, 'branch_id' => $branchA->id, 'action_type' => 'promise_to_pay', 'employee_id' => $managerA->id,
        ]);
        $followUp = $loanA->collectionFollowUps()->firstOrFail();
        $this->assertSame('2026-08-25', $followUp->promise_date->toDateString());
        $this->assertDatabaseHas('audit_logs', ['action' => 'collections.follow_up_created', 'branch_id' => $branchA->id]);
        $this->actingAs($managerA)->get(route('arrears.show', $loanA))->assertOk()->assertSee('Penalty history')->assertSee('Collection follow-up history')->assertSee('25 Aug 2026');
    }

    public function test_new_loan_snapshots_penalty_terms_so_product_edits_do_not_rewrite_contract_terms(): void
    {
        $business = Business::firstOrFail();
        $branch = Branch::create(['business_id' => $business->id, 'name' => 'Penalty Snapshot Branch', 'status' => 'active']);
        $customer = Customer::create([
            'branch_id' => $branch->id, 'customer_number' => 'CUS-PEN-SNAPSHOT', 'identity_mode' => 'without_nin',
            'first_name' => 'Rehema', 'last_name' => 'John', 'phone' => '0756000011', 'status' => 'active',
        ]);
        $product = $this->product($business, 'Snapshot Product', 'daily_percentage', '1.500000');
        $admin = User::where('username', 'admin')->firstOrFail();
        $loan = app(LoanApplicationService::class)->create([
            'branch_id' => $branch->id, 'customer_id' => $customer->id, 'principal' => '10000.00',
            'purpose' => 'Stock purchase', 'schedule_start_date' => '2026-08-12', 'collateral_ids' => [],
        ], $product, $admin);

        $this->assertSame('daily_percentage', $loan->late_payment_penalty_type);
        $this->assertSame('1.500000', $loan->late_payment_penalty_value);
        $product->update(['late_payment_penalty_type' => 'fixed', 'late_payment_penalty_value' => '999.000000']);
        $this->inBranch($loan->branch_id, fn () => $loan->refresh());
        $this->assertSame('daily_percentage', $loan->late_payment_penalty_type);
        $this->assertSame('1.500000', $loan->late_payment_penalty_value);
    }

    public function test_nightly_overdue_scheduler_is_registered_before_morning_reminders(): void
    {
        $events = app(Schedule::class)->events();
        $event = collect($events)->first(fn ($event) => str_contains((string) $event->command, 'loans:update-overdue'));
        $this->assertNotNull($event);
        $this->assertSame('5 0 * * *', $event->expression);
        $this->assertSame(config('app.timezone'), $event->timezone);
    }

    private function assertPenalty(Loan $loan, int $count, string $total): void
    {
        [$actualCount, $sum, $outstanding] = $this->inBranch($loan->branch_id, function () use ($loan): array {
            return [
                $loan->penaltyCharges()->count(),
                number_format((float) $loan->penaltyCharges()->sum('amount'), 2, '.', ''),
                $loan->fresh()->penalties_outstanding,
            ];
        });
        $this->assertSame($count, $actualCount);
        $this->assertSame($total, $sum);
        $this->assertSame($total, $outstanding);
    }

    /** @return array{0:Branch,1:Loan} */
    private function activeLoan(string $tag, string $penaltyType, string $penaltyValue, int $daysOverdue, string $target): array
    {
        $business = Business::firstOrFail();
        $branch = Branch::create(['business_id' => $business->id, 'name' => 'Arrears '.str($tag)->headline(), 'status' => 'active']);
        $customer = Customer::create([
            'branch_id' => $branch->id, 'customer_number' => 'CUS-ARR-'.strtoupper(substr(md5($tag), 0, 8)), 'identity_mode' => 'without_nin',
            'first_name' => 'Amina', 'last_name' => str($tag)->headline()->toString(), 'phone' => '0744'.str_pad((string) $branch->id, 6, '0', STR_PAD_LEFT), 'status' => 'active',
        ]);
        $product = $this->product($business, 'Arrears Product '.str($tag)->headline(), $penaltyType, $penaltyValue);
        $loan = Loan::create([
            'branch_id' => $branch->id, 'customer_id' => $customer->id, 'loan_product_id' => $product->id,
            'sequence_number' => 1, 'sequence_year' => 2026, 'loan_number' => 'LN-ARR-'.strtoupper(substr(md5($tag), 0, 8)),
            'principal' => '10000.00', 'interest' => '0.00', 'processing_fee' => '0.00', 'insurance_fee' => '0.00', 'other_charges' => '0.00',
            'fees' => '0.00', 'total_repayment' => '10000.00', 'principal_outstanding' => '10000.00', 'interest_outstanding' => '0.00',
            'fees_outstanding' => '0.00', 'penalties_outstanding' => '0.00', 'total_outstanding' => '10000.00', 'total_paid' => '0.00',
            'late_payment_penalty_type' => $penaltyType, 'late_payment_penalty_value' => $penaltyValue,
            'purpose' => 'Arrears testing', 'schedule_start_date' => '2026-07-01', 'status' => 'active',
            'disbursed_at' => '2026-07-01', 'activated_at' => '2026-07-01', 'schedule_activated_at' => '2026-07-01',
        ]);
        $due = \Carbon\CarbonImmutable::parse($target)->subDays($daysOverdue)->toDateString();
        $loan->installments()->create([
            'installment_number' => 1, 'principal' => '10000.00', 'interest' => '0.00', 'fees' => '0.00', 'amount_due' => '10000.00',
            'due_date' => $due, 'amount_paid' => '0.00', 'remaining' => '10000.00', 'status' => 'upcoming',
            'principal_paid' => '0.00', 'interest_paid' => '0.00', 'fees_paid' => '0.00', 'penalty_due' => '0.00', 'penalty_paid' => '0.00',
        ]);
        CustomerStatementEntry::create([
            'customer_id' => $customer->id, 'loan_id' => $loan->id, 'branch_id' => $branch->id,
            'entry_date' => '2026-07-01', 'reference' => 'DSB-'.$tag, 'type' => 'loan_disbursement',
            'description' => 'Loan obligation '.$loan->loan_number, 'debit' => '10000.00', 'credit' => '0.00', 'running_balance' => '10000.00',
        ]);
        return [$branch, $loan];
    }

    private function product(Business $business, string $name, string $penaltyType, string $penaltyValue): LoanProduct
    {
        return LoanProduct::create([
            'business_id' => $business->id, 'name' => $name, 'minimum_amount' => '1000.00', 'maximum_amount' => '500000.00',
            'duration' => 1, 'duration_unit' => 'months', 'interest_rate' => '0.000000', 'interest_calculation_method' => 'flat',
            'repayment_frequency' => 'monthly', 'grace_period_days' => 0, 'late_payment_penalty_type' => $penaltyType,
            'late_payment_penalty_value' => $penaltyValue, 'processing_fee_type' => 'fixed', 'processing_fee_value' => '0.000000',
            'insurance_fee_type' => 'fixed', 'insurance_fee_value' => '0.000000', 'other_charges' => '0.00',
            'collateral_required' => false, 'guarantor_required' => false, '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;
    }
}
