<?php

namespace Tests\Feature\Supports;

use App\Livewire\Supports\BorrarSoporteModal;
use App\Livewire\Supports\Index as CatalogoDeSoportes;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Support\Support;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Borrar un tipo de soporte del catálogo.
 *
 * **El hueco que cierra**: `/supports` tenía listado, alta y edición y **ninguna forma de
 * quitar un tipo**. Es el mismo que tenían los tipos de entorno (MGR-062) y la octava
 * aparición del patrón en este release: *funcionalidad que parecía existir y no tenía
 * puerta*. El permiso `admin.supports.destroy` existía **sin gatear nada** desde el
 * principio, se borró con los otros huérfanos (MGR-101) y vuelve aquí **con su pantalla**.
 *
 * **La regla: no se borra si algún contrato lo usa.** `Support` tiene borrado lógico, así
 * que borrar un tipo en uso **no da ningún error**: deja los contratos apuntando a un tipo
 * que ya no se puede leer, la pantalla del entorno dice «sin tipo» sin explicar por qué, y
 * los avisos de caducidad siguen contándolos con el hueco en el nombre.
 */
class BorrarTipoDeSoporteTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        // La tabla `permissions` de este proyecto tiene `model` NOT NULL.
        foreach (['admin.supports.index', 'admin.supports.destroy'] as $permiso) {
            Permission::firstOrCreate(
                ['name' => $permiso, 'guard_name' => 'web'],
                ['model' => 'supports', 'description' => $permiso]
            );
        }

        $usuario = User::factory()->create();
        $usuario->givePermissionTo(['admin.supports.index', 'admin.supports.destroy']);
        $this->actingAs($usuario);

        Cache::flush();

        return $usuario;
    }

    private function tipo(string $nombre = 'Soporte Premium'): Support
    {
        return Support::create([
            'name' => $nombre,
            'shortname' => Str::slug($nombre),
            'description' => 'Para la prueba',
        ]);
    }

    /** Un contrato de ese tipo en un entorno nuevo. */
    private function contrato(Support $tipo, string $fin = '+1 year'): Environment
    {
        $cliente = Client::create([
            'name' => 'Cliente ' . Str::random(4),
            'shortname' => Str::lower(Str::random(6)),
            'actived' => true,
        ]);

        $entorno = Environment::create([
            'client_id' => $cliente->id,
            'name' => 'Campus',
            'domain' => 'https://campus-' . Str::random(6) . '.example.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
        ]);

        DB::table('environment_support')->insert([
            'environment_id' => $entorno->id,
            'support_id' => $tipo->id,
            'start_at' => now()->subYear()->format('Y-m-d'),
            'end_at' => date('Y-m-d', strtotime($fin)),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $entorno;
    }

    public function test_un_tipo_que_nadie_usa_se_borra(): void
    {
        $this->admin();
        $tipo = $this->tipo();

        Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id])
            ->call('borrar');

        // **Lógico, no duro**: la fila sigue en la base marcada como borrada.
        $this->assertSoftDeleted('supports', ['id' => $tipo->id]);
        $this->assertSame(0, Support::count());
        $this->assertSame(1, Support::withTrashed()->count());
    }

    public function test_un_tipo_con_contratos_no_se_borra(): void
    {
        // **La regla.** Borrarlo no daría error: dejaría contratos sin tipo legible.
        $this->admin();
        $tipo = $this->tipo();
        $this->contrato($tipo);

        Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id])
            ->call('borrar');

        $this->assertNotSoftDeleted('supports', ['id' => $tipo->id]);
    }

    public function test_un_contrato_caducado_tambien_bloquea(): void
    {
        // Un contrato caducado es **el histórico de lo que se le vendió a un cliente**: si
        // su tipo desaparece, ese histórico se queda sin nombre.
        $this->admin();
        $tipo = $this->tipo();
        $this->contrato($tipo, '-2 years');

        $componente = Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id]);

        $this->assertTrue($componente->instance()->estaEnUso());

        $componente->call('borrar');

        $this->assertNotSoftDeleted('supports', ['id' => $tipo->id]);
    }

    public function test_el_impacto_distingue_los_vigentes(): void
    {
        // Los que aún cubren algo hoy son los que se notarían de inmediato, así que la
        // modal los dice aparte.
        $this->admin();
        $tipo = $this->tipo();

        $this->contrato($tipo, '+1 year');
        $this->contrato($tipo, '-2 years');

        $impacto = Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id])
            ->instance()
            ->impacto();

        $this->assertSame(2, $impacto['contratos']);
        $this->assertSame(2, $impacto['entornos']);
        $this->assertSame(2, $impacto['clientes']);
        $this->assertSame(1, $impacto['vigentes'], 'Solo uno sigue en vigor.');
    }

    public function test_el_bloqueo_no_se_puede_saltar_con_el_snapshot(): void
    {
        // **El fallo que ya se cazó en el borrado de tipos de entorno.** Livewire rehidrata
        // el modelo desde su id en cada petición y los atributos agregados no sobreviven al
        // snapshot: con un `withCount` en el `mount()`, el contador volvía como null —o sea
        // 0— y el bloqueo se saltaba solo al confirmar. Por eso el impacto se consulta en
        // vivo, y esto lo fija.
        $this->admin();
        $tipo = $this->tipo();

        // Se abre la modal ANTES de que exista el contrato…
        $componente = Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id]);
        $this->assertFalse($componente->instance()->estaEnUso());

        // …y aparece uno entre abrir y confirmar.
        $this->contrato($tipo);

        $componente->call('borrar');

        $this->assertNotSoftDeleted('supports', ['id' => $tipo->id]);
    }

    public function test_el_borrado_queda_registrado(): void
    {
        $this->admin();
        $tipo = $this->tipo('Mantenimiento anual');

        Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id])
            ->call('borrar');

        $rastro = MonitoringLog::where('code', '16036')->latest('id')->first();

        $this->assertNotNull($rastro, 'El borrado no ha dejado rastro.');
        $this->assertStringContainsString('Mantenimiento anual', $rastro->msg);
    }

    public function test_el_codigo_del_rastro_esta_documentado(): void
    {
        // Un número sin descripción no se puede filtrar en el visor: nadie recuerda que el
        // 16036 es esto.
        $this->assertArrayHasKey('16036', MonitoringLog::CODIGOS);
    }

    public function test_sin_permiso_no_se_puede(): void
    {
        // Las dos puertas: el permiso de la ruta no se reaplica en /livewire/update.
        Permission::firstOrCreate(
            ['name' => 'admin.supports.destroy', 'guard_name' => 'web'],
            ['model' => 'supports', 'description' => 'x']
        );

        $this->actingAs(User::factory()->create());
        Cache::flush();

        $tipo = $this->tipo();

        Livewire::test(BorrarSoporteModal::class, ['supportId' => $tipo->id])
            ->assertForbidden();

        $this->assertNotSoftDeleted('supports', ['id' => $tipo->id]);
    }

    public function test_el_listado_cuenta_los_contratos_de_cada_tipo(): void
    {
        // Para pintar el candado sin abrir la modal, y **con una consulta para toda la
        // página**, no una por fila.
        $this->admin();

        $conContratos = $this->tipo('Con contratos');
        $this->tipo('Libre');
        $this->contrato($conContratos);

        Livewire::test(CatalogoDeSoportes::class)
            ->assertViewHas('contratos', fn (array $contratos) => ($contratos[$conContratos->id] ?? 0) === 1);
    }

    public function test_el_listado_se_repinta_al_borrar(): void
    {
        // Escuchar el evento es lo que provoca el `render()`. Sin él, el tipo se borra y la
        // fila sigue en la tabla hasta que alguien recargue.
        $this->admin();
        $tipo = $this->tipo();

        Livewire::test(CatalogoDeSoportes::class)
            ->assertSee('Soporte Premium')
            ->call('abrirBorrado', $tipo->id)
            ->assertDispatched('openModal');

        $tipo->delete();

        Livewire::test(CatalogoDeSoportes::class)
            ->dispatch('soportesCambiados')
            ->assertDontSee('Soporte Premium');
    }
}
