<?php

namespace Tests\Feature\Monitoring;

use App\Livewire\Monitoring\Index as PortadaDeMonitorizacion;
use App\Models\ApiRequestLog;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\Notice;
use App\Models\Products\LicenseToken;
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;

/**
 * La portada de Monitorización.
 *
 * **La sección entraba por «Tráfico y cortes»**: `/monitoring` era directamente esa
 * pantalla, así que la puerta de cinco herramientas era la más densa de las cinco —una
 * tabla de cortes paginada— y ninguna decía qué son las otras cuatro. Es el mismo problema
 * que tenía Configuración, que entraba por «Tipos de entornos».
 *
 * Lo que se fija aquí no es la maquetación: es que **los números digan la verdad**, que en
 * esta sección tiene trampa. Un corte en observación no corta a nadie, y la mitad de lo que
 * la API responde con `4xx` no es un error.
 */
class PortadaDeMonitorizacionTest extends TestCase
{
    use RefreshDatabase;

    private const VER = 'admin.api-logs.index';

    /** @param  list<string>  $permisos */
    private function usuario(array $permisos = [self::VER]): User
    {
        foreach ($permisos as $permiso) {
            // La tabla `permissions` de este proyecto tiene `model` NOT NULL.
            Permission::firstOrCreate(
                ['name' => $permiso, 'guard_name' => 'web'],
                ['model' => 'monitoring', 'description' => $permiso]
            );
        }

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

        Cache::flush();

        return $usuario;
    }

    private function corte(bool $cortando, string $estado = ApiBlock::STATE_BLOCKED): ApiBlock
    {
        return ApiBlock::create([
            'subject_type' => ApiBlock::SUBJECT_IP,
            'ip' => '203.0.113.' . random_int(1, 250),
            'reason' => ApiBlock::REASON_FAILURES,
            'state' => $estado,
            'enforced' => $cortando,
            'first_blocked_at' => now()->subHour(),
            'last_blocked_at' => now()->subMinutes(5),
            'hits' => 12,
            'limit_value' => 20,
        ]);
    }

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

    /* ===================== La puerta ===================== */

    public function test_la_raiz_de_la_seccion_es_la_portada_y_no_la_tabla_de_cortes(): void
    {
        // El cambio, dicho como ruta: `/monitoring` deja de ser «Tráfico y cortes», que
        // pasa a `/monitoring/traffic`.
        $this->usuario();

        $this->get(route('monitoring.index'))
            ->assertOk()
            ->assertSee('Tráfico y cortes')
            ->assertSee('Peticiones API')
            ->assertSee('Caducidades')
            ->assertSee('Avisos');

        $this->assertSame(url('/monitoring'), route('monitoring.index'));
        $this->assertSame(url('/monitoring/traffic'), route('monitoring.traffic'));
    }

    public function test_sin_el_permiso_de_la_seccion_no_se_entra(): void
    {
        $this->usuario(['admin.dashboard.index']);

        $this->get(route('monitoring.index'))->assertForbidden();
    }

    public function test_las_tarjetas_que_piden_otro_permiso_no_salen_sin_el(): void
    {
        // «Acciones del panel» pide `admin.logs.index` y no lo tiene todo el mundo:
        // ofrecer una tarjeta que lleva a un 403 es peor que no ofrecerla.
        $this->usuario();

        // Se busca el texto de la tarjeta y no su título: «Acciones del panel» es también
        // el nombre de la pestaña, y las pestañas se gatean aparte.
        $this->get(route('monitoring.index'))->assertDontSee('Ver el rastro');

        $this->usuario([self::VER, 'admin.logs.index']);

        $this->get(route('monitoring.index'))->assertSee('Ver el rastro');
    }

    /* ===================== Los números ===================== */

    public function test_un_corte_en_observacion_no_se_cuenta_como_cortando(): void
    {
        // **Es la trampa de esta sección.** En observación se apunta lo que se habría
        // cortado y la petición sigue adelante: contarlo como corte sería decir que hay
        // clientes sin servicio cuando no los hay. En pre son 99 filas y ninguna corta.
        $this->usuario();

        $this->corte(cortando: false);
        $this->corte(cortando: false);

        $numeros = Livewire::test(PortadaDeMonitorizacion::class)->viewData('n');

        $this->assertSame(0, $numeros['cortando']);
        $this->assertSame(2, $numeros['observando']);
    }

