<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\DTOs\RenewCreditData;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\User;
use App\Services\Credits\CreditOperationOrchestrator;
use App\Services\Credits\CreditOperationValidator;
use App\Support\CreditRules;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * #95 — La renovación debe permitir créditos con capital pagado (es su caso típico:
 *       un cliente que ya amortizó y quiere un préstamo nuevo). Antes `validateForRenewal`
 *       llamaba `assertNoCapitalPaid()` y a la vez exigía pagos → contradicción inalcanzable.
 *
 * #96.3 — `hasCapitalPayments` ahora es preciso: detecta abonos parciales que exceden la
 *         porción de interés (amortizan capital), no solo las cuotas 100% saldadas.
 */
class RenewalCapitalPolicyTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $user;

    protected function setUp(): void
    {
        parent::setUp();

        $this->company = Company::factory()->create(['interest_method' => 'flat_rate']);
        $this->user = User::factory()->create(['company_id' => $this->company->id]);
        $this->actingAs($this->user);
    }

    /** Crédito flat-rate con las primeras N cuotas 100% pagadas (capital amortizado). */
    private function createCreditWithPaidInstallments(
        int $totalInstallments,
        int $paidInstallments,
        float $amount,
        float $interestRate,
    ): Credit {
        $total = $amount * (1 + $interestRate / 100);
        $perInstallment = $total / $totalInstallments;

        $client = Client::factory()->create(['company_id' => $this->company->id]);

        $credit = Credit::create([
            'company_id' => $this->company->id,
            'client_id' => $client->id,
            'collector_user_id' => $this->user->id,
            'created_by_user_id' => $this->user->id,
            'status' => Credit::STATUS_DELAYED,
            'amount' => $amount,
            'interest_rate' => $interestRate,
            'installments_count' => $totalInstallments,
            'periodicity' => 'weekly',
            'start_date' => Carbon::now()->subWeeks($totalInstallments),
        ]);

        for ($i = 1; $i <= $totalInstallments; $i++) {
            $isPaid = $i <= $paidInstallments;
            Installment::create([
                'company_id' => $this->company->id,
                'credit_id' => $credit->id,
                'installment_number' => $i,
                'status' => $isPaid ? Installment::STATUS_PAID : Installment::STATUS_PENDING,
                'due_date' => Carbon::now()->subWeeks($totalInstallments - $i),
                'paid_date' => $isPaid ? Carbon::now()->subWeeks($totalInstallments - $i) : null,
                'principal_amount' => $amount / $totalInstallments,
                'interest_amount' => ($amount * $interestRate / 100) / $totalInstallments,
                'total_amount' => $perInstallment,
                'principal_balance_after' => $isPaid ? 0 : $perInstallment,
                'amount_paid' => $isPaid ? $perInstallment : 0,
            ]);
        }

        return $credit->fresh(['installments']);
    }

    /** Crédito con UNA cuota parcial: total = principal + interés, con amount_paid dado. */
    private function createCreditWithSinglePartialInstallment(
        float $principal,
        float $interest,
        float $amountPaid,
    ): Credit {
        $client = Client::factory()->create(['company_id' => $this->company->id]);
        $total = $principal + $interest;

        $credit = Credit::create([
            'company_id' => $this->company->id,
            'client_id' => $client->id,
            'collector_user_id' => $this->user->id,
            'created_by_user_id' => $this->user->id,
            'status' => Credit::STATUS_DELAYED,
            'amount' => $principal,
            'interest_rate' => ($interest / $principal) * 100,
            'installments_count' => 1,
            'periodicity' => 'weekly',
            'start_date' => Carbon::now()->subWeek(),
        ]);

        Installment::create([
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PARTIAL_PAID,
            'due_date' => Carbon::now(),
            'principal_amount' => $principal,
            'interest_amount' => $interest,
            'total_amount' => $total,
            'principal_balance_after' => $total - $amountPaid,
            'amount_paid' => $amountPaid,
        ]);

        return $credit->fresh(['installments']);
    }

    #[Test]
    public function renewal_is_allowed_for_a_credit_with_capital_paid(): void
    {
        // 500k @ 40% → total 700k, 10 cuotas de 70k. 6 pagadas (capital amortizado),
        // 4 pendientes → saldo 280k. Antes: renovación RECHAZADA por assertNoCapitalPaid.
        $credit = $this->createCreditWithPaidInstallments(
            totalInstallments: 10,
            paidInstallments: 6,
            amount: 500000,
            interestRate: 40,
        );

        $this->assertTrue($credit->hasCapitalPayments(), 'El crédito debe tener capital pagado (precondición).');

        // El orquestador ahora SÍ permite renovar.
        $this->assertTrue(
            app(CreditOperationOrchestrator::class)->canRenew($credit),
            'canRenew debe ser true para un crédito con capital pagado (#95).',
        );

        // Y el validador no lanza; devuelve el saldo pendiente (~280k).
        $data = new RenewCreditData(
            creditId: $credit->id,
            newInstallmentsCount: 12,
            newCreditAmount: 500000, // >= saldo pendiente
            reason: 'Cliente puntual quiere un préstamo nuevo',
        );

        $balance = app(CreditOperationValidator::class)->validateForRenewal($credit, $data);
        $this->assertEqualsWithDelta(280000, (float) $balance->value(), 0.01);
    }

    #[Test]
    public function has_capital_payments_detects_partial_payment_beyond_interest(): void
    {
        // Cuota 60k (50k capital + 10k interés). Abona 30k → supera el interés → amortizó capital.
        $credit = $this->createCreditWithSinglePartialInstallment(
            principal: 50000,
            interest: 10000,
            amountPaid: 30000,
        );

        $this->assertTrue(
            CreditRules::hasCapitalPayments($credit),
            'Un abono parcial que excede el interés debe contar como capital pagado (#96.3).',
        );
    }

    #[Test]
    public function partial_payment_within_interest_is_not_capital(): void
    {
        // Cuota 60k (50k capital + 10k interés). Abona 8k (< interés) → solo interés, sin capital.
        $credit = $this->createCreditWithSinglePartialInstallment(
            principal: 50000,
            interest: 10000,
            amountPaid: 8000,
        );

        $this->assertFalse(
            CreditRules::hasCapitalPayments($credit),
            'Un abono menor o igual al interés NO debe contar como capital pagado.',
        );
    }

    #[Test]
    public function normal_extension_is_blocked_but_extend_with_interest_allowed_for_partial_capital(): void
    {
        // Abono parcial que amortiza capital → la extensión SIMPLE queda bloqueada
        // (empujando al usuario a extend-with-interest, la operación correcta).
        $credit = $this->createCreditWithSinglePartialInstallment(
            principal: 50000,
            interest: 10000,
            amountPaid: 30000,
        );

        $orchestrator = app(CreditOperationOrchestrator::class);
        $this->assertFalse($orchestrator->canExtend($credit), 'Extensión simple bloqueada con capital parcial pagado (#96.3).');
        $this->assertTrue($orchestrator->canExtendWithInterest($credit), 'Extend-with-interest sí permitida.');
    }
}
