<?php

declare(strict_types=1);

namespace Tests\Feature\Payments;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\User;
use App\Services\Dashboard\AdminDashboardMetricsService;
use App\Services\Metrics\CollectionMetricsService;
use App\Services\Metrics\PortfolioMetricsService;
use App\Services\Payments\PaymentReverser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Anular un pago debe dejar las metricas EXACTAMENTE como estaban antes.
 *
 * `PaymentReverser` deja dos filas: la original con `voided = 1` y un asiento
 * compensatorio de monto NEGATIVO que NO queda marcado como anulado. Filtrar
 * solo por `voided = false` restaba el monto dos veces — excluia la original y
 * encima sumaba el negativo.
 *
 * En produccion subestimaba los cobros: junio -$284.667, julio -$132.000,
 * agosto -$190.000. La caja nunca se vio afectada porque al anular se borra el
 * ingreso; el dano vivia solo en las sumas sobre `payments`.
 *
 * El test recorre las metricas de las tres superficies (panel, PWA, repositorio)
 * para que el proximo sitio que sume pagos tenga que pasar por aqui.
 */
class ReversalNoCuentaComoCobroTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $collector;

    private User $admin;

    private Credit $credit;

    private Installment $installment;

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

        foreach (['admin', 'collector'] as $role) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
        }

        $this->company = Company::factory()->create();

        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');

        $this->admin = User::factory()->create(['company_id' => $this->company->id]);
        $this->admin->assignRole('admin');

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

        $this->credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => $client->id,
            'collector_user_id' => $this->collector->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 500000,
            'installments_count' => 10,
        ]);

        $this->installment = Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PENDING,
            'due_date' => now()->toDateString(),
            'principal_amount' => 50000,
            'interest_amount' => 20000,
            'total_amount' => 70000,
            'amount_paid' => 0,
            'principal_balance_after' => 450000,
        ]);
    }

    /** Registra un pago imputado a la cuota, como lo haria el flujo real. */
    private function registrarPago(float $monto): Payment
    {
        $payment = Payment::create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'installment_id' => $this->installment->id,
            'amount' => $monto,
            'applied_amount' => $monto,
            'extra_amount' => 0,
            'payment_date' => now()->toDateString(),
            'payment_method' => 'cash',
            'registered_by_user_id' => $this->collector->id,
        ]);

        $payment->installments()->attach($this->installment->id, ['applied_amount' => $monto]);

        $this->installment->update([
            'amount_paid' => $monto,
            'status' => Installment::STATUS_PARTIAL_PAID,
        ]);

        return $payment;
    }

    private function anular(Payment $payment): void
    {
        app(PaymentReverser::class)->reverse($payment, $this->admin, 'cuota mal ingresada');
    }

    #[Test]
    public function la_anulacion_deja_dos_filas_pero_ninguna_es_dinero_recibido(): void
    {
        $payment = $this->registrarPago(70000);
        $this->anular($payment);

        // La forma exacta del problema: dos filas, una anulada y una negativa viva.
        $this->assertSame(2, Payment::where('credit_id', $this->credit->id)->count());
        $this->assertTrue($payment->fresh()->voided);

        $reversal = Payment::where('original_payment_id', $payment->id)->firstOrFail();
        $this->assertFalse((bool) $reversal->voided, 'el asiento de reversion NO queda marcado como anulado');
        $this->assertSame(-70000.0, (float) $reversal->amount);

        // El filtro ingenuo resta dos veces: -$70.000 en vez de $0.
        $ingenuo = (float) Payment::where('credit_id', $this->credit->id)->where('voided', false)->sum('amount');
        $this->assertSame(-70000.0, $ingenuo, 'asi se veia el bug');

        // El scope da la respuesta correcta.
        $correcto = (float) Payment::where('credit_id', $this->credit->id)->collections()->sum('amount');
        $this->assertSame(0.0, $correcto);
    }

    #[Test]
    public function el_cobrado_de_hoy_del_panel_vuelve_a_cero(): void
    {
        $metrics = app(AdminDashboardMetricsService::class);

        $antes = $metrics->getMetrics($this->company->id)['collected_today'];

        $payment = $this->registrarPago(70000);
        $this->assertSame(70000.0, $metrics->getMetrics($this->company->id)['collected_today']['amount']);

        $this->anular($payment);
        $despues = $metrics->getMetrics($this->company->id)['collected_today'];

        $this->assertSame($antes['amount'], $despues['amount']);
        $this->assertSame($antes['count'], $despues['count']);
    }

    #[Test]
    public function las_metricas_de_cobranza_vuelven_al_valor_previo(): void
    {
        $collection = app(CollectionMetricsService::class);
        $portfolio = app(PortfolioMetricsService::class);

        $hoy = now();
        // Estas metricas cachean: sin limpiar, el "despues" devolveria el "antes".
        $medir = function () use ($collection, $portfolio, $hoy): array {
            Cache::flush();

            return [
                'equipo_hoy' => $collection->getTeamSummary($this->company->id, $this->admin->id)['collected_today'] ?? null,
                'cartera_cobrado' => $portfolio->getCollectedAmount($this->company->id, $hoy->copy()->startOfMonth(), $hoy),
                'cobrado_dia' => $portfolio->getCollectedAmount($this->company->id, $hoy->copy()->startOfDay(), $hoy),
                'ultimo_pago' => $this->credit->fresh()->latestPayment?->amount,
            ];
        };

        $antes = $medir();

        $payment = $this->registrarPago(70000);
        $this->anular($payment);

        $this->assertEquals($antes, $medir(), 'alguna metrica quedo contaminada por el asiento de reversion');
    }

    #[Test]
    public function el_ultimo_pago_del_credito_nunca_es_un_monto_negativo(): void
    {
        // Antes: al anular el unico pago, el "ultimo pago" del cliente pasaba a ser
        // la reversion, o sea -$70.000 en pantalla.
        $payment = $this->registrarPago(70000);
        $this->anular($payment);

        $this->assertNull(
            $this->credit->fresh()->latestPayment,
            'tras anular el unico pago no debe quedar ningun ultimo pago'
        );

        $otro = $this->registrarPago(30000);
        $this->assertSame(30000.0, (float) $this->credit->fresh()->latestPayment->amount);
        $this->assertGreaterThan(0, (float) $this->credit->fresh()->latestPayment->amount);
    }

    #[Test]
    public function el_credito_vuelve_a_ser_editable_tras_anular_su_unico_pago(): void
    {
        // Es el motivo por el que se anula un pago casi siempre: poder corregir el
        // credito. Contar la reversion como pago lo dejaba en solo lectura (#387).
        $payment = $this->registrarPago(70000);
        $this->anular($payment);

        $this->assertFalse(
            $this->credit->fresh()->hasPayments(),
            'el credito quedo bloqueado por un asiento que no es dinero recibido'
        );
    }

    #[Test]
    public function el_ritmo_mensual_de_la_pwa_no_resta_la_reversion(): void
    {
        $this->credit->update(['collector_user_id' => $this->collector->id]);

        $payment = $this->registrarPago(70000);
        $this->anular($payment);

        $cobradoMes = (float) Payment::query()
            ->withoutGlobalScopes()
            ->where('company_id', $this->company->id)
            ->collections()
            ->whereBetween('payment_date', [now()->startOfMonth()->toDateString(), now()->toDateString()])
            ->sum('amount');

        $this->assertSame(0.0, $cobradoMes);
    }
}
