<?php

namespace Tests\Feature\Dashboard;

use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\ApiRateEvent;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Products\LicenseToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Dashboard como página de control: ver `briefs/dashboard.md` y el diseño
 * "Dashboard Manager" de Claude Design.
 *
 * Lo que se fija aquí: que lo que requiere acción se vea, que el estado "todo en
 * orden" exista de verdad, y que un rol limitado no vea datos que no le tocan —los
 * tres estados que el brief exige diseñar y que casi nunca se prueban.
 */
class DashboardTest extends TestCase
{
    use RefreshDatabase;

    private function permiso(string $nombre, string $modelo = 'dashboard'): void
    {
        Permission::firstOrCreate(
            ['name' => $nombre, 'guard_name' => 'web'],
            ['model' => $modelo, 'description' => $nombre]
        );
    }

    private function actingAsCon(array $permisos): User
    {
        $user = User::factory()->create();

        foreach ($permisos as $nombre) {
            $this->permiso($nombre);
        }

        $user->givePermissionTo($permisos);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

    private function admin(): User
    {
        return $this->actingAsCon([
            'admin.dashboard.index',
            'admin.environments.index',
            'admin.clients.index',
            'admin.license-tokens.index',
            'admin.products.index',
            'admin.api-logs.index',
        ]);
    }

    public function test_sin_incidencias_dice_que_todo_esta_en_orden(): void
    {
        $this->admin();

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('Todo en orden')
            ->assertSee('Requiere tu atención');
    }

    public function test_los_avisos_del_sistema_se_ven_con_su_texto(): void
    {
        // La plantilla anterior imprimía `$log->message`, que no existe —la columna es
        // `msg`— y el bloque salía en blanco. Ver known-issues MGR-028.
        $this->admin();

        MonitoringLog::db('danger', '16003', 'Un aviso que se tiene que leer', 'ApiRateEvent', '1');

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('Un aviso que se tiene que leer');
    }

    public function test_muestra_las_ips_que_han_alcanzado_el_tope(): void
    {
        $this->admin();

        ApiRateEvent::create([
            'ip' => '203.0.113.7',
            'window_started_at' => now()->subMinutes(5),
            'limit_kind' => ApiRateEvent::LIMIT_FAILURES,
            'kind' => ApiRateEvent::KIND_BLOCKED,
            'observed' => 25,
            'limit_value' => 20,
            'host' => 'https://sitio-en-bucle.test',
        ]);

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('203.0.113.7')
            ->assertSee('alcanzó el límite de peticiones');
    }

    public function test_avisa_de_las_licencias_que_caducan_sin_contar_las_vencidas(): void
    {
        $this->admin();

        $cliente = Client::create(['name' => 'Cliente Caduca', 'shortname' => 'caduca', 'actived' => true]);

        LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia que vence',
            'token' => '3IP-VENCE-PRONTO',
            'active' => true,
            'end_at' => now()->addDays(10),
        ]);

