<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\Index;
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 Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * El visor de peticiones, contando por severidad.
 *
 * **Lo que cierra esto es MGR-078.** La pantalla contaba errores por `http_status >= 400`,
 * así que con los datos de desarrollo enseñaba **14 errores donde había 7** —la mitad de
 * las «averías» eran respuestas correctas— y el ranking de «sitios con mayor tasa de
 * error» ordenaba por lo mismo: podía poner primero al sitio más sano del parque, el que
 * solo pide contenido que aún no está publicado, y mandar a Soporte justo donde no hay
 * nada que hacer.
 *
 * Es la pantalla de la que salió `norma-cero-errores.md`, y era la última que quedaba sin
 * aplicarla.
 */
class VisorPorSeveridadTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(array $permisos = ['admin.api-logs.index', 'admin.environments.index']): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDelVisor', 'guard_name' => 'web']);

        foreach ($permisos as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'api-logs', 'description' => $nombre]
            ));
        }

        $user = User::factory()->create();
        $user->assignRole($rol);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

    private function entorno(string $nombre = 'Campus del SEPE'): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'visor'],
            ['name' => 'Cliente del visor', 'actived' => true]
        );

        return Environment::create([
            'name' => $nombre,
            'domain' => 'https://sitio-' . (Environment::withTrashed()->count() + 1) . '.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ]);
    }

    private function peticion(?Environment $entorno, array $atributos = []): ApiRequestLog
    {
        return ApiRequestLog::create(array_merge([
            'request_uuid' => (string) Str::uuid(),
            'method' => 'POST',
            'path' => '/api/v1',
            'auth_type' => 'token',
            'environment_id' => $entorno?->id,
            // El logger lo rellena en cada peticion, y de el salen los enlaces al cliente.
            'client_id' => $entorno?->client_id,
            'host' => $entorno?->domain,
            'action' => 'licence',
            'plugin' => 'block_tresipuntsepe',
            'http_status' => 200,
            'error_code' => 0,
            'duration_ms' => 46,
            'started_at' => now()->subHour(),
            'ended_at' => now()->subHour(),
        ], $atributos));
    }

    /**
     * El parque que enseña el problema: siete respuestas que pasan de 400 y de las que
     * **solo una es un error de verdad**.
     */
    private function parqueMixto(Environment $entorno): void
    {
        // Correctas.
        foreach (range(1, 5) as $i) {
            $this->peticion($entorno, ['started_at' => now()->subMinutes($i)]);
        }

        // `5002`: no hay contenido publicado. Respuesta correcta.
        foreach (range(1, 3) as $i) {
            $this->peticion($entorno, [
                'action' => 'features',
                'http_status' => 404,
                'error_code' => 5002,
                'error' => 'No compatible version found',
                'started_at' => now()->subMinutes(10 + $i),
            ]);
        }

        // `403`: no lo tiene contratado. Respuesta correcta, y el plugin la usa para la promo.
        $this->peticion($entorno, [
            'http_status' => 403,
            'error_code' => 403,
            'error' => 'Entorno inactivo',
            'started_at' => now()->subMinutes(20),
        ]);

        // Y el único error de verdad: la API no reconoce el dominio.
        $this->peticion($entorno, [
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host',
            'started_at' => now()->subMinutes(30),
        ]);
    }

    public function test_los_indicadores_cuentan_errores_por_severidad(): void
    {
        // **El test que cierra MGR-078.** Cinco respuestas pasan de 400 —tres `5002`, un
        // `403` y un `2002`— y solo la última es un error. Contando por `http_status` este
        // sitio salía con cinco.
        $this->usuario();
        $entorno = $this->entorno();
        $this->parqueMixto($entorno);

        $componente = Livewire::test(Index::class)->assertOk();

        $this->assertSame(10, $componente->viewData('total'));
        $this->assertSame(1, $componente->viewData('errores'),
            'solo el 2002 es un error de verdad');

        $porSeveridad = $componente->viewData('porSeveridad');
        $this->assertSame(5, (int) $porSeveridad[Severidad::OK]);
        $this->assertSame(3, (int) $porSeveridad[Severidad::SIN_CONTENIDO]);
        $this->assertSame(1, (int) $porSeveridad[Severidad::NEGOCIO]);

        // Y el reparto por HTTP sigue disponible: deja de ser la cifra de cabecera, no
        // deja de ser cierto.
        // Cinco respuestas pasan de 400: los tres 404 del `5002`, el `403` y el `401`
        // del `2002`. Solo una de las cinco es un error.
        $this->assertSame(5, $componente->viewData('count4xx'));
    }

    public function test_el_veredicto_dice_si_hay_algo_que_arreglar(): void
    {
        $this->usuario();
        $entorno = $this->entorno('Campus con un fallo');
        $this->parqueMixto($entorno);

        $veredicto = Livewire::test(Index::class)->viewData('veredicto');

        $this->assertSame('malo', $veredicto['tono']);
        $this->assertStringContainsString('1 error por arreglar', $veredicto['titulo']);
        $this->assertStringContainsString('Campus con un fallo', $veredicto['titulo']);
    }

    public function test_sin_errores_de_verdad_el_veredicto_esta_tranquilo(): void
    {
        // **El estado normal.** Un sitio que pide contenido que no está publicado no es
        // un sitio roto, y la pantalla no puede decir que lo esté: es lo que enseñaba a
        // no mirarla.
        $this->usuario();
        $entorno = $this->entorno();

        $this->peticion($entorno);
        $this->peticion($entorno, [
            'action' => 'features',
            'http_status' => 404,
            'error_code' => 5002,
            'error' => 'No compatible version found',
        ]);

        $componente = Livewire::test(Index::class)->assertOk();
        $veredicto = $componente->viewData('veredicto');

        $this->assertSame('ok', $veredicto['tono']);
        $this->assertSame('Nada que arreglar', $veredicto['titulo']);
        // Y se nombran, para que nadie las descubra en la tabla y abra una incidencia.
        $this->assertStringContainsString('sin contenido publicado', $veredicto['detalle']);
        $this->assertStringContainsString('respuestas correctas', $veredicto['detalle']);

        $this->assertSame(0, $componente->viewData('errores'));
        $this->assertTrue($componente->viewData('sitiosConErrores')->isEmpty(),
            'sin errores de verdad la lista está vacía, no es un top de lo menos malo');
    }

    public function test_los_sitios_con_errores_se_ordenan_por_cuantos_y_no_por_tasa(): void
    {
        // La tasa premiaba al que llama poco: un sitio con 1 petición y 1 error salía por
        // delante de otro con 7 errores de 100. Lo que hay que mirar primero es dónde hay
        // más cosas rotas.
        $this->usuario();
        $muchos = $this->entorno('Sitio con siete');
        $uno = $this->entorno('Sitio con uno');

        foreach (range(1, 7) as $i) {
            $this->peticion($muchos, [
                'http_status' => 401,
                'error_code' => 2002,
                'error' => 'Environment not found for this host',
                'started_at' => now()->subMinutes($i),
            ]);
        }
        foreach (range(1, 90) as $i) {
            $this->peticion($muchos, ['started_at' => now()->subMinutes(100 + $i)]);
        }

        $this->peticion($uno, [
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host',
        ]);

        $sitios = Livewire::test(Index::class)->viewData('sitiosConErrores');

        $this->assertSame('Sitio con siete', $sitios->first()['entorno']->name);
        $this->assertSame(7, $sitios->first()['errores']);
    }

    public function test_la_pantalla_abre_sin_recortar_por_severidad(): void
    {
        // **Estuvo un día abriéndose en «errores» y el usuario lo pidió al revés.** El
        // argumento que manda: el registro es un registro, y una pantalla que se abre
        // recortando obliga a acordarse del recorte para interpretar cualquier cifra —«no
        // hay peticiones de este sitio» puede querer decir «no hay errores de este sitio»—.
        // Llegar a los errores es un clic; darse cuenta de que faltaba tráfico, no.
        $this->usuario();
        $entorno = $this->entorno();
        $this->parqueMixto($entorno);

        $componente = Livewire::test(Index::class)->assertOk();

        $this->assertSame('', $componente->get('severity'));
        $this->assertCount(10, $componente->viewData('logs'), 'las diez del parque mixto');
        $this->assertSame(10, $componente->viewData('total'));
    }

    public function test_el_chip_de_todo_quita_el_recorte(): void
    {
        // La salida del defecto, a un clic y con su número al lado.
        $this->usuario();
        $entorno = $this->entorno();
        $this->parqueMixto($entorno);

        $componente = Livewire::test(Index::class)->call('filtrarPorSeveridad', null);

        // Vacío y no `null`: con `except: 'error'` en la URL, un null no sobrevive al
        // viaje y el chip se volvía a encender solo. Ver `filtrarPorSeveridad()`.
        $this->assertSame('', $componente->get('severity'));
        $this->assertCount(10, $componente->viewData('logs'));
    }

    public function test_pulsar_el_chip_puesto_lo_quita(): void
    {
        // Es lo que se espera de un chip, y evita dejar a nadie encerrado en «solo errores»
        // sin ver cómo salir.
        $this->usuario();
        $entorno = $this->entorno();
        $this->parqueMixto($entorno);

        $componente = Livewire::test(Index::class)
            ->call('filtrarPorSeveridad', Severidad::ERROR);

        $this->assertSame(Severidad::ERROR, $componente->get('severity'));
        $this->assertCount(1, $componente->viewData('logs'),
            'de las diez del parque mixto, solo una es un error de verdad');

        // Pulsarlo otra vez lo apaga y se vuelve a ver todo.
        $componente->call('filtrarPorSeveridad', Severidad::ERROR);

        $this->assertSame('', $componente->get('severity'));
        $this->assertCount(10, $componente->viewData('logs'));
    }

    public function test_el_filtro_de_severidad_vive_en_la_url(): void
    {
        // Los quince filtros están en la URL para poder pasar una investigación por
        // enlace; el nuevo no puede ser la excepción.
        $this->usuario();

        $reflexion = new \ReflectionProperty(Index::class, 'queryString');
        $reflexion->setAccessible(true);

        $queryString = $reflexion->getValue(new Index);

        // **Con `except`, que se queda aunque el defecto vuelva a ser «todo»**: la constante
        // es la que manda, así que el día que el defecto cambie otra vez —ya ha cambiado
        // dos— la URL sigue llevando solo lo que difiere de él.
        $this->assertArrayHasKey('severity', $queryString);
        $this->assertSame(['except' => Index::SEVERIDAD_POR_DEFECTO], $queryString['severity']);
        $this->assertSame('', Index::SEVERIDAD_POR_DEFECTO, 'la pantalla abre sin recortar');
    }

    public function test_la_tabla_enlaza_al_sitio_y_al_cliente(): void
    {
        // Con los tres permisos: el enlace a cada ficha se pinta solo si quien mira tiene
        // el de esa pantalla, y eso tiene su propio test más abajo.
        $this->usuario(['admin.api-logs.index', 'admin.environments.index', 'admin.clients.index']);
        $entorno = $this->entorno();
        $this->peticion($entorno);

        // Con la severidad quitada: esta petición es correcta y la pantalla abre por
        // errores. Lo que se prueba aquí son los enlaces de la fila.
        Livewire::test(Index::class)
            ->set('severity', '')
            ->assertOk()
            ->assertSee(route('environments.show', $entorno), escape: false)
            ->assertSee(route('clients.show', $entorno->client), escape: false);
    }

    public function test_un_host_sin_entorno_se_avisa_en_la_fila(): void
    {
        // «Alguien está llamando y no sabemos quién» es un alta sin terminar o un dominio
        // cambiado: es información, no un hueco.
        $this->usuario();
        $this->peticion(null, ['host' => 'https://desconocido.example', 'environment_id' => null]);

        Livewire::test(Index::class)
            ->set('severity', '')
            ->assertOk()
            ->assertSee('desconocido.example');
    }
}
