<?php

declare(strict_types=1);

namespace Tests\Feature\Filament;

use App\Filament\Resources\Credits\Pages\ViewCredit;
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\PaymentManager;
use Carbon\Carbon;
use Filament\Facades\Filament;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * La vista del crédito (ViewCredit) permite ANULAR un pago directamente desde la
 * tabla de pagos: cada fila efectiva dispara `mountAction('voidPayment', {payment})`,
 * que reutiliza el mismo mecanismo seguro que `/admin/payments`
 * (PaymentManager::canDeletePayment + deletePayment). "Anular" = REVERSAR (asiento
 * compensatorio + `voided=1`), nunca un DELETE físico del registro contable.
 */
class CreditViewVoidPaymentTest extends TestCase
{
    use DatabaseTransactions;

    /** @return array{0: Company, 1: User, 2: Credit} */
    private function creditWithInstallments(): array
    {
        Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);

        $company = Company::factory()->create(['interest_method' => 'flat_rate']);
        $user = User::factory()->create(['company_id' => $company->id]);
        $user->assignRole('admin');
        $this->actingAs($user);
        Filament::setCurrentPanel(Filament::getDefaultPanel());

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

        $credit = Credit::create([
            'company_id' => $company->id,
            'client_id' => $client->id,
            'created_by_user_id' => $user->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 100000,
            'interest_rate' => 20,
            'installments_count' => 4,
            'periodicity' => 'weekly',
            'start_date' => Carbon::now(),
        ]);

        for ($i = 1; $i <= 4; $i++) {
            Installment::create([
                'company_id' => $company->id,
                'credit_id' => $credit->id,
                'installment_number' => $i,
                'status' => Installment::STATUS_PENDING,
                'due_date' => Carbon::now()->addWeeks($i),
                'principal_amount' => 25000,
                'interest_amount' => 5000,
                'total_amount' => 30000,
                'principal_balance_after' => 30000,
                'amount_paid' => 0,
            ]);
        }

        return [$company, $user, $credit->fresh()];
    }

    #[Test]
    public function an_active_payment_can_be_voided_from_the_credit_view(): void
    {
        [$company, $user, $credit] = $this->creditWithInstallments();

        // Pago real registrado por el flujo normal (se distribuye a las cuotas).
        $payment = app(PaymentManager::class)->registerPaymentForCredit(
            $credit,
            30000,
            Carbon::now(),
            $user->id,
            'cash',
        );

        $this->assertTrue($credit->fresh()->hasPayments(), 'el crédito parte con un pago efectivo');

        $component = Livewire::test(ViewCredit::class, ['record' => $credit->getRouteKey()]);

        // La tabla de pagos de la vista renderiza el botón "Anular" cableado al action.
        $component->assertSee('Anular');
        $component->assertSeeHtml("mountAction('voidPayment', { payment: {$payment->id} })");

        $component->callAction(
            'voidPayment',
            ['reason' => 'Pago mal registrado, se anula para corregir el plan.'],
            ['payment' => $payment->id],
        )->assertHasNoActionErrors();

        // El pago original quedó ANULADO (no eliminado) y con motivo auditado.
        $original = $payment->fresh();
        $this->assertTrue((bool) $original->voided, 'el pago original debe quedar marcado como anulado');

        // Se generó el asiento de reversión que compensa el pago.
        $this->assertDatabaseHas('payments', [
            'credit_id' => $credit->id,
            'payment_method' => 'reversal',
            'original_payment_id' => $payment->id,
        ]);

        // El pago original NO se borró físicamente (integridad del ledger).
        $this->assertDatabaseHas('payments', ['id' => $payment->id]);

        // El payoff de #387: sin pagos EFECTIVOS, el crédito vuelve a ser editable.
        $this->assertFalse($credit->fresh()->hasPayments(), 'tras anular su único pago el crédito es editable de nuevo');
    }

    #[Test]
    public function the_action_refuses_a_payment_that_does_not_belong_to_the_viewed_credit(): void
    {
        [$company, $user, $creditA] = $this->creditWithInstallments();

        // Segundo crédito de la MISMA compañía con su propio pago.
        $client = Client::factory()->create(['company_id' => $company->id]);
        $creditB = Credit::create([
            'company_id' => $company->id,
            'client_id' => $client->id,
            'created_by_user_id' => $user->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => 50000,
            'interest_rate' => 20,
            'installments_count' => 2,
            'periodicity' => 'weekly',
            'start_date' => Carbon::now(),
        ]);
        for ($i = 1; $i <= 2; $i++) {
            Installment::create([
                'company_id' => $company->id,
                'credit_id' => $creditB->id,
                'installment_number' => $i,
                'status' => Installment::STATUS_PENDING,
                'due_date' => Carbon::now()->addWeeks($i),
                'principal_amount' => 15000,
                'interest_amount' => 3000,
                'total_amount' => 30000,
                'principal_balance_after' => 30000,
                'amount_paid' => 0,
            ]);
        }

        $paymentB = app(PaymentManager::class)->registerPaymentForCredit(
            $creditB->fresh(),
            30000,
            Carbon::now(),
            $user->id,
            'cash',
        );

        // Desde la vista del crédito A, intentar anular un pago del crédito B.
        Livewire::test(ViewCredit::class, ['record' => $creditA->getRouteKey()])
            ->callAction(
                'voidPayment',
                ['reason' => 'Intento cruzado'],
                ['payment' => $paymentB->id],
            )
            ->assertHasNoActionErrors();

        // El pago del crédito B sigue intacto: el guard por crédito lo impidió.
        $this->assertFalse((bool) $paymentB->fresh()->voided, 'no se puede anular un pago ajeno al crédito visto');
        $this->assertDatabaseMissing('payments', [
            'credit_id' => $creditB->id,
            'payment_method' => 'reversal',
            'original_payment_id' => $paymentB->id,
        ]);
    }

    #[Test]
    public function a_collector_cannot_void_payments_from_the_credit_view(): void
    {
        [$company, $admin, $credit] = $this->creditWithInstallments();

        // Pago registrado por el admin sobre el crédito.
        $payment = app(PaymentManager::class)->registerPaymentForCredit(
            $credit,
            30000,
            Carbon::now(),
            $admin->id,
            'cash',
        );

        // Un cobrador de la misma compañía, dueño del crédito (para poder verlo).
        Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);
        $collector = User::factory()->create(['company_id' => $company->id]);
        $collector->assignRole('collector');
        $credit->update(['collector_user_id' => $collector->id]);

        $this->actingAs($collector);
        Filament::setCurrentPanel(Filament::getDefaultPanel());

        $component = Livewire::test(ViewCredit::class, ['record' => $credit->getRouteKey()]);

        // El cobrador NO ve el botón "Anular" (la columna de acciones se oculta para él).
        $component->assertDontSeeHtml("mountAction('voidPayment'");

        // La acción tampoco es invocable: Filament rechaza montar una acción no visible.
        // (`callAction` de una acción oculta falla la aserción de visibilidad — por eso
        // no se prueba aquí; el bloqueo lo garantizan `visible()` + el guard de rol.)
        $this->assertFalse((bool) $payment->fresh()->voided, 'ver el crédito no anula ningún pago');
    }
}