        // Ya vencida: NO debe aparecer como "vence en X días". Es el error de MGR-025.
        LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia ya vencida',
            'token' => '3IP-YA-VENCIDA',
            'active' => true,
            'end_at' => now()->subDays(5),
        ]);

        $respuesta = $this->get('/dashboard')->assertOk();

        $respuesta->assertSee('Licencia que vence');
        $respuesta->assertDontSee('Licencia ya vencida');
    }

    public function test_avisa_de_los_entornos_que_han_dejado_de_sincronizar(): void
    {
        $this->admin();

        $cliente = Client::create(['name' => 'Cliente Mudo', 'shortname' => 'mudo', 'actived' => true]);

        Environment::create([
            'name' => 'Entorno callado',
            'domain' => 'https://callado.test',
            'version' => '4.5.3',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'active' => true,
            'refresh_at' => now()->subDays(9),
        ]);

        // Nunca ha sincronizado: eso no es una incidencia nueva, es configuración
        // pendiente, y no debe ensuciar la lista.
        Environment::create([
            'name' => 'Entorno nuevo',
            'domain' => 'https://nuevo.test',
            'version' => '4.5.3',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'active' => true,
            'refresh_at' => null,
        ]);

        $respuesta = $this->get('/dashboard')->assertOk();

        $respuesta->assertSee('callado.test');
        $respuesta->assertSee('sin sincronizar');
        $respuesta->assertDontSee('nuevo.test');

        // Los días, en POSITIVO: `now()->diffInDays($pasado)` va con signo en Carbon 3
        // y la fila llegó a decir "hace -9 día(s)".
        $respuesta->assertSee('sin sincronizar desde hace 9 día(s)');
        $respuesta->assertDontSee('hace -');
    }

    public function test_un_rol_limitado_no_ve_los_bloques_de_administracion(): void
    {
        // Solo dashboard y productos: ni entornos, ni licencias, ni logs.
        $this->actingAsCon(['admin.dashboard.index', 'admin.products.index']);

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('Requiere tu atención')
            ->assertSee('según tu rol')
            ->assertDontSee('Estado del parque')
            ->assertDontSee('Comprobar un dominio');
    }

    public function test_los_kpis_muestran_las_cifras_de_cada_area(): void
    {
        $this->admin();

        $cliente = Client::create(['name' => 'Cliente KPI', 'shortname' => 'kpi', 'actived' => true]);

        Environment::create([
            'name' => 'Entorno KPI',
            'domain' => 'https://kpi.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'active' => true,
        ]);

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('Clientes')
            ->assertSee('Entornos')
            ->assertSee('Licencias')
            ->assertSee('Productos')
            // El entorno creado no tiene token: debe avisar en el KPI.
            ->assertSee('1 sin licencia');
    }

    public function test_los_kpis_separan_las_licencias_vencidas_de_las_que_vencen(): void
    {
        $this->admin();

        $cliente = Client::create(['name' => 'Cliente Lic', 'shortname' => 'lic', 'actived' => true]);

        LicenseToken::create([
            'client_id' => $cliente->id, 'name' => 'Vence pronto',
            'token' => '3IP-KPI-PRONTO', 'active' => true, 'end_at' => now()->addDays(10),
        ]);

        LicenseToken::create([
            'client_id' => $cliente->id, 'name' => 'Ya vencida',
            'token' => '3IP-KPI-VENCIDA', 'active' => true, 'end_at' => now()->subDays(3),
        ]);

        // Una en cada conjunto, sin solaparse: es el error de MGR-025 convertido en
        // test. Con la consulta antigua, la vencida contaría en las dos.
        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('1 vence(n) en ≤30 días')
            ->assertSee('1 vencida(s)');
    }

    public function test_un_rol_limitado_solo_ve_los_kpis_que_le_tocan(): void
    {
        // Solo productos: ni clientes, ni entornos, ni licencias.
        $this->actingAsCon(['admin.dashboard.index', 'admin.products.index']);

        $respuesta = $this->get('/dashboard')->assertOk();

        $respuesta->assertSee('Productos');
        $respuesta->assertDontSee('Licencias');
    }

    public function test_el_buscador_de_dominio_encuentra_el_entorno(): void
    {
        $this->admin();

        $cliente = Client::create(['name' => 'Cliente Busca', 'shortname' => 'busca', 'actived' => true]);

        Environment::create([
            'name' => 'Entorno buscable',
            'domain' => 'https://buscame.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'active' => true,
        ]);

        \Livewire\Livewire::test(\App\Livewire\Dashboard\Index::class)
            ->set('domainQuery', 'buscame')
            ->assertSee('buscame.test')
            ->assertSee('Sin licencia asignada');
    }

    public function test_los_iconos_de_accesos_rapidos_son_los_del_menu_lateral(): void
    {
        // MGR-029: el Dashboard llevaba SVG escritos a mano y no coincidían con los
        // del menú de la izquierda. El nombre del icono se lee del PROPIO menú
        // (`Navigation::$menuItems`), no de una lista copiada aquí: si mañana el
        // menú cambia el icono de Entornos, este test exige que el Dashboard también
        // cambie.
        // Los `.index` hacen falta porque el menú se filtra por permisos: sin ellos
        // `Navigation::mount()` devuelve una lista vacía y no habría con qué comparar.
        $this->actingAsCon([
            'admin.dashboard.index',
            'admin.environments.index',
            'admin.environments.create',
            'admin.clients.index',
            'admin.clients.create',
            'admin.license-tokens.index',
            'admin.license-tokens.create',
        ]);

        $html = $this->get('/dashboard')->assertOk()->getContent();

        foreach (['environments.index', 'clients.index', 'license_tokens.index'] as $ruta) {
            $icono = $this->iconoDelMenu($ruta);

            $this->assertStringContainsString(
                'blade-untitledui-icons/' . $icono . '.svg',
                $html,
                "El Dashboard no usa el icono '{$icono}' que el menú asigna a {$ruta}"
            );
        }
    }

    /**
     * El icono que el menú lateral asigna a una ruta, leído de la fuente real.
     */
    private function iconoDelMenu(string $ruta): string
    {
        $menu = (new \ReflectionClass(\App\Livewire\Layout\Navigation::class))
            ->newInstanceWithoutConstructor();

        $menu->mount();

        foreach ($menu->menuItems as $item) {
            if ($item['route'] === $ruta) {
                return $item['icon'];
            }
        }

        $this->fail("El menú no tiene entrada para la ruta {$ruta}");
    }

    /* ===================== Estado del parque ===================== */

    public function test_el_parque_cuenta_soportes_con_contrato_y_no_el_flag(): void
    {
        // El contador decía «N con soporte activo» leyendo `has_support`, una casilla que
        // nadie recalcula: en la base local había 5 entornos marcados sin una sola fila en
        // `environment_support`. Aquí se fija que cuenta contratos y que el descuadre se
        // dice en lugar de taparse.
        $this->admin();

        $moodle = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        $cliente = Client::create(['name' => 'Cliente Soporte', 'shortname' => 'sop', 'actived' => true]);
        $tipoDeSoporte = \App\Models\Support\Support::create([
            'name' => 'Soporte Premium',
            'shortname' => 'premium',
            'description' => 'Soporte de pruebas',
        ]);

        $conContrato = Environment::create([
            'name' => 'Con contrato',
            'domain' => 'https://con-contrato.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'type_id' => $moodle->id,
            'active' => true,
            'has_support' => true,
        ]);

        // Marcado y sin nada detrás: es el caso que inflaba el contador.
        Environment::create([
            'name' => 'Solo el flag',
            'domain' => 'https://solo-el-flag.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'type_id' => $moodle->id,
            'active' => true,
            'has_support' => true,
        ]);

        \App\Models\Support\EnvironmentSupport::create([
            'environment_id' => $conContrato->id,
            'support_id' => $tipoDeSoporte->id,
            'start_at' => now()->subMonths(6)->format('Y-m-d'),
            'end_at' => now()->addDays(60)->format('Y-m-d'),
        ]);

        Cache::flush();

        $respuesta = $this->get('/dashboard')->assertOk();

        $respuesta->assertSee('1 de 2');
        $respuesta->assertSee('con soporte en vigor');
        $respuesta->assertSee('1 marcado sin contrato');
    }

    public function test_un_soporte_caducado_no_cuenta_como_vigente(): void
    {
        $this->admin();

        $moodle = \App\Models\Environments\Type::create(['name' => 'Moodle LMS', 'shortname' => 'moodle']);
        $cliente = Client::create(['name' => 'Cliente Caducado', 'shortname' => 'cad', 'actived' => true]);
        $tipoDeSoporte = \App\Models\Support\Support::create([
            'name' => 'Soporte Básico',
            'shortname' => 'basico',
            'description' => 'Soporte de pruebas',
        ]);

        $entorno = Environment::create([
            'name' => 'Soporte vencido',
            'domain' => 'https://vencido.test',
            'version' => '4.5.3',
            'env' => 'pro',
            'client_id' => $cliente->id,
            'type_id' => $moodle->id,
            'active' => true,
            'has_support' => true,
        ]);

        \App\Models\Support\EnvironmentSupport::create([
            'environment_id' => $entorno->id,
            'support_id' => $tipoDeSoporte->id,
            'start_at' => now()->subYears(2)->format('Y-m-d'),
            'end_at' => now()->subDays(5)->format('Y-m-d'),
        ]);

        Cache::flush();

        $respuesta = $this->get('/dashboard')->assertOk();

        $respuesta->assertSee('0 de 1');
        // Tiene contrato, aunque vencido: no es un descuadre del flag.
        $respuesta->assertDontSee('marcado sin contrato');
    }

    /* ===================== Actividad ===================== */

    public function test_la_actividad_trae_la_serie_de_errores_en_porcentaje(): void
    {
        // El diseño tiene dos sparklines y solo se pintaba la de peticiones. La de
        // errores va en PORCENTAJE: un día con 10.000 peticiones y 50 fallos está mejor
        // que uno con 100 y 20, y una línea de absolutos diría lo contrario.
        $this->admin();

        $this->peticion(now()->subMinutes(10), 200);
        $this->peticion(now()->subMinutes(9), 200);
        $this->peticion(now()->subMinutes(8), 200);
        $this->peticion(now()->subMinutes(7), 500);

        Cache::flush();

        // `viewData` sobre la respuesta HTTP no vale: la ruta monta un componente de
        // Livewire a pagina completa y los datos de render son los del componente.
        $actividad = \Livewire\Livewire::test(\App\Livewire\Dashboard\Index::class)
            ->viewData('actividad');

        $this->assertTrue($actividad['disponible']);
        $this->assertCount(7, $actividad['serieError']);
        // Hoy es el último día de la serie: 1 de 4 peticiones fallidas.
        $this->assertSame(25.0, (float) end($actividad['serieError']));
        $this->assertSame(4, (int) $actividad['serie'][6]);
    }

    private function peticion(\DateTimeInterface $cuando, int $estado): void
    {
        \App\Models\ApiRequestLog::create([
            'request_uuid' => (string) \Illuminate\Support\Str::uuid(),
            'method' => 'POST',
            'path' => 'api/v1',
            'ip' => '203.0.113.9',
            'auth_type' => 'bearer',
            'http_status' => $estado,
            'action' => 'products',
            'host' => 'https://sitio.test',
            'started_at' => $cuando,
            'ended_at' => $cuando,
            'duration_ms' => 15,
        ]);
    }

    /* ===================== El bloque plegado ===================== */

    public function test_el_bloque_de_atencion_llega_plegado_y_resume_por_nivel(): void
    {
        // Es el bloque más largo de la pantalla: el día que todo va bien empujaba el
        // resto del Dashboard fuera de la vista para decir «Todo en orden». Plegado, la
        // cabecera tiene que responder qué hay y **de qué tipo**: un total suelto no
        // distingue tres avisos informativos de tres entornos caídos.
        $this->admin();

        MonitoringLog::db('danger', '16003', 'Algo critico', 'ApiRateEvent', '1');

        $respuesta = $this->get('/dashboard')->assertOk();

        // Plegado por defecto: el contenedor del detalle nace con `abierto: false`.
        $respuesta->assertSee('x-data="{ abierto: false }"', false);
        $respuesta->assertSee('1 crítico');
        $respuesta->assertSee('Ver detalle');
    }

    public function test_sin_incidencias_el_bloque_plegado_dice_que_todo_esta_en_orden(): void
    {
        $this->admin();

        $this->get('/dashboard')
            ->assertOk()
            ->assertSee('Todo en orden')
            ->assertDontSee('Ver detalle');
    }
}