    public function test_un_corte_revisado_ya_no_esta_activo_pero_sigue_guardado(): void
    {
        // Los dos números contestan cosas distintas: «¿hay algo cortado ahora?» y «¿cuánto
        // hay guardado en esa tabla?». El segundo es el que crece sin freno.
        $this->usuario();

        $this->corte(cortando: true, estado: ApiBlock::STATE_CLEARED);

        $numeros = Livewire::test(PortadaDeMonitorizacion::class)->viewData('n');

        $this->assertSame(0, $numeros['cortando']);
        $this->assertSame(0, $numeros['observando']);
        $this->assertSame(1, $numeros['cortesGuardados']);
    }

    public function test_los_errores_de_la_api_son_solo_los_de_verdad_y_sin_revisar(): void
    {
        // Dos reglas de la casa en un número: `sin_contenido` y `negocio` son respuestas
        // correctas y no cuentan (`norma-cero-errores.md`), y un error ya revisado sigue
        // siendo un error pero ya no es novedad para nadie.
        $this->usuario();

        $this->peticion(['http_status' => 500, 'error_code' => 5001, 'severity' => Severidad::ERROR]);
        $this->peticion(['http_status' => 404, 'error_code' => 4002, 'severity' => Severidad::SIN_CONTENIDO]);
        $this->peticion(['http_status' => 403, 'error_code' => 403, 'severity' => Severidad::NEGOCIO]);
        $this->peticion([
            'http_status' => 500, 'error_code' => 5001, 'severity' => Severidad::ERROR,
            'reviewed_at' => now(),
        ]);

        $numeros = Livewire::test(PortadaDeMonitorizacion::class)->viewData('n');

        $this->assertSame(1, $numeros['erroresSinRevisar'], 'de las cuatro, solo una es un error por mirar');
        $this->assertSame(4, $numeros['peticiones'], 'pero el volumen las cuenta todas');
    }

    public function test_las_peticiones_viejas_no_entran_en_el_volumen(): void
    {
        // La portada contesta «¿pasa algo ahora?»: son siete días, y el rango largo es una
        // decisión que se toma ya dentro del visor.
        $this->usuario();

        $this->peticion();
        $this->peticion(['started_at' => now()->subDays(30), 'ended_at' => now()->subDays(30)]);

        $numeros = Livewire::test(PortadaDeMonitorizacion::class)->viewData('n');

        $this->assertSame(1, $numeros['peticiones']);
        $this->assertSame(PortadaDeMonitorizacion::DIAS, 7);
    }

    public function test_cuenta_los_avisos_sin_entregar_y_las_licencias_por_caducar(): void
    {
        $this->usuario();

        Notice::create(['subject_type' => 'licence', 'subject_id' => 1, 'kind' => 'expiry', 'threshold' => 30]);
        Notice::create(['subject_type' => 'licence', 'subject_id' => 2, 'kind' => 'expiry', 'threshold' => 15, 'sent_at' => now(), 'sent_to' => 'a@b.test']);

        $cliente = Client::create(['name' => 'C', 'shortname' => 'c', 'actived' => true]);
        LicenseToken::create([
            'client_id' => $cliente->id, 'name' => 'Por caducar', 'token' => '3IP-PORTADA-1',
            'active' => true, 'end_at' => now()->addDays(10),
        ]);
        LicenseToken::create([
            'client_id' => $cliente->id, 'name' => 'Lejana', 'token' => '3IP-PORTADA-2',
            'active' => true, 'end_at' => now()->addMonths(6),
        ]);

        $numeros = Livewire::test(PortadaDeMonitorizacion::class)->viewData('n');

        $this->assertSame(1, $numeros['avisosSinEntregar'], 'el entregado no es un pendiente');
        $this->assertSame(1, $numeros['caducaPronto'], 'la de seis meses no está cerca');
    }

    public function test_el_estado_bueno_se_dice_y_no_se_deja_en_blanco(): void
    {
        // Cero cortes es lo normal y lo deseable, y una tarjeta sin nada se lee como que la
        // pantalla no ha cargado. Es la regla de `briefs/general.md`.
        $this->usuario();

        $this->get(route('monitoring.index'))
            ->assertOk()
            ->assertSee('sin cortes')
            ->assertSee('nada que arreglar')
            ->assertSee('todos entregados');
    }
}
