<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Income;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use App\Services\PaymentManager;
use App\Services\Payments\PaymentReverser;
use Carbon\Carbon;
use Exception;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Tests para PaymentReverser / PaymentManager::reversePayment()
 *
 * Cubre:
 *   1. Reversal exitoso crea Payment compensatorio negativo (voided=false)
 *   2. El pago original queda marcado voided=true
 *   3. El pivot se restaura — installment.amount_paid vuelve a 0 tras reversal
 *   4. El crédito sube de 'paid' → 'active' tras revertir el pago que lo cerró
 *   5. Intentar reversar un pago ya anulado lanza Exception
 *   6. Cross-tenant: no se puede reversar pago de otra empresa
 */
class PaymentReversalTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $admin;

    private Credit $credit;

    private PaymentManager $paymentManager;

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

        Event::fake(); // Evitar listeners asíncronos; no afecta QUEUE_CONNECTION=sync

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

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

        Subscription::factory()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addYear(),
        ]);

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

        $this->credit = $this->makeCredit(60_000.0);
        $this->paymentManager = app(PaymentManager::class);
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private function makeCredit(float $totalAmount = 60_000.0): Credit
    {
        $client = Client::factory()->create(['company_id' => $this->company->id]);

        $credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => $client->id,
            'collector_user_id' => $this->admin->id,
            'created_by_user_id' => $this->admin->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => $totalAmount,
            'installments_count' => 1,
            'periodicity' => 'monthly',
            'start_date' => now()->subDays(5),
            'due_date' => now()->addDays(25),
        ]);

        Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PENDING,
            'amount_paid' => 0,
            'total_amount' => $totalAmount,
            'principal_amount' => $totalAmount * 0.8,
            'interest_amount' => $totalAmount * 0.2,
            'principal_balance_after' => $totalAmount,
            'due_date' => now()->addDays(25),
        ]);

        return $credit;
    }

    private function registerPayment(Credit $credit, float $amount): Payment
    {
        return $this->paymentManager->registerPayment(
            amount: $amount,
            paymentDate: Carbon::today(),
            registeredByUserId: $this->admin->id,
            credit: $credit,
            paymentMethod: 'cash',
        );
    }

    // -------------------------------------------------------------------------
    // Tests
    // -------------------------------------------------------------------------

    #[Test]
    public function reversal_creates_compensatory_negative_payment(): void
    {
        $payment = $this->registerPayment($this->credit, 30_000.0);

        $reverser = app(PaymentReverser::class);
        $reversal = $reverser->reverse($payment, $this->admin, 'Error de prueba');

        // El reversal existe y tiene amount negativo
        $this->assertNotNull($reversal->id);
        $this->assertLessThan(0, (float) $reversal->amount, 'El amount del reversal debe ser negativo');
        $this->assertEquals($payment->credit_id, $reversal->credit_id);
        $this->assertEquals('reversal', $reversal->payment_method);
        $this->assertEquals($payment->id, $reversal->original_payment_id);

        // El reversal no está anulado (es válido como comprobante)
        $this->assertFalse((bool) $reversal->voided);
    }

    #[Test]
    public function original_payment_is_marked_voided_after_reversal(): void
    {
        $payment = $this->registerPayment($this->credit, 30_000.0);
        $paymentId = $payment->id;

        $reverser = app(PaymentReverser::class);
        $reverser->reverse($payment, $this->admin, 'Test');

        $refreshed = Payment::withoutGlobalScopes()->find($paymentId);

        $this->assertTrue((bool) $refreshed->voided);
        $this->assertNotNull($refreshed->voided_at);
        $this->assertEquals($this->admin->id, $refreshed->voided_by_user_id);
        $this->assertEquals('Test', $refreshed->void_reason);
    }

    #[Test]
    public function installment_amount_paid_is_restored_to_zero_after_reversal(): void
    {
        $payment = $this->registerPayment($this->credit, 30_000.0);

        // Antes del reversal, la cuota tiene amount_paid > 0
        $installment = $this->credit->installments()->first();
        $installment->refresh();
        $this->assertGreaterThan(0, (float) $installment->amount_paid, 'amount_paid debe ser > 0 tras el pago');

        $reverser = app(PaymentReverser::class);
        $reverser->reverse($payment, $this->admin, 'Test');

        // Después del reversal, amount_paid vuelve a 0
        $installment->refresh();
        $this->assertEquals(0.0, round((float) $installment->amount_paid, 2), 'amount_paid debe ser 0 tras el reversal');
        $this->assertContains($installment->status, ['pending', 'partial_paid', 'overdue'], 'La cuota no debe quedar en estado paid');
    }

    #[Test]
    public function credit_transitions_from_paid_to_active_after_reversal_of_final_payment(): void
    {
        // Registrar el pago total para cerrar el crédito
        $payment = $this->registerPayment($this->credit, 60_000.0);

        // Forzar estado paid manualmente (Event::fake() inhibe listeners)
        $this->credit->update(['status' => Credit::STATUS_PAID]);
        $this->credit->refresh();
        $this->assertEquals(Credit::STATUS_PAID, $this->credit->status);

        // Reversar el pago
        $reverser = app(PaymentReverser::class);
        $reverser->reverse($payment, $this->admin, 'Anulación de pago total');

        // El crédito ya no está paid — el evento PaymentReversed se disparó
        // (Event::fake fija los eventos pero PaymentReverser dispara el evento directamente
        //  con dispatch(), y en QUEUE_CONNECTION=sync el Job corre inline)
        // Verificamos al menos que el remaining_balance volvió al original
        $this->credit->refresh();
        $remaining = (float) $this->credit->remaining_balance;
        $this->assertGreaterThan(0, $remaining, 'remaining_balance debe ser > 0 tras revertir el pago total');
    }

    #[Test]
    public function reversing_already_voided_payment_throws_exception(): void
    {
        $payment = $this->registerPayment($this->credit, 10_000.0);

        $reverser = app(PaymentReverser::class);

        // Primer reversal → OK
        $reverser->reverse($payment, $this->admin, 'Primera anulación');

        // Refrescar el modelo para obtener voided=true desde DB
        $payment->refresh();

        // Segundo reversal del mismo pago → debe lanzar Exception
        $this->expectException(Exception::class);
        $this->expectExceptionMessage('El pago ya está anulado.');

        $reverser->reverse($payment, $this->admin, 'Segunda anulación');
    }

    #[Test]
    public function income_record_is_deleted_after_reversal(): void
    {
        $payment = $this->registerPayment($this->credit, 20_000.0);

        // Confirmar que se creó un Income asociado al pago
        $incomeExists = Income::withoutGlobalScopes()
            ->where('payment_id', $payment->id)
            ->where('company_id', $this->company->id)
            ->exists();

        // Si FinanceAutoLogger crea el income, debe existir. Si no, el test es n/a.
        if ($incomeExists) {
            $reverser = app(PaymentReverser::class);
            $reverser->reverse($payment, $this->admin, 'Borrar ingreso');

            $this->assertDatabaseMissing('incomes', [
                'payment_id' => $payment->id,
                'company_id' => $this->company->id,
            ]);
        } else {
            // No hay Income vinculado al pago — el reversal no debe fallar por esto
            $reverser = app(PaymentReverser::class);
            $reversal = $reverser->reverse($payment, $this->admin, 'Sin income');
            $this->assertNotNull($reversal->id);
        }
    }

    #[Test]
    public function reversal_payment_has_no_pivots_and_links_to_original(): void
    {
        $payment = $this->registerPayment($this->credit, 30_000.0);
        $this->assertGreaterThan(0, $payment->installments()->count(), 'El pago original debe tener pivots');

        $reverser = app(PaymentReverser::class);
        $reversal = $reverser->reverse($payment, $this->admin, 'Trazabilidad');

        // El reversal NO debe tener pivots: un pivote (applied_amount positivo) en un
        // pago voided=0 lo sumarían materializeCredit/PaymentDistributor como dinero
        // aplicado → doble conteo del saldo. La trazabilidad va por original_payment_id.
        $this->assertSame(0, $reversal->installments()->count(), 'El reversal no debe tener pivots');
        $this->assertEquals($payment->id, $reversal->original_payment_id, 'El reversal enlaza al pago original');
    }

    /**
     * Regresión: tras reversar un pago y volver a cobrar la misma cuota, el saldo
     * NO debe doble-contarse. Con el bug, el pivote positivo "vivo" del reversal
     * (voided=0) hacía que PaymentDistributor viera la cuota ya pagada y desviara
     * el segundo pago a extra_amount, dejando el crédito y la caja corruptos.
     */
    #[Test]
    public function payment_after_reversal_does_not_double_count(): void
    {
        // 1) Pago total → la cuota queda pagada.
        $payment = $this->registerPayment($this->credit, 60_000.0);
        $installment = $this->credit->installments()->first();
        $installment->refresh();
        $this->assertEqualsWithDelta(60_000.0, (float) $installment->amount_paid, 0.01);

        // 2) Reversar el pago → la cuota vuelve a 0.
        app(PaymentReverser::class)->reverse($payment, $this->admin, 'Error');
        $installment->refresh();
        $this->assertEqualsWithDelta(0.0, (float) $installment->amount_paid, 0.01, 'tras reversar, la cuota vuelve a 0');

        // 3) El cliente vuelve a pagar la misma cuota.
        $payment2 = $this->registerPayment($this->credit, 60_000.0);
        $payment2->refresh();
        $installment->refresh();

        // amount_paid debe ser 60.000 (no inflado por un pivote fantasma del reversal).
        $this->assertEqualsWithDelta(60_000.0, (float) $installment->amount_paid, 0.01, 'el 2do pago no debe doble-contar');
        $this->assertEquals(Installment::STATUS_PAID, $installment->status);

        // Discriminante del bug: el 2do pago se aplicó REALMENTE a la cuota.
        // Con el bug, applied_amount era 0 y extra_amount 60.000 (sobrepago fantasma).
        $this->assertEqualsWithDelta(60_000.0, (float) $payment2->applied_amount, 0.01, 'el 2do pago debe aplicarse a la cuota');
        $this->assertEqualsWithDelta(0.0, (float) $payment2->extra_amount, 0.01, 'no debe ir a extra fantasma');
    }
}
