<?php

declare(strict_types=1);

namespace Tests\Feature\Filament;

use App\Filament\Resources\Credits\Pages\ListCredits;
use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
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;

/**
 * Las tres cubetas del listado de créditos de Filament.
 *
 * El filtro viejo ("Archivados") definía archivado como
 * `status IN ('paid','canceled')` — 2 de los 12 estados reales — así que la
 * vista por defecto dejaba pasar los créditos extendidos, refinanciados,
 * renovados y reestructurados. Esos ya fueron sustituidos por un hijo, de modo
 * que el mismo préstamo aparecía dos veces en la lista.
 *
 * Aquí se fija el contrato de los scopes que alimentan las pestañas, y sobre
 * todo el invariante que importa: las tres cubetas parten el universo sin
 * solaparse ni dejar huecos.
 */
class CreditListTabsTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private Client $client;

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

        // hasActiveSubscription() cachea: sin esto el panel redirige a /suscripcion.
        Cache::flush();

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

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

        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(),
        ]);
    }

    private function credit(string $status, ?Credit $parent = null): Credit
    {
        return Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => $this->client->id,
            'status' => $status,
            'parent_credit_id' => $parent?->id,
        ]);
    }

    /**
     * Query acotada a la empresa del test: la BD de dev es copia de producción.
     *
     * @return Builder<Credit>
     */
    private function scoped(): Builder
    {
        return Credit::query()->where('company_id', $this->company->id);
    }

    #[Test]
    public function en_curso_trae_los_estados_activos_sin_hijo(): void
    {
        $activo = $this->credit(Credit::STATUS_ACTIVE);
        $mora = $this->credit(Credit::STATUS_DELAYED);
        $vencido = $this->credit(Credit::STATUS_OVERDUE);
        $this->credit(Credit::STATUS_PAID);

        $ids = $this->scoped()->activeLeafCredits()->pluck('id')->all();

        $this->assertEqualsCanonicalizing([$activo->id, $mora->id, $vencido->id], $ids);
    }

    #[Test]
    public function en_curso_excluye_un_credito_activo_que_ya_tiene_hijo(): void
    {
        // El caso raro pero real: un padre que se quedó en estado activo. Si la
        // cubeta se decidiera solo por estado, este se colaría junto a su hijo.
        $padre = $this->credit(Credit::STATUS_ACTIVE);
        $hijo = $this->credit(Credit::STATUS_ACTIVE, $padre);

        $ids = $this->scoped()->activeLeafCredits()->pluck('id')->all();

        $this->assertSame([$hijo->id], $ids);
    }

    #[Test]
    public function reemplazados_trae_solo_los_padres(): void
    {
        $extendido = $this->credit(Credit::STATUS_EXTENDED);
        $hijo = $this->credit(Credit::STATUS_ACTIVE, $extendido);
        $sueltoActivo = $this->credit(Credit::STATUS_ACTIVE);
        $sueltoPagado = $this->credit(Credit::STATUS_PAID);

        $ids = $this->scoped()->replacedCredits()->pluck('id')->all();

        $this->assertSame([$extendido->id], $ids);
        $this->assertNotContains($hijo->id, $ids);
        $this->assertNotContains($sueltoActivo->id, $ids);
        $this->assertNotContains($sueltoPagado->id, $ids);
    }

    #[Test]
    public function cerrados_excluye_los_que_tienen_hijo(): void
    {
        $pagado = $this->credit(Credit::STATUS_PAID);
        $castigo = $this->credit(Credit::STATUS_DEFAULTED);

        // Cancelado CON hijo: en producción existe uno así. Pertenece a
        // "Reemplazados", no a "Cerrados".
        $canceladoConHijo = $this->credit(Credit::STATUS_CANCELED);
        $this->credit(Credit::STATUS_ACTIVE, $canceladoConHijo);

        $ids = $this->scoped()->closedLeafCredits()->pluck('id')->all();

        $this->assertEqualsCanonicalizing([$pagado->id, $castigo->id], $ids);
        $this->assertNotContains($canceladoConHijo->id, $ids);
    }

    #[Test]
    public function las_tres_cubetas_parten_el_universo_sin_solapes_ni_huecos(): void
    {
        // Un ejemplar de cada estado, más una cadena padre→hijo.
        foreach (array_keys(Credit::statusOptions()) as $status) {
            $this->credit($status);
        }
        $padre = $this->credit(Credit::STATUS_EXTENDED);
        $this->credit(Credit::STATUS_ACTIVE, $padre);

        $todos = $this->scoped()->pluck('id')->all();
        $enCurso = $this->scoped()->activeLeafCredits()->pluck('id')->all();
        $reemplazados = $this->scoped()->replacedCredits()->pluck('id')->all();
        $cerrados = $this->scoped()->closedLeafCredits()->pluck('id')->all();

        $union = array_merge($enCurso, $reemplazados, $cerrados);

        $this->assertSame(
            count($union),
            count(array_unique($union)),
            'las cubetas se solapan: un crédito aparece en más de una pestaña'
        );

        $this->assertEqualsCanonicalizing(
            $todos,
            $union,
            'las cubetas dejan huecos: hay créditos que no caen en ninguna pestaña'
        );
    }

    #[Test]
    public function el_filtro_de_estado_ofrece_los_doce_estados(): void
    {
        // El select viejo listaba 5. Elegir "Extendido" era imposible.
        $options = Credit::statusOptions();

        $this->assertCount(12, $options);
        $this->assertSame('Extendido', $options[Credit::STATUS_EXTENDED]);
        $this->assertSame('En Castigo', $options[Credit::STATUS_DEFAULTED]);
        $this->assertSame('Activo', $options[Credit::STATUS_ACTIVE]);
    }

    #[Test]
    public function el_credito_padre_sabe_cual_lo_sustituyo(): void
    {
        $padre = $this->credit(Credit::STATUS_EXTENDED);
        $hijo = $this->credit(Credit::STATUS_ACTIVE, $padre);

        $this->assertSame($hijo->code, $padre->fresh()->replacementCode());
        $this->assertNull($hijo->fresh()->replacementCode());
    }

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

        $enCurso = $this->credit(Credit::STATUS_ACTIVE);
        $padre = $this->credit(Credit::STATUS_EXTENDED);
        $hijo = $this->credit(Credit::STATUS_ACTIVE, $padre);
        $pagado = $this->credit(Credit::STATUS_PAID);

        // Este es el bug reportado: el padre y su hijo salían juntos, así que el
        // mismo préstamo aparecía dos veces.
        $this->actingAs($admin);

        Livewire::test(ListCredits::class)
            ->assertCanSeeTableRecords([$enCurso, $hijo])
            ->assertCanNotSeeTableRecords([$padre, $pagado]);
    }

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

        $enCurso = $this->credit(Credit::STATUS_ACTIVE);
        $padre = $this->credit(Credit::STATUS_EXTENDED);
        $hijo = $this->credit(Credit::STATUS_ACTIVE, $padre);
        $pagado = $this->credit(Credit::STATUS_PAID);

        $this->actingAs($admin);

        Livewire::test(ListCredits::class, ['activeTab' => 'reemplazados'])
            ->assertCanSeeTableRecords([$padre])
            ->assertCanNotSeeTableRecords([$enCurso, $hijo, $pagado])
            // Sin esta línea, "Extendido" no dice cuál crédito lo sustituyó.
            ->assertSee("sustituido por {$hijo->code}");

        Livewire::test(ListCredits::class, ['activeTab' => 'cerrados'])
            ->assertCanSeeTableRecords([$pagado])
            ->assertCanNotSeeTableRecords([$enCurso, $hijo, $padre]);

        Livewire::test(ListCredits::class, ['activeTab' => 'todos'])
            ->assertCanSeeTableRecords([$enCurso, $hijo, $padre, $pagado]);
    }

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

        return $admin;
    }
}
