<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\BorrarLogsDeSitioModal;
use App\Livewire\ApiLogs\Index as VisorDeLogs;
use App\Livewire\ApiLogs\LimpiezaDelHistoricoModal as Limpieza;
use App\Models\ApiRequestLog;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Services\Api\Severidad;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * Los selectores buscables del visor y el borrado de errores por sitio.
 *
 * **Dos huecos del mismo sitio.** Uno: `Cliente` y `Sitio` eran dos `<select>` que se
 * llenaban con una consulta escrita **dentro de la plantilla**, así que las dos tablas
 * enteras se leían y se serializaban en cada `render()` —y `render()` ocurre en cada tecleo
 * de cualquier campo con debounce—. Dos: `host` no tenía ningún campo, solo se llegaba
 * pulsando «mismo host» en una fila que ya estuviera en pantalla, o sea que **un host que
 * no es de ningún entorno no se podía buscar**.
 *
 * Y encima: se podían borrar logs por antigüedad o por rango, pero no «los errores de este
 * sitio», que es lo que se quiere justo después de arreglar una avería.
 */
class SelectoresYBorradoPorSitioTest extends TestCase
{
    use RefreshDatabase;

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

        $usuario = User::factory()->create();
        $usuario->givePermissionTo($permisos);
        $this->actingAs($usuario);

        Cache::flush();

