<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\Index as VisorDeLogs;
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;

/**
 * El visor de peticiones no cuenta los entornos apagados.
 *
 * **El problema:** un entorno apagado es un sitio que ya no está en servicio —una baja, una
 * migración, un alta que no llegó a nada— y su Moodle puede seguir llamando durante meses.
 * Todas esas llamadas se llevan un error, así que salían en el visor como averías por
 * arreglar y en «sitios con errores» por delante de los sitios de verdad: **la lista de
 * trabajo apuntaba a sitios que ya no se atienden**.
 *
 * Lo que se comprueba aquí es que el recorte hace lo que dice y **nada más que eso**: las
 * peticiones de host desconocido siguen saliendo, pedir un sitio concreto manda sobre el
 * recorte, y la pantalla dice cuántas filas está escondiendo.
 */
class EntornosApagadosTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        Permission::firstOrCreate(
            ['name' => 'admin.api-logs.index', 'guard_name' => 'web'],
            ['model' => 'api-logs', 'description' => 'ver']
        );

        $usuario = User::factory()->create();
        $usuario->givePermissionTo('admin.api-logs.index');
        $this->actingAs($usuario);

        Cache::flush();

        return $usuario;
    }

    private function entorno(string $nombre, string $dominio, bool $activo): Environment
    {
        $cliente = Client::create([
            'name' => 'Cliente ' . $nombre,
            'shortname' => \Illuminate\Support\Str::slug($nombre),
            'actived' => true,
        ]);

        return Environment::create([
            'client_id' => $cliente->id,
            'name' => $nombre,
            'domain' => $dominio,
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => $activo,
        ]);
    }

    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://desconocido.example.test',
            'action' => 'licence',
            'http_status' => 200,
            'error_code' => 0,
            'duration_ms' => 46,
        ], $datos));
    }

    /**
     * El visor abierto **viéndolo todo**.
     *
     * La pantalla abre por «errores sin revisar», que es lo que se quiere el 90 % de las
     * veces. Lo que se prueba en este fichero es otra cosa —el recorte de los entornos
     * apagados—, y sus peticiones son correctas, así que aquí se dice explícitamente que
     * se quieren ver todas. Si no, no saldría ninguna y el fichero probaría el filtro de
     * severidad sin querer.
     */
    private function visor(): \Livewire\Features\SupportTesting\Testable
    {
        // **La cadena vacía y no `null`**: `Testable::set($nombre, null)` no aplica el
        // valor —Livewire trata el null como «no hay valor»— y la propiedad se quedaba en
        // `error`. El vacío es además lo que manda el chip «Todo» de la pantalla.
        return Livewire::test(VisorDeLogs::class)->set('severity', '');
    }

    /** Los `request_uuid` de las filas que enseña el listado. */
    private function hosts(\Livewire\Features\SupportTesting\Testable $componente): array
    {
        $hosts = collect($componente->viewData('logs')->items())->pluck('host')->all();
        sort($hosts);

        return $hosts;
    }

    /* ============ El recorte ============ */

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

        $vivo = $this->entorno('En servicio', 'https://vivo.test', activo: true);
        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

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

        $componente = $this->visor();

        $this->assertSame(['https://vivo.test'], $this->hosts($componente));
        $this->assertSame(1, $componente->viewData('total'),
            'los indicadores tienen que contar lo mismo que el listado, o la cabecera miente');
    }

    public function test_las_peticiones_sin_entorno_reconocido_siguen_saliendo(): void
    {
        // **La trampa del `NOT IN`.** Con `environment_id NOT IN (…)` y `environment_id`
        // nulo, SQL evalúa NULL —no cierto—, así que la fila se cae. Se llevaría por
        // delante justo las peticiones que más importan: un Moodle llamando desde un
        // dominio que no está dado de alta, que no es de ningún entorno apagado.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

        $this->peticion(['environment_id' => null, 'host' => 'https://huerfano.test']);
        $this->peticion(['environment_id' => $apagado->id, 'host' => 'https://apagado.test']);

        $componente = $this->visor();

        $this->assertSame(['https://huerfano.test'], $this->hosts($componente));
    }

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

        $vivo = $this->entorno('En servicio', 'https://vivo.test', activo: true);
        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

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

        $componente = $this->visor()->set('incluirApagados', true);

        $this->assertSame(['https://apagado.test', 'https://vivo.test'], $this->hosts($componente));

        // **Y la cabecera cambia con él.** Los indicadores se cachean cinco minutos, así
        // que si el recorte no entrara en la clave, encender el interruptor dejaría los
        // números de antes con más filas debajo.
        $this->assertSame(2, $componente->viewData('total'));
    }

    public function test_un_entorno_apagado_y_borrado_sigue_sin_contarse(): void
    {
        // Sin `withTrashed()` en la lista de apagados, el ámbito de `SoftDeletes` lo
        // dejaría fuera y sus peticiones volverían a contarse en el caso más claro de
        // todos: un sitio de baja y con la ficha borrada.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);
        $this->peticion(['environment_id' => $apagado->id, 'host' => 'https://apagado.test']);
        $apagado->delete();

        $componente = $this->visor();

        $this->assertSame([], $this->hosts($componente));
    }

    /* ============ Pedir un sitio concreto manda sobre el recorte ============ */

    public function test_elegir_el_entorno_apagado_ensena_sus_peticiones(): void
    {
        // Responder con una pantalla vacía porque el sitio está de baja sería el peor
        // resultado posible: no dice que esté apagado, dice que no ha llamado nunca. Y es
        // el camino por el que se llega desde la ficha del propio entorno.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);
        $this->peticion(['environment_id' => $apagado->id, 'host' => 'https://apagado.test']);

        $componente = $this->visor()->call('elegirEntorno', $apagado->id);

        $this->assertFalse($componente->instance()->recortaApagados());
        $this->assertSame(['https://apagado.test'], $this->hosts($componente));
        $this->assertSame(0, $componente->viewData('apagadosOcultos'),
            'con el sitio pedido a mano no hay nada escondido de lo que avisar');
    }

    public function test_filtrar_por_host_tambien_manda_sobre_el_recorte(): void
    {
        // El otro camino a un sitio concreto: la lista de hosts que han llamado, que sale
        // del propio registro y no del catálogo de entornos.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);
        $this->peticion(['environment_id' => $apagado->id, 'host' => 'https://apagado.test']);

        $componente = $this->visor()->call('elegirHost', 'https://apagado.test');

        $this->assertSame(['https://apagado.test'], $this->hosts($componente));
    }

    /* ============ Lo que esconde, dicho ============ */

    public function test_la_pantalla_dice_cuantas_filas_esconde(): void
    {
        // Un recorte que no se ve es un número en el que no se puede confiar.
        $this->admin();

        $vivo = $this->entorno('En servicio', 'https://vivo.test', activo: true);
        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

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

        foreach (range(1, 3) as $i) {
            $this->peticion(['environment_id' => $apagado->id, 'host' => 'https://apagado.test']);
        }

        $componente = $this->visor();

        $this->assertSame(3, $componente->viewData('apagadosOcultos'));

        $componente->set('incluirApagados', true);

        $this->assertSame(0, $componente->viewData('apagadosOcultos'),
            'con el interruptor puesto ya no se esconde nada: el aviso desaparece');
    }

    public function test_el_numero_se_cuenta_sobre_el_resto_de_los_filtros(): void
    {
        // Si dijera «hay 40.000 peticiones de sitios apagados» mientras se está mirando
        // una acción concreta, el número no se referiría a lo que se tiene delante.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

        $this->peticion(['environment_id' => $apagado->id, 'action' => 'licence']);
        $this->peticion(['environment_id' => $apagado->id, 'action' => 'stats']);

        $componente = $this->visor()->set('action', 'licence');

        $this->assertSame(1, $componente->viewData('apagadosOcultos'));
    }

    /* ============ Los errores que ya no son de nadie ============ */

    public function test_los_sitios_con_errores_no_mandan_a_un_sitio_apagado(): void
    {
        // **Es el motivo de todo esto.** Esa lista es «dónde ir a arreglar», y un sitio de
        // baja llamando con un token que ya no vale llena la lista de trabajo de algo que
        // nadie va a atender.
        $this->admin();

        $apagado = $this->entorno('De baja', 'https://apagado.test', activo: false);

        foreach (range(1, 5) as $i) {
            $this->peticion([
                'environment_id' => $apagado->id,
                'host' => 'https://apagado.test',
                'http_status' => 500,
                'error_code' => 5001,
                'severity' => Severidad::ERROR,
            ]);
        }

        // **Aquí la pantalla se abre tal cual**, sin tocar la severidad: estas peticiones
        // son errores, así que el defecto las enseñaría — y lo que se comprueba es que el
        // recorte de los apagados las quita igualmente.
        $componente = Livewire::test(VisorDeLogs::class);

        $this->assertCount(0, $componente->viewData('sitiosConErrores'));
        $this->assertSame(0, $componente->viewData('errores'));
        $this->assertSame('Ninguna petición en este rango', $componente->viewData('veredicto')['titulo']);
    }

    /* ============ Quitar filtros ============ */

    public function test_quitar_filtros_devuelve_el_recorte(): void
    {
        // El recorte es el defecto de la pantalla, así que «quitar filtros» lo devuelve;
        // y por lo mismo, tenerlo puesto **no** cuenta como filtro puesto: si contara, el
        // botón estaría siempre encendido y dejaría de significar nada.
        $this->admin();

        // Tal cual, sin tocar nada: la cuenta de filtros puestos se mide **contra el
        // defecto**, así que abrir la pantalla y no tocar nada tiene que dar cero.
        $componente = Livewire::test(VisorDeLogs::class);

        $this->assertSame(0, $componente->instance()->appliedFiltersCounts()['total']);

        $componente->set('incluirApagados', true);

        $this->assertSame(1, $componente->instance()->appliedFiltersCounts()['total'],
            'incluirlos sí es una decisión de quien mira, y el contador de filtros la dice');

        $componente->call('resetFilters');

        $this->assertFalse($componente->get('incluirApagados'));
        $this->assertTrue($componente->instance()->recortaApagados());
    }
}
