<?php

declare(strict_types=1);

namespace Tests\Feature\Filament;

use App\Enums\CreditAuditEvent;
use App\Filament\Resources\Credits\Pages\ListCredits;
use App\Filament\Resources\Credits\Pages\ViewCredit;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\CreditAuditLog;
use App\Models\Installment;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Archivar retira un crédito de la cartera operativa sin cerrarlo.
 *
 * Es para los créditos cuyo desenlace aún no está decidido y que mientras tanto
 * ensucian la ruta del cobrador. NO toca ningún registro financiero: no borra el
 * desembolso, no condona, no reescribe cuotas.
 *
 * Lo que sí implica —y es la parte que hay que fijar con tests— es que el saldo
 * sale de la cartera y no se pueden registrar pagos, pero la deuda sigue siendo
 * un activo para la conciliación de patrimonio.
 */
class ArchivarCreditoTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $admin;

    private User $collector;

    private Credit $credit;

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

        Cache::flush();

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

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

        Subscription::factory()->create([
            'company_id' => $this->company->id,
            'plan_id' => Plan::factory()->create()->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->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');

        $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' => 2,
        ]);

        // Dos cuotas de $70.000: una cobrada, otra pendiente. Saldo vivo: $70.000.
        Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'installment_number' => 1,
            'status' => Installment::STATUS_PAID,
            'due_date' => now()->subDays(7)->toDateString(),
            'principal_amount' => 50000,
            'interest_amount' => 20000,
            'total_amount' => 70000,
            'amount_paid' => 70000,
            'principal_balance_after' => 450000,
        ]);

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

    private function archivar(): void
    {
        Livewire::test(ViewCredit::class, ['record' => $this->credit->getRouteKey()])
            ->callAction('archive_credit', ['reason' => 'el cliente desapareció, no sabemos si cerrarlo'])
            ->assertHasNoActionErrors();
    }

    #[Test]
    public function archivar_saca_el_credito_de_la_cartera_del_cobrador(): void
    {
        $this->actingAs($this->admin);

        $this->assertTrue(
            Credit::query()->where('company_id', $this->company->id)->activeLeafCredits()
                ->whereKey($this->credit->id)->exists()
        );

        $this->archivar();

        $this->assertSame(Credit::STATUS_ARCHIVED, $this->credit->fresh()->status);
        $this->assertFalse(
            Credit::query()->where('company_id', $this->company->id)->activeLeafCredits()
                ->whereKey($this->credit->id)->exists(),
            'el crédito archivado sigue en la cartera operativa'
        );
    }

    #[Test]
    public function archivar_no_toca_ningun_registro_financiero(): void
    {
        $this->actingAs($this->admin);

        $cuotasAntes = $this->credit->installments()->get()
            ->map(fn ($i) => [$i->status, (float) $i->amount_paid, (float) $i->total_amount])
            ->toArray();

        $this->archivar();

        $cuotasDespues = $this->credit->fresh()->installments()->get()
            ->map(fn ($i) => [$i->status, (float) $i->amount_paid, (float) $i->total_amount])
            ->toArray();

        // Archivar esconde, no condona: las cuotas quedan exactamente igual.
        $this->assertSame($cuotasAntes, $cuotasDespues);
    }

    #[Test]
    public function el_credito_archivado_no_puede_recibir_pagos(): void
    {
        $this->actingAs($this->admin);
        $this->archivar();

        $this->assertFalse(
            in_array($this->credit->fresh()->status, Credit::ACTIVE_STATUSES, true),
            'un crédito archivado no debe aceptar pagos'
        );
    }

    #[Test]
    public function queda_registrado_quien_lo_archivo_y_por_que(): void
    {
        $this->actingAs($this->admin);
        $this->archivar();

        $log = CreditAuditLog::where('credit_id', $this->credit->id)
            ->where('action', CreditAuditEvent::CREDIT_ARCHIVED->value)
            ->firstOrFail();

        // Dentro de tres meses alguien va a preguntar por qué este crédito no
        // está en la ruta. La respuesta tiene que estar aquí.
        $entrada = (array) (($log->data ?? [])['input'] ?? []);

        $this->assertStringContainsString('desapareció', (string) ($entrada['reason'] ?? ''));
        $this->assertSame(70000.0, (float) ($entrada['saldo_retirado_de_cartera'] ?? 0));
        $this->assertSame($this->admin->id, $log->user_id);
    }

    #[Test]
    public function desarchivar_recalcula_el_estado_segun_las_cuotas(): void
    {
        $this->actingAs($this->admin);
        $this->archivar();

        // Mientras estuvo archivado el tiempo siguió corriendo: la cuota vence.
        $this->credit->installments()
            ->where('installment_number', 2)
            ->update(['due_date' => now()->subDays(20)->toDateString()]);

        Livewire::test(ViewCredit::class, ['record' => $this->credit->getRouteKey()])
            ->callAction('unarchive_credit')
            ->assertHasNoActionErrors();

        $estado = $this->credit->fresh()->status;

        $this->assertNotSame(Credit::STATUS_ARCHIVED, $estado);
        $this->assertContains(
            $estado,
            Credit::ACTIVE_STATUSES,
            'al desarchivar, el estado debe recalcularse desde las cuotas y volver a la cartera'
        );
    }

    #[Test]
    public function el_cobrador_no_puede_archivar(): void
    {
        // Archivar mueve plata fuera de la cartera: es decisión del dueño, no de
        // quien está en la calle.
        $this->assertFalse($this->collector->can('archive', $this->credit));
        $this->assertTrue($this->admin->can('archive', $this->credit));
    }

    #[Test]
    public function no_se_puede_archivar_un_credito_ya_cerrado(): void
    {
        $this->credit->update(['status' => Credit::STATUS_PAID]);

        $this->assertFalse(
            $this->admin->can('archive', $this->credit->fresh()),
            'un crédito cerrado ya está fuera de la cartera: archivarlo no significa nada'
        );
    }

    #[Test]
    public function aparece_en_su_propia_pestana_y_no_entre_los_cerrados(): void
    {
        $this->actingAs($this->admin);
        $this->archivar();

        Livewire::test(ListCredits::class, ['activeTab' => 'archivados'])
            ->assertCanSeeTableRecords([$this->credit]);

        Livewire::test(ListCredits::class, ['activeTab' => 'cerrados'])
            ->assertCanNotSeeTableRecords([$this->credit]);

        Livewire::test(ListCredits::class, ['activeTab' => 'en_curso'])
            ->assertCanNotSeeTableRecords([$this->credit]);
    }

    #[Test]
    public function archivar_no_abre_una_brecha_en_la_conciliacion_de_patrimonio(): void
    {
        $this->actingAs($this->admin);

        // El invariante que más importa: archivar es una operación legítima y no
        // puede hacer fallar el control nocturno. La deuda sigue siendo un activo
        // aunque salga de la cartera operativa.
        $antes = $this->capitalPorRecuperar();

        $this->archivar();

        $this->assertSame(
            $antes,
            $this->capitalPorRecuperar(),
            'el capital archivado dejó de contarse y la conciliación se rompería'
        );
    }

    /** Replica lo que mide `credify:verify-equity` como capital por recuperar. */
    private function capitalPorRecuperar(): float
    {
        $recuperables = [...Credit::ACTIVE_STATUSES, Credit::STATUS_ARCHIVED];

        return (float) Installment::query()
            ->join('credits as c', 'c.id', '=', 'installments.credit_id')
            ->where('c.company_id', $this->company->id)
            ->whereIn('c.status', $recuperables)
            ->whereIn('installments.status', ['pending', 'partial_paid', 'overdue'])
            ->sum(\DB::raw('GREATEST(0, installments.principal_amount - GREATEST(0, installments.amount_paid - installments.interest_amount))'));
    }
}
