<?php

declare(strict_types=1);

namespace Tests\Feature\Metrics;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Income;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

/**
 * `credify:verify-equity` concilia el patrimonio por dos caminos independientes.
 *
 * Nace de una auditoria donde el puente no cerraba por $450.000. Las tres causas
 * eran legitimas pero invisibles; aqui se comprueba que el comando las modela y
 * que una brecha real SI lo hace fallar.
 */
class VerifyEquityCommandTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private Client $client;

    private User $user;

    private ?int $partnerId = null;

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

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

    /** Un credito colocado: sale plata de la caja y nace la cartera. */
    private function colocar(float $capital, float $interes, int $cuotas = 1): Credit
    {
        $credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => $this->client->id,
            'collector_user_id' => $this->user->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => $capital,
            'installments_count' => $cuotas,
        ]);

        Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PENDING,
            'due_date' => now()->addDays(30)->toDateString(),
            'principal_amount' => $capital,
            'interest_amount' => $interes,
            'total_amount' => $capital + $interes,
            'amount_paid' => 0,
            'principal_balance_after' => 0,
        ]);

        $this->gasto('disbursement', $capital, ['credit_id' => $credit->id]);

        return $credit;
    }

    /** @param  array<string, mixed>  $extra */
    private function gasto(string $categoria, float $monto, array $extra = []): void
    {
        Expense::create(array_merge([
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'category' => $categoria,
            'expense_type' => 'operational',
            'amount' => $monto,
            'operation_date' => now()->toDateString(),
            'requires_approval' => false,
            'description' => 'Gasto de prueba',
        ], $extra));
    }

    /** @param  array<string, mixed>  $extra */
    private function ingreso(string $categoria, float $monto, array $extra = []): void
    {
        Income::create(array_merge([
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'category' => $categoria,
            'amount' => $monto,
            'operation_date' => now()->toDateString(),
            'description' => 'Ingreso de prueba',
        ], $extra));
    }

    /**
     * Aporte legítimo: pasa por el módulo de socios, así que deja el ingreso Y su
     * fila en `capital_contributions` apuntándole. Es la forma que tienen los 20
     * aportes reales de producción.
     */
    private function aportar(float $monto, ?string $fecha = null): void
    {
        $fecha ??= now()->toDateString();

        $income = Income::create([
            'company_id' => $this->company->id,
            'user_id' => $this->user->id,
            'category' => 'capital_injection',
            'amount' => $monto,
            'operation_date' => $fecha,
            'description' => 'Aporte de prueba',
        ]);

        DB::table('capital_contributions')->insert([
            'company_id' => $this->company->id,
            'partner_id' => $this->partnerId(),
            'income_id' => $income->id,
            'user_id' => $this->user->id,
            'amount' => $monto,
            'type' => 'additional',
            'method' => 'transfer',
            'operation_date' => $fecha,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    /**
     * Aporte suelto: plata que entra como capital sin socio detrás. Es la forma
     * exacta del ingreso #483 que encontró la auditoría.
     */
    private function aportarSuelto(float $monto): void
    {
        $this->ingreso('capital_injection', $monto);
    }

    private function partnerId(): int
    {
        return $this->partnerId ??= (int) DB::table('partners')->insertGetId([
            'company_id' => $this->company->id,
            'name' => 'Socio de prueba',
            'ownership_model' => 'fixed',
            'fixed_ownership_percentage' => 100,
            'status' => 'active',
            'joined_at' => now()->subYear()->toDateString(),
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    private function correr(int $tolerancia = 1): int
    {
        return $this->artisan('credify:verify-equity', [
            '--company' => $this->company->id,
            '--tolerance' => $tolerancia,
        ])->run();
    }

    #[Test]
    public function un_negocio_recien_capitalizado_cuadra(): void
    {
        $this->aportar(1000000);

        // Toda la plata en caja, nada colocado: patrimonio = aporte.
        $this->assertSame(0, $this->correr());
    }

    #[Test]
    public function cuadra_despues_de_colocar_un_credito(): void
    {
        $this->aportar(1000000);
        $this->colocar(capital: 400000, interes: 100000);

        // Salieron $400.000 de la caja y nacieron $400.000 de capital por cobrar.
        // El interes de $100.000 NO es patrimonio todavia: aun no se ha cobrado.
        $this->assertSame(0, $this->correr());
    }

    #[Test]
    public function un_sobrepago_no_se_confunde_con_utilidad(): void
    {
        $this->aportar(1000000);
        $credit = $this->colocar(capital: 400000, interes: 100000);

        // El modelo Income exige que un sobrepago cuelgue de un pago real.
        $payment = Payment::create([
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'amount' => 560000,
            'applied_amount' => 500000,
            'extra_amount' => 60000,
            'payment_date' => now()->toDateString(),
            'payment_method' => 'cash',
            'registered_by_user_id' => $this->user->id,
        ]);

        $installment = $credit->installments()->firstOrFail();
        $installment->update([
            'amount_paid' => 500000,
            'status' => Installment::STATUS_PAID,
        ]);

        // La imputacion es lo que le permite al comando saber cuanto de ese pago
        // fue interes (interes-primero) y cuanto devolucion de capital.
        $payment->installments()->attach($installment->id, ['applied_amount' => 500000]);

        $this->ingreso('payment', 500000, ['payment_id' => $payment->id, 'credit_id' => $credit->id]);

        // Plata que entro a la caja pero le pertenece al cliente.
        $this->ingreso('overpayment', 60000, ['payment_id' => $payment->id, 'credit_id' => $credit->id]);

        // Sin descontarlo, el comando reportaria $60.000 de patrimonio inexistente.
        $this->assertSame(0, $this->correr());
    }

    #[Test]
    public function una_condonacion_de_capital_reduce_el_patrimonio(): void
    {
        $this->aportar(1000000);
        $credit = $this->colocar(capital: 400000, interes: 100000);

        // El cliente paga solo el interes y se le perdona el capital.
        $credit->installments()->update([
            'amount_paid' => 500000,
            'forgiven_amount' => 400000,
            'status' => Installment::STATUS_PAID,
        ]);

        $this->gasto('interest_waiver', 400000, ['credit_id' => $credit->id]);

        // El enum trata la condonacion como si nunca tocara el capital; el comando
        // calcula el reparto real desde las cuotas e igual cuadra.
        $this->assertSame(0, $this->correr());
    }

    #[Test]
    public function detecta_un_aporte_de_capital_duplicado(): void
    {
        // El caso de produccion: $1.500.000 registrados dos veces el mismo dia por
        // el mismo socio. El puente NO lo ve, porque el error infla los dos lados
        // por igual: sube el capital aportado Y sube la caja. La brecha sigue en
        // cero y la conciliacion pasa — por eso hace falta este control aparte.
        $this->aportar(1000000);
        $this->aportar(1000000);

        $this->assertSame(1, $this->correr(), 'un aporte duplicado debe hacer fallar el comando');
    }

    #[Test]
    public function un_mismo_monto_en_fechas_distintas_no_es_sospechoso(): void
    {
        // Aportar la misma cifra en dos dias distintos es normal; solo la coincidencia
        // de monto Y fecha tiene la firma de un movimiento cargado dos veces.
        $this->aportar(1000000, now()->subDays(10)->toDateString());
        $this->aportar(1000000, now()->toDateString());

        $this->assertSame(0, $this->correr());
    }

    #[Test]
    public function detecta_capital_que_entra_sin_socio_vinculado(): void
    {
        // Forma exacta del ingreso #483 de produccion: entra como capital pero no
        // hay ninguna fila en capital_contributions que lo reclame.
        $this->aportarSuelto(1500000);

        $this->assertSame(1, $this->correr());
    }

    #[Test]
    public function una_brecha_real_hace_fallar_el_comando(): void
    {
        $this->aportar(1000000);
        $credit = $this->colocar(capital: 400000, interes: 100000);

        // Capital que se esfuma sin contrapartida: la cuota baja su saldo sin que
        // haya entrado plata ni se haya registrado una condonacion.
        $credit->installments()->update(['amount_paid' => 250000]);

        $this->assertSame(1, $this->correr(), 'el comando debe fallar ante una brecha sin explicacion');
    }
}
