<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Models\Credit;
use App\Models\Installment;
use App\Services\SmartCreditStateResolver;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * Tests for SmartCreditStateResolver.
 *
 * Verifies all state transitions and guard rules:
 * - Parent credit guard (immutable by child relationship)
 * - Immutable legacy states
 * - PAID priority over OVERDUE
 * - OVERDUE (credit.due_date < today AND remaining balance > 0)
 * - DELAYED (installment.due_date < today AND amount_paid < total)
 * - ACTIVE (everything up to date)
 * - Edge cases: null due_date, 0.01 residue, decimal precision
 */
class SmartCreditStateResolverTest extends TestCase
{
    use DatabaseTransactions;

    protected SmartCreditStateResolver $resolver;

    protected function setUp(): void
    {
        parent::setUp();
        $this->resolver = new SmartCreditStateResolver;
    }

    // ── Factory happy path ────────────────────────────────────────────────────

    #[Test]
    public function it_marks_credit_as_paid_when_all_installments_are_fully_paid(): void
    {
        $credit = Credit::factory()->fullyPaid()->create();

        $this->assertSame(Credit::STATUS_PAID, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_credit_as_overdue_when_due_date_has_passed_and_has_pending_installments(): void
    {
        $credit = Credit::factory()->overdue()->create();

        $this->assertSame(Credit::STATUS_OVERDUE, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_credit_as_delayed_when_there_are_partial_payments_but_not_overdue(): void
    {
        $credit = Credit::factory()->delayed()->create();

        $this->assertSame(Credit::STATUS_DELAYED, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_credit_as_active_when_everything_is_up_to_date(): void
    {
        $credit = Credit::factory()->activeUpToDate()->create();

        $this->assertSame(Credit::STATUS_ACTIVE, $this->resolver->resolve($credit));
    }

    // ── Parent credit guard ───────────────────────────────────────────────────

    #[Test]
    public function it_returns_current_status_for_parent_credit_regardless_of_installments(): void
    {
        $parent = Credit::factory()->create([
            'status' => Credit::STATUS_RESTRUCTURED,
            'due_date' => Carbon::yesterday(),
        ]);

        // Create a child credit pointing to parent
        Credit::factory()->create([
            'company_id' => $parent->company_id,
            'client_id' => $parent->client_id,
            'parent_credit_id' => $parent->id,
            'status' => Credit::STATUS_ACTIVE,
        ]);

        // Add an overdue installment to the parent — should still keep RESTRUCTURED
        Installment::factory()->create([
            'company_id' => $parent->company_id,
            'credit_id' => $parent->id,
            'status' => Installment::STATUS_OVERDUE,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_RESTRUCTURED, $this->resolver->resolve($parent->fresh()));
    }

    // ── Immutable legacy states ───────────────────────────────────────────────

    #[Test]
    #[DataProvider('immutableStatusProvider')]
    public function it_preserves_immutable_status_even_with_overdue_installments(string $status): void
    {
        $credit = Credit::factory()->create([
            'status' => $status,
            'due_date' => Carbon::yesterday(),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_OVERDUE,
            'due_date' => Carbon::yesterday()->subDay(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame($status, $this->resolver->resolve($credit));
    }

    public static function immutableStatusProvider(): array
    {
        return [
            'canceled' => [Credit::STATUS_CANCELED],
            'extended' => [Credit::STATUS_EXTENDED],
            'refinanced' => [Credit::STATUS_REFINANCED],
            'restructured' => [Credit::STATUS_RESTRUCTURED],
            'renewed' => [Credit::STATUS_RENEWED],
            'interest_capitalized' => [Credit::STATUS_INTEREST_CAPITALIZED],
            'defaulted' => [Credit::STATUS_DEFAULTED],
            'archived' => [Credit::STATUS_ARCHIVED],
        ];
    }

    // ── PAID priority ─────────────────────────────────────────────────────────

    #[Test]
    public function paid_takes_priority_over_overdue_when_all_installments_fully_paid(): void
    {
        // credit.due_date has passed but every installment is fully paid
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::yesterday(),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PAID,
            'due_date' => Carbon::yesterday()->subDays(2),
            'amount_paid' => 60_000,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_PAID, $this->resolver->resolve($credit));
    }

    #[Test]
    public function paid_takes_priority_when_multiple_installments_are_all_paid(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::yesterday(),
        ]);

        foreach (range(1, 3) as $i) {
            Installment::factory()->create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_number' => $i,
                'status' => Installment::STATUS_PAID,
                'due_date' => Carbon::yesterday()->subDays(4 - $i),
                'amount_paid' => 40_000,
                'total_amount' => 40_000,
            ]);
        }

        $this->assertSame(Credit::STATUS_PAID, $this->resolver->resolve($credit));
    }

    // ── OVERDUE ───────────────────────────────────────────────────────────────

    #[Test]
    public function it_marks_overdue_when_credit_due_date_passed_and_balance_remains(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::yesterday(),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_OVERDUE, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_does_not_mark_as_overdue_when_credit_has_no_due_date(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => null,
        ]);

        // Installment due in the future → should be ACTIVE
        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::tomorrow(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_ACTIVE, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_does_not_mark_as_overdue_when_credit_due_date_is_today(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today(), // today → NOT overdue yet
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        // due_date >= today → does NOT qualify as OVERDUE; but installment due today is not past
        $result = $this->resolver->resolve($credit);
        $this->assertNotSame(Credit::STATUS_OVERDUE, $result);
    }

    // ── DELAYED ───────────────────────────────────────────────────────────────

    #[Test]
    public function it_marks_delayed_when_installment_due_date_passed_with_remaining_balance(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(30),
        ]);

        // Installment overdue (past due_date, not fully paid)
        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PARTIAL_PAID,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 20_000,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_DELAYED, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_delayed_when_unpaid_installment_has_past_due_date(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(10),
        ]);

        // Fully unpaid installment with past due_date
        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_DELAYED, $this->resolver->resolve($credit));
    }

    // ── ACTIVE ────────────────────────────────────────────────────────────────

    #[Test]
    public function it_marks_active_when_all_installments_are_future_and_pending(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(60),
        ]);

        foreach ([[1, 30], [2, 60]] as [$num, $offsetDays]) {
            Installment::factory()->create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_number' => $num,
                'status' => Installment::STATUS_PENDING,
                'due_date' => Carbon::today()->addDays($offsetDays),
                'amount_paid' => 0,
                'total_amount' => 50_000,
            ]);
        }

        $this->assertSame(Credit::STATUS_ACTIVE, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_active_when_past_installments_are_fully_paid_and_future_pending(): void
    {
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(14),
        ]);

        // Past installment fully paid
        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PAID,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 60_000,
            'total_amount' => 60_000,
        ]);

        // Future installment pending
        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'installment_number' => 2,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today()->addDays(7),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_ACTIVE, $this->resolver->resolve($credit));
    }

    // ── DEFAULTED ─────────────────────────────────────────────────────────────

    #[Test]
    public function it_resolves_to_defaulted_when_installment_exceeds_days_until_default(): void
    {
        // days_until_default is read from CompanyFinancialSettings (default 90).
        // We create an installment overdue by 91 days to guarantee the threshold is crossed.
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(365), // credit itself is not expired
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today()->subDays(91),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_DEFAULTED, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_does_not_resolve_to_defaulted_when_installment_is_below_threshold(): void
    {
        // An installment overdue by only 1 day should not trigger DEFAULTED (threshold 90).
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(365),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $result = $this->resolver->resolve($credit);
        $this->assertNotSame(Credit::STATUS_DEFAULTED, $result);
    }

    #[Test]
    public function archived_credit_is_treated_as_immutable_and_never_auto_resolved(): void
    {
        // archived is set only by admin operations, never by the resolver automatically.
        // The resolver must preserve it unchanged regardless of installment state.
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ARCHIVED,
            'due_date' => Carbon::yesterday(),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PENDING,
            'due_date' => Carbon::today()->subDays(95), // would trigger DEFAULTED if mutable
            'amount_paid' => 0,
            'total_amount' => 60_000,
        ]);

        $this->assertSame(Credit::STATUS_ARCHIVED, $this->resolver->resolve($credit));
    }

    // ── Decimal precision edge cases ──────────────────────────────────────────

    #[Test]
    public function it_marks_delayed_when_installment_has_only_one_cent_remaining(): void
    {
        // Financial invariant: 0.01 residue must still be detected as unpaid
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(30),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PARTIAL_PAID,
            'due_date' => Carbon::yesterday(),
            'amount_paid' => 59_999.99,  // only $0.01 remains
            'total_amount' => 60_000.00,
        ]);

        $this->assertSame(Credit::STATUS_DELAYED, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_overdue_when_credit_has_only_one_cent_remaining_balance(): void
    {
        // credit.due_date expired, tiny remaining balance (0.01) still triggers OVERDUE
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::yesterday(),
        ]);

        Installment::factory()->create([
            'company_id' => $credit->company_id,
            'credit_id' => $credit->id,
            'status' => Installment::STATUS_PARTIAL_PAID,
            'due_date' => Carbon::yesterday()->subDay(),
            'amount_paid' => 59_999.99,
            'total_amount' => 60_000.00,
        ]);

        $this->assertSame(Credit::STATUS_OVERDUE, $this->resolver->resolve($credit));
    }

    #[Test]
    public function it_marks_paid_when_installment_paid_with_minor_rounding_residue(): void
    {
        // amount_paid == total_amount exactly → PAID
        $credit = Credit::factory()->create([
            'status' => Credit::STATUS_ACTIVE,
            'due_date' => Carbon::today()->addDays(10),
        ]);

        // 3 installments of 33.33 + one 0.01 rounding correction
        foreach ([[1, '33.33'], [2, '33.33'], [3, '33.34']] as [$num, $amount]) {
            Installment::factory()->create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_number' => $num,
                'status' => Installment::STATUS_PAID,
                'due_date' => Carbon::today()->addDays(7),
                'amount_paid' => $amount,
                'total_amount' => $amount,
            ]);
        }

        $this->assertSame(Credit::STATUS_PAID, $this->resolver->resolve($credit));
    }
}