        return $usuario;
    }

    private function cliente(string $nombre): Client
    {
        // `clients.shortname` es NOT NULL, así que se deriva del nombre.
        return Client::create([
            'name' => $nombre,
            'shortname' => \Illuminate\Support\Str::slug($nombre),
            'actived' => true,
        ]);
    }

    private function entorno(Client $cliente, string $nombre, string $dominio): Environment
    {
        return Environment::create([
            'client_id' => $cliente->id,
            'name' => $nombre,
            'domain' => $dominio,
            // `version` y `env` son NOT NULL en el esquema.
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
        ]);
    }

    private function peticion(array $datos = []): ApiRequestLog
    {
        return ApiRequestLog::create(array_merge([
            'request_uuid' => \Illuminate\Support\Str::uuid()->toString(),
            'method' => 'POST',
            'path' => '/api/v1',
            'auth_type' => 'token',
            'started_at' => now()->subHours(2),
            'ended_at' => now()->subHours(2),
            'host' => 'https://uno.example.test',
            'action' => 'licence',
            'plugin' => 'block_tresipuntsepe',
            'http_status' => 200,
            'error_code' => 0,
            'duration_ms' => 46,
        ], $datos));
    }

    /* ============ Los selectores ============ */

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

        $this->cliente('Ayuntamiento de Reus');
        $this->cliente('Universitat de Girona');

        $componente = Livewire::test(VisorDeLogs::class)->set('clientSearch', 'girona');

        $nombres = $componente->instance()->sugerenciasDeClientes()['filas']->pluck('name');

        $this->assertContains('Universitat de Girona', $nombres->all());
        $this->assertNotContains('Ayuntamiento de Reus', $nombres->all());
    }

    public function test_sin_escribir_nada_se_ofrecen_los_primeros(): void
    {
        // Es un selector: abrirlo y no ver nada hasta escribir dos letras sería peor que
        // la lista completa de antes.
        $this->admin();

        $this->cliente('Un cliente');

        $sugerencias = Livewire::test(VisorDeLogs::class)->instance()->sugerenciasDeClientes();

        $this->assertCount(1, $sugerencias['filas']);
    }

    public function test_no_se_ofrecen_mas_de_doce_y_se_dice_cuantas_quedan(): void
    {
        // Una lista recortada en silencio se lee como la lista completa.
        $this->admin();

        for ($i = 1; $i <= 20; $i++) {
            $this->cliente('Cliente ' . str_pad((string) $i, 2, '0', STR_PAD_LEFT));
        }

        $sugerencias = Livewire::test(VisorDeLogs::class)->instance()->sugerenciasDeClientes();

        $this->assertCount(\App\Support\SugerenciasDeClientes::CUANTAS, $sugerencias['filas']);
        $this->assertSame(20, $sugerencias['total'], 'Hay que saber cuántas quedan fuera.');
    }

    public function test_los_entornos_se_buscan_tambien_por_dominio(): void
    {
        // El dominio es lo que se tiene en la mano leyendo un log, y antes el `<select>`
        // lo pintaba pero no se podía buscar por él.
        $this->admin();

        $cliente = $this->cliente('Un cliente');
        $this->entorno($cliente, 'Formación interna', 'https://formacion.example.test');
        $this->entorno($cliente, 'Campus', 'https://campus.otracosa.test');

        $sugerencias = Livewire::test(VisorDeLogs::class)
            ->set('environmentSearch', 'otracosa')
            ->instance()
            ->sugerenciasDeEntornos()['filas'];

        $this->assertCount(1, $sugerencias);
        $this->assertSame('Campus', $sugerencias->first()->name);
    }

    public function test_los_entornos_se_acotan_al_cliente_elegido(): void
    {
        // Ofrecer el entorno de otro cliente es ofrecer una combinación que no puede
        // devolver nada.
        $this->admin();

        $uno = $this->cliente('Cliente uno');
        $otro = $this->cliente('Cliente dos');
        $this->entorno($uno, 'Campus del uno', 'https://uno.example.test');
        $this->entorno($otro, 'Campus del dos', 'https://dos.example.test');

        $sugerencias = Livewire::test(VisorDeLogs::class)
            ->call('elegirCliente', $uno->id)
            ->instance()
            ->sugerenciasDeEntornos()['filas'];

        $this->assertCount(1, $sugerencias);
        $this->assertSame('Campus del uno', $sugerencias->first()->name);
    }

    /* ============ El buscador de host ============ */

    public function test_se_puede_buscar_un_host_que_no_es_de_ningun_entorno(): void
    {
        // **El caso que lo pedía.** Antes solo se llegaba a un host pulsando «mismo host»
        // en una fila que ya estuviera en pantalla.
        $this->admin();

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['host' => 'https://campus.example.test', 'environment_id' => $entorno->id]);
        $this->peticion(['host' => 'https://fantasma.example.test', 'error_code' => 2002, 'http_status' => 404]);
        $this->peticion(['host' => 'https://fantasma.example.test', 'error_code' => 2002, 'http_status' => 404]);

        $sugerencias = Livewire::test(VisorDeLogs::class)
            ->set('hostSearch', 'fantasma')
            ->instance()
            ->sugerenciasDeHosts()['filas'];

        $this->assertCount(1, $sugerencias);
        $this->assertSame('https://fantasma.example.test', $sugerencias->first()->host);
        $this->assertSame(2, (int) $sugerencias->first()->peticiones);
        // Es el dato que se venía a buscar: nadie responde por ese host.
        $this->assertSame(2, (int) $sugerencias->first()->sin_entorno);
    }

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

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['host' => 'https://campus.example.test', 'environment_id' => $entorno->id]);

        $fila = Livewire::test(VisorDeLogs::class)->instance()->sugerenciasDeHosts()['filas']->first();

        $this->assertSame(0, (int) $fila->sin_entorno);
    }

    public function test_los_hosts_se_acotan_al_rango_de_fechas(): void
    {
        // Para que el número que se ofrece sea el que se va a ver al pulsar.
        $this->admin();

        $this->peticion(['host' => 'https://viejo.example.test', 'started_at' => now()->subDays(60)]);
        $this->peticion(['host' => 'https://reciente.example.test', 'started_at' => now()->subHours(3)]);

        $hosts = Livewire::test(VisorDeLogs::class)
            ->set('rangePreset', '7d')
            ->instance()
            ->sugerenciasDeHosts()['filas']
            ->pluck('host');

        $this->assertContains('https://reciente.example.test', $hosts->all());
        $this->assertNotContains('https://viejo.example.test', $hosts->all());
    }

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

        Livewire::test(VisorDeLogs::class)
            ->set('hostSearch', 'fant')
            ->call('elegirHost', 'https://fantasma.example.test')
            ->assertSet('host', 'https://fantasma.example.test')
            // El buscador se cierra: dejar escrito lo que se buscaba confunde con el
            // filtro puesto.
            ->assertSet('hostSearch', '');
    }

    public function test_los_buscadores_no_van_a_la_url(): void
    {
        // Lo que se comparte es qué se ha elegido, no lo que alguien estaba teclando.
        $propiedad = new \ReflectionProperty(VisorDeLogs::class, 'queryString');
        $declarado = $propiedad->getValue(new VisorDeLogs);

        // La lista mezcla las dos formas: `'severity'` y `'rangePreset' => [...]`.
        $enLaUrl = [];

        foreach ($declarado as $clave => $valor) {
            $enLaUrl[] = is_int($clave) ? $valor : $clave;
        }

        foreach (['clientSearch', 'environmentSearch', 'hostSearch'] as $buscador) {
            $this->assertNotContains($buscador, $enLaUrl, "{$buscador} no debe ir en la URL.");
        }
    }

    /* ============ Borrar los errores de un sitio ============ */

    public function test_borra_los_errores_de_un_entorno_y_deja_lo_demas(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $uno = $this->entorno($cliente, 'Campus uno', 'https://uno.example.test');
        $otro = $this->entorno($cliente, 'Campus dos', 'https://dos.example.test');

        // Dos errores del primero, uno correcto del primero, un error del segundo.
        $this->peticion(['environment_id' => $uno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);
        $this->peticion(['environment_id' => $uno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);
        $this->peticion(['environment_id' => $uno->id, 'severity' => Severidad::OK]);
        $this->peticion(['environment_id' => $otro->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', $uno->id)
            ->call('purge');

        $this->assertSame(0, ApiRequestLog::where('environment_id', $uno->id)->where('severity', Severidad::ERROR)->count());
        // Lo que funciona bien de ese sitio no se toca…
        $this->assertSame(1, ApiRequestLog::where('environment_id', $uno->id)->count());
        // …ni los errores del vecino.
        $this->assertSame(1, ApiRequestLog::where('environment_id', $otro->id)->count());
    }

    public function test_borra_los_errores_de_un_host_sin_entorno(): void
    {
        // **La otra mitad del caso**: un host que no resuelve a ningún entorno acumula
        // errores que no son de nadie y no tienen ficha a la que ir.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::ERROR, 'http_status' => 404]);
        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::ERROR, 'http_status' => 404]);
        $this->peticion(['host' => 'https://otro.example.test', 'severity' => Severidad::ERROR, 'http_status' => 404]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', null, 'https://fantasma.example.test')
            ->call('purge');

        $this->assertSame(0, ApiRequestLog::where('host', 'https://fantasma.example.test')->count());
        $this->assertSame(1, ApiRequestLog::where('host', 'https://otro.example.test')->count());
    }

    public function test_borrar_por_host_no_se_lleva_las_filas_con_entorno_del_mismo_host(): void
    {
        // Es el grupo exacto que enseña la lista de «sitios con errores»: sin el
        // `whereNull` se borraría más de lo que decía el número pulsado.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $conEntorno = $this->peticion([
            'host' => 'https://campus.example.test',
            'environment_id' => $entorno->id,
            'severity' => Severidad::ERROR,
            'http_status' => 401,
        ]);
        $this->peticion([
            'host' => 'https://campus.example.test',
            'severity' => Severidad::ERROR,
            'http_status' => 404,
        ]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', null, 'https://campus.example.test')
            ->call('purge');

        $this->assertNotNull($conEntorno->fresh(), 'La fila con entorno reconocido no era de ese grupo.');
        $this->assertSame(1, ApiRequestLog::where('host', 'https://campus.example.test')->count());
    }

    public function test_solo_borra_lo_que_es_un_error_de_verdad(): void
    {
        // Los `*002` son respuestas correctas: borrarlos con esto se llevaría el
        // histórico de lo que funciona bien.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);
        $sinContenido = $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::SIN_CONTENIDO, 'error_code' => 3002]);
        $negocio = $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::NEGOCIO, 'error_code' => 2001]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', $entorno->id)
            ->call('purge');

        $this->assertNotNull($sinContenido->fresh(), 'Sin contenido no es un error.');
        $this->assertNotNull($negocio->fresh(), 'No contratado no es un error.');
        $this->assertSame(2, ApiRequestLog::count());
    }

    public function test_respeta_el_rango_de_fechas_visible(): void
    {
        // Se borra lo que la persona tiene delante: si se llevara todo el histórico, el
        // número de la confirmación no sería el de la pantalla.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $viejo = $this->peticion([
            'environment_id' => $entorno->id,
            'severity' => Severidad::ERROR,
            'http_status' => 401,
            'started_at' => now()->subDays(60),
        ]);
        $this->peticion([
            'environment_id' => $entorno->id,
            'severity' => Severidad::ERROR,
            'http_status' => 401,
            'started_at' => now()->subHours(3),
        ]);

        Livewire::test(VisorDeLogs::class)
            ->set('rangePreset', '7d')
            ->call('openPurgeErroresDeSitioModal', $entorno->id)
            ->call('purge');

        $this->assertNotNull($viejo->fresh(), 'Lo de fuera del rango no se toca.');
        $this->assertSame(1, ApiRequestLog::count());
    }

    public function test_el_borrado_queda_registrado_con_el_criterio(): void
    {
        // Un borrado de logs que no deja rastro es lo que haría alguien tapando huellas.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::ERROR, 'http_status' => 404]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', null, 'https://fantasma.example.test')
            ->call('purge');

        $rastro = \App\Models\Monitoring\Log::where('code', '16002')->latest('id')->first();

        $this->assertNotNull($rastro, 'El borrado no ha dejado rastro.');
        $this->assertStringContainsString('fantasma.example.test', $rastro->msg);
        $this->assertStringContainsString('sin entorno reconocido', $rastro->msg);
    }

    public function test_sin_el_permiso_de_borrado_no_se_puede(): void
    {
        $this->admin(['admin.api-logs.index']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');
        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', $entorno->id)
            ->assertForbidden();

        $this->assertSame(1, ApiRequestLog::count());
    }

    public function test_sin_nada_que_borrar_no_pide_confirmacion(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        Livewire::test(VisorDeLogs::class)
            ->call('openPurgeErroresDeSitioModal', $entorno->id)
            ->assertNotDispatched('openModal');
    }
    /* ============ Borrar el histórico de un entorno, desde la limpieza ============ */

    public function test_el_selector_de_la_limpieza_llega_a_cualquier_entorno(): void
    {
        // **La decisión que importa**: el filtro del visor decide qué se está mirando, y el
        // de la limpieza a quién se le borra el histórico. Acotar la limpieza al cliente
        // filtrado esconderÍa entornos que sí se quieren limpiar.
        //
        // Desde que el formulario está en su propia modal, los dos estados ni se tocan
        // —son dos componentes—; lo que sigue pudiendo romperse es que alguien le pase un
        // cliente al servicio de sugerencias, y eso es lo que fija este test.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $uno = $this->cliente('Cliente uno');
        $otro = $this->cliente('Cliente dos');
        $this->entorno($uno, 'Campus del uno', 'https://uno.example.test');
        $this->entorno($otro, 'Campus del dos', 'https://dos.example.test');

        $sugerencias = Livewire::test(Limpieza::class)->instance()->sugerencias()['entornos'];

        $this->assertCount(2, $sugerencias, 'La limpieza tiene que poder llegar a cualquier entorno.');
    }

    public function test_el_selector_de_la_limpieza_busca_por_nombre_y_dominio(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $this->entorno($cliente, 'Formación interna', 'https://formacion.example.test');
        $this->entorno($cliente, 'Campus', 'https://campus.otracosa.test');

        $componente = Livewire::test(Limpieza::class);

        $porNombre = $componente->set('busqueda', 'Formación')->instance()->sugerencias()['entornos'];
        $this->assertCount(1, $porNombre);

        $porDominio = $componente->set('busqueda', 'otracosa')->instance()->sugerencias()['entornos'];
        $this->assertCount(1, $porDominio);
        $this->assertSame('Campus', $porDominio->first()->name);
    }

    public function test_el_visor_no_guarda_estado_del_formulario_de_limpieza(): void
    {
        // Compartir estado haría que elegir en la limpieza cambiara el listado de detrás,
        // y al revés: que un filtro puesto para mirar preseleccionara un borrado. Con el
        // formulario en su propia modal eso ya no puede pasar, y lo único del borrado que
        // el visor guarda es la orden — que empieza vacía y no toca ningún filtro.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        Livewire::test(VisorDeLogs::class)
            ->assertSet('ordenDeLimpieza', [])
            ->assertSet('environmentSearch', '')
            ->assertSet('environmentId', null);
    }

    public function test_abrir_la_limpieza_de_un_entorno_exige_el_permiso_de_borrado(): void
    {
        $this->admin(['admin.api-logs.index']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        // La modal del sitio es la que borra, así que es la que tiene que negarse:
        // llega por un evento del navegador y el permiso de la ruta no se reaplica en
        // /livewire/update (MGR-005). Que el formulario de limpieza tampoco se abra sin
        // permiso está fijado en PurgeTest.
        Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->assertForbidden();
    }

    public function test_el_aviso_cuenta_todas_las_severidades_y_los_errores_sin_revisar(): void
    {
        // El desglose está en el aviso porque casi todo suele ser tráfico correcto: quien
        // viene buscando «los errores» de un sitio quizá quería la otra acción.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::OK]);
        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::OK]);
        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);
        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::ERROR, 'http_status' => 401, 'reviewed_at' => now()]);
        $this->peticion(['environment_id' => $entorno->id, 'severity' => Severidad::SIN_CONTENIDO, 'error_code' => 3002]);

        $impacto = Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->instance()
            ->impacto();

        $this->assertSame(5, $impacto['total']);
        $this->assertSame(2, $impacto['ok']);
        $this->assertSame(2, $impacto['error']);
        $this->assertSame(1, $impacto['sinContenido']);
        // Uno de los dos errores está revisado: el aviso solo alarma del que nadie ha
        // mirado, porque es el que se pierde de vista al borrarlo.
        $this->assertSame(1, $impacto['erroresSinRevisar']);
    }

    public function test_borra_todo_el_historico_del_entorno_y_solo_el_suyo(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $uno = $this->entorno($cliente, 'Campus uno', 'https://uno.example.test');
        $otro = $this->entorno($cliente, 'Campus dos', 'https://dos.example.test');

        $this->peticion(['environment_id' => $uno->id, 'severity' => Severidad::OK]);
        $this->peticion(['environment_id' => $uno->id, 'severity' => Severidad::ERROR, 'http_status' => 401]);
        $this->peticion(['environment_id' => $otro->id, 'severity' => Severidad::OK]);
        // Y una sin entorno reconocido, que tampoco es suya.
        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::OK]);

        Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $uno->id])
            ->call('borrar');

        $this->assertSame(0, ApiRequestLog::where('environment_id', $uno->id)->count());
        $this->assertSame(1, ApiRequestLog::where('environment_id', $otro->id)->count());
        $this->assertSame(1, ApiRequestLog::whereNull('environment_id')->count());
    }

    public function test_se_lleva_tambien_lo_de_hoy_y_lo_de_hace_un_ano(): void
    {
        // **No respeta el rango de fechas de arriba**, y el aviso lo dice con las dos
        // fechas: es la diferencia con «borrar los errores», que sí lo respeta.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['environment_id' => $entorno->id, 'started_at' => now()->subYear()]);
        $this->peticion(['environment_id' => $entorno->id, 'started_at' => now()->subMinutes(5)]);

        Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->call('borrar');

        $this->assertSame(0, ApiRequestLog::count());
    }

    public function test_un_entorno_sin_peticiones_no_revienta(): void
    {
        // Vacío es una respuesta, no un error, y el aviso la da: un botón que no hace nada
        // deja pensando si ha fallado algo.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $impacto = Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->instance()
            ->impacto();

        $this->assertSame(0, $impacto['total']);
        $this->assertNull($impacto['desde']);
    }

    public function test_el_borrado_del_historico_queda_registrado(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus del cliente', 'https://campus.example.test');

        $this->peticion(['environment_id' => $entorno->id]);

        Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->call('borrar');

        $rastro = \App\Models\Monitoring\Log::where('code', '16002')->latest('id')->first();

        $this->assertNotNull($rastro, 'El borrado no ha dejado rastro.');
        $this->assertStringContainsString('Campus del cliente', $rastro->msg);
        $this->assertStringContainsString('histórico completo', $rastro->msg);
    }

    public function test_sin_permiso_no_se_abre_el_aviso(): void
    {
        // La otra puerta: el permiso de la ruta no se reaplica en /livewire/update.
        $this->admin(['admin.api-logs.index']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        Livewire::test(BorrarLogsDeSitioModal::class, ['environmentId' => $entorno->id])
            ->assertForbidden();
    }
    /* ============ Y el mismo aviso, para un host que no está dado de alta ============ */

    public function test_el_selector_de_la_limpieza_ofrece_los_hosts_sin_entorno(): void
    {
        // **El hueco simétrico.** El selector buscaba en la tabla de entornos, así que un
        // dominio que no está dado de alta no aparecía — y su histórico no se podía limpiar
        // desde aquí, siendo el que nadie mira.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['host' => 'https://fantasma.example.test']);
        $this->peticion(['host' => 'https://fantasma.example.test']);

        $sugerencias = Livewire::test(Limpieza::class)
            ->set('busqueda', 'fantasma')
            ->instance()
            ->sugerencias();

        $this->assertCount(0, $sugerencias['entornos'], 'Ningún entorno se llama así.');
        $this->assertCount(1, $sugerencias['hosts']);
        $this->assertSame('https://fantasma.example.test', $sugerencias['hosts']->first()->host);
        $this->assertSame(2, (int) $sugerencias['hosts']->first()->peticiones);
    }

    public function test_los_hosts_de_la_limpieza_no_se_acotan_al_rango_de_fechas(): void
    {
        // Aquí se borra todo el histórico de un sitio, así que ofrecer solo los del rango
        // visible del visor esconderÍa precisamente los viejos, que son los que se vienen a
        // limpiar. Es lo contrario que en el buscador del filtro, y a propósito.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $this->peticion(['host' => 'https://viejo.example.test', 'started_at' => now()->subDays(400)]);

        $hosts = Livewire::test(Limpieza::class)
            ->instance()
            ->sugerencias()['hosts']
            ->pluck('host');

        $this->assertContains('https://viejo.example.test', $hosts->all());
    }

    public function test_un_host_dado_de_alta_no_sale_como_huerfano(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['host' => 'https://campus.example.test', 'environment_id' => $entorno->id]);

        $sugerencias = Livewire::test(Limpieza::class)->instance()->sugerencias();

        $this->assertCount(0, $sugerencias['hosts']);
        $this->assertCount(1, $sugerencias['entornos']);
    }

    public function test_borra_el_historico_de_un_host_sin_entorno(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::OK]);
        $this->peticion(['host' => 'https://fantasma.example.test', 'severity' => Severidad::ERROR, 'http_status' => 404]);
        $this->peticion(['host' => 'https://otro.example.test', 'severity' => Severidad::OK]);

        Livewire::test(BorrarLogsDeSitioModal::class, ['host' => 'https://fantasma.example.test'])
            ->call('borrar');

        $this->assertSame(0, ApiRequestLog::where('host', 'https://fantasma.example.test')->count());
        $this->assertSame(1, ApiRequestLog::where('host', 'https://otro.example.test')->count());
    }

    public function test_por_host_no_se_lleva_las_del_mismo_dominio_que_si_tienen_entorno(): void
    {
        // **El caso del alta a medias**: el mismo dominio con filas de las dos clases,
        // porque el entorno se dio de alta a media mañana. Borrar «el host» llevándose las
        // que sí tienen entorno sería borrar más de lo que dice el aviso.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $conEntorno = $this->peticion([
            'host' => 'https://campus.example.test',
            'environment_id' => $entorno->id,
        ]);
        $this->peticion(['host' => 'https://campus.example.test']);

        Livewire::test(BorrarLogsDeSitioModal::class, ['host' => 'https://campus.example.test'])
            ->call('borrar');

        $this->assertNotNull($conEntorno->fresh(), 'Esa fila no era de las que no son de nadie.');
        $this->assertSame(1, ApiRequestLog::where('host', 'https://campus.example.test')->count());
    }

    public function test_el_aviso_de_un_host_cuenta_solo_las_que_no_son_de_nadie(): void
    {
        // El número del aviso y el del borrado tienen que ser el mismo: los dos salen de
        // la misma consulta.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $cliente = $this->cliente('Un cliente');
        $entorno = $this->entorno($cliente, 'Campus', 'https://campus.example.test');

        $this->peticion(['host' => 'https://campus.example.test', 'environment_id' => $entorno->id]);
        $this->peticion(['host' => 'https://campus.example.test']);
        $this->peticion(['host' => 'https://campus.example.test']);

        $impacto = Livewire::test(BorrarLogsDeSitioModal::class, ['host' => 'https://campus.example.test'])
            ->instance()
            ->impacto();

        $this->assertSame(2, $impacto['total']);
    }

    public function test_el_borrado_de_un_host_deja_rastro_distinguible(): void
    {
        // Quien audite el borrado tiene que poder distinguir un entorno de un host suelto.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        $this->peticion(['host' => 'https://fantasma.example.test']);

        Livewire::test(BorrarLogsDeSitioModal::class, ['host' => 'https://fantasma.example.test'])
            ->call('borrar');

        $rastro = \App\Models\Monitoring\Log::where('code', '16002')->latest('id')->first();

        $this->assertStringContainsString('fantasma.example.test', $rastro->msg);
        $this->assertStringContainsString('sin entorno reconocido', $rastro->msg);
    }

    public function test_sin_entorno_ni_host_no_se_abre_la_modal(): void
    {
        // Seguir adelante dejaría una consulta sin `where`, o sea la tabla entera.
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        Livewire::test(Limpieza::class)
            ->call('abrirSitio')
            ->assertNotDispatched('openModal');
    }

    public function test_la_modal_sin_ninguna_de_las_dos_cosas_aborta(): void
    {
        $this->admin(['admin.api-logs.index', 'admin.api-logs.destroy']);

        Livewire::test(BorrarLogsDeSitioModal::class, ['host' => '  '])
            ->assertStatus(404);
    }
}