<?php

namespace Tests\Feature\ApiLogs;

use App\Livewire\ApiLogs\LogDetailModal;
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 detalle de una petición de la API.
 *
 * Lo que había era un volcado de nombres de columna. Lo que se prueba aquí es lo que se
 * añadió, que son **las dos preguntas con las que se abre una petición**: qué le ha pasado
 * a este sitio, y si le pasa siempre o ha sido esta vez.
 *
 * Y el que más importa es `test_un_sin_contenido_no_se_pinta_como_averia`: la modal
 * pintaba `http_status >= 400` en ámbar, así que un `5002` —«no hay contenido publicado
 * para esa versión»— se leía como un fallo. Es la norma de cero errores.
 */
class DetalleDePeticionTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(array $permisos = ['admin.api-logs.index', 'admin.environments.index']): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeLogs', '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(): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'logs'],
            ['name' => 'Cliente de los logs', 'actived' => true]
        );

        return Environment::create([
            'name' => 'Campus del SEPE',
            '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' => 'features',
            'plugin' => 'block_tresipuntsepe',
            'http_status' => 200,
            'error_code' => 0,
            'duration_ms' => 96,
            'started_at' => now()->subHour(),
            'ended_at' => now()->subHour(),
        ], $atributos));
    }

    /** Una petición «sin contenido»: `5002`, que es una lista vacía y no una avería. */
    private function sinContenido(?Environment $entorno, array $atributos = []): ApiRequestLog
    {
        return $this->peticion($entorno, array_merge([
            'http_status' => 404,
            'error_code' => 5002,
            'error' => 'No compatible version found',
        ], $atributos));
    }

    /* ============ El host que sí está dado de alta ============ */

    /**
     * Un rechazo por host: el entorno existe, pero la llamada vino con otra licencia.
     *
     * Se guarda `environment_id = null` porque **ninguno atendió la petición** —el
     * middleware busca solo entre los entornos de la licencia que llama—, que es lo que
     * dejaba la ficha diciendo «ninguno reconocido para este host».
     */
    private function rechazadaPorHost(Environment $entorno, string $mensaje): ApiRequestLog
    {
        return $this->peticion(null, [
            'host' => $entorno->domain,
            'client_id' => $entorno->client_id,
            'action' => 'licence',
            'plugin' => 'fresk_premium',
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host. Host searched: '
                . $entorno->domain . '. ' . $mensaje,
        ]);
    }

    public function test_si_el_host_esta_dado_de_alta_la_ficha_lo_dice_en_vez_de_negarlo(): void
    {
        // **El fallo que fija este test.** La ficha decía «ninguno reconocido para este
        // host» y, tres líneas más abajo, el mensaje del servidor decía «the environment
        // exists». Quien lo leía se iba a dar de alta un entorno que ya existe.
        $this->usuario();
        $entorno = $this->entorno();
        $peticion = $this->rechazadaPorHost(
            $entorno,
            'The environment exists for this client but belongs to a different licence.'
        );

        $componente = Livewire::test(LogDetailModal::class, ['logId' => $peticion->id]);

        $detras = $componente->viewData('detrasDelHost');

        $this->assertNotNull($detras, 'el entorno se resuelve por el host');
        $this->assertSame($entorno->id, $detras['entorno']->id);
        $this->assertTrue($detras['mismoCliente']);

        $componente
            ->assertSee('Este host sí está dado de alta')
            ->assertSee($entorno->name)
            // Y dice qué hacer, que es la razón de que el bloque exista.
            ->assertSee('No hay que crear nada');
    }

    public function test_el_diagnostico_ya_no_manda_a_dar_de_alta_lo_que_ya_existe(): void
    {
        // `SaludDelEntorno::explicar()` daba el mismo texto —«el alta de este entorno está
        // sin terminar»— a los tres casos que el middleware sí distingue.
        $this->usuario();
        $entorno = $this->entorno();
        $peticion = $this->rechazadaPorHost(
            $entorno,
            'The environment exists for this client but belongs to a different licence.'
        );

        $explicacion = Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->viewData('explicacion');

        $this->assertStringContainsString('sí está dado de alta', $explicacion);
        $this->assertStringContainsString('corregir la vinculación', $explicacion);
        $this->assertStringNotContainsString('sin terminar', $explicacion);
    }

    public function test_un_host_que_no_existe_sigue_diciendo_que_falta_el_alta(): void
    {
        // La otra mitad: si el dominio no está en ningún sitio, el diagnóstico de siempre
        // es el correcto y no debe cambiar.
        $this->usuario();
        $peticion = $this->peticion(null, [
            'host' => 'https://no-existe-en-ninguna-parte.test',
            'action' => 'licence',
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host. Host searched: https://no-existe-en-ninguna-parte.test',
        ]);

        $componente = Livewire::test(LogDetailModal::class, ['logId' => $peticion->id]);

        $this->assertNull($componente->viewData('detrasDelHost'));
        $this->assertStringContainsString('sin terminar', $componente->viewData('explicacion'));
        $componente->assertDontSee('Este host sí está dado de alta');
    }

    public function test_cada_caso_del_host_tiene_su_propio_motivo(): void
    {
        // Son tres tareas distintas —dar de alta, asignar licencia, corregir una
        // vinculación— y con un solo motivo no se podían ni filtrar ni agrupar.
        $de = fn (string $mensaje) => \App\Services\Api\Motivo::de(401, 2002, $mensaje);

        $this->assertSame(
            \App\Services\Api\Motivo::HOST_DE_OTRA_LICENCIA,
            $de('Environment not found for this host. Host searched: x. The environment exists for this client but belongs to a different licence.')
        );
        $this->assertSame(
            \App\Services\Api\Motivo::HOST_SIN_LICENCIA,
            $de('Environment not found for this host. Host searched: x. The environment exists for this client but has no licence assigned yet.')
        );
        // Y el genérico sigue siendo el genérico, aunque comparta prefijo con los dos.
        $this->assertSame(
            \App\Services\Api\Motivo::HOST_NO_RECONOCIDO,
            $de('Environment not found for this host. Host searched: x')
        );
    }

    public function test_sin_permiso_de_lectura_no_se_abre(): void
    {
        $this->usuario(['admin.clients.index']);
        $peticion = $this->peticion($this->entorno());

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertForbidden();
    }

    public function test_un_sin_contenido_no_se_pinta_como_averia(): void
    {
        // **La norma de cero errores.** Antes: `http_status >= 400 ? ámbar : verde`, así
        // que este `404` con `5002` salía como un fallo. Es la respuesta correcta a una
        // pregunta legítima: ese producto no tiene features publicadas.
        $this->usuario();
        $entorno = $this->entorno();
        $peticion = $this->sinContenido($entorno);

        $this->assertSame(Severidad::SIN_CONTENIDO, $peticion->severity);

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertOk()
            ->assertSee('Sin contenido')
            ->assertSee('No es un fallo')
            ->assertDontSee('Error 5002');
    }

    public function test_un_error_de_verdad_se_explica_en_castellano(): void
    {
        // Un `2002` es un alta sin terminar: la API no reconoce el dominio desde el que
        // llama. Con el código a secas no lo sabe nadie que no se sepa las cuatro
        // familias de memoria.
        $this->usuario();
        $peticion = $this->peticion($this->entorno(), [
            'action' => 'licence',
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host',
        ]);

        $this->assertSame(Severidad::ERROR, $peticion->severity);

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertOk()
            ->assertSee('Qué significa')
            ->assertSee('el alta de este entorno está sin terminar');
    }

    public function test_un_fallo_ya_resuelto_lo_dice(): void
    {
        // **El caso del SEPE**: cuatro fallos y después OK, porque entre medias se publicó
        // la versión. Decir «roto» ahí sería falso y mandaría a alguien a arreglar algo
        // que ya funciona.
        $this->usuario();
        $entorno = $this->entorno();

        $fallo = $this->sinContenido($entorno, ['started_at' => now()->subHours(3)]);
        $this->peticion($entorno, ['started_at' => now()->subHours(2)]);
        $this->peticion($entorno, ['started_at' => now()->subHour()]);

        $contexto = Livewire::test(LogDetailModal::class, ['logId' => $fallo->id])
            ->assertOk()
            ->assertSee('Ya está resuelto')
            ->viewData('contexto');

        $this->assertTrue($contexto['resuelto']);
        $this->assertSame(2, $contexto['posteriores']);
    }

    public function test_una_peticion_correcta_no_dice_que_esta_resuelta(): void
    {
        // «Resuelto» solo tiene sentido si algo falló: en una petición correcta sería
        // ruido, y además sugiere que hubo un problema.
        $this->usuario();
        $entorno = $this->entorno();

        $ok = $this->peticion($entorno, ['started_at' => now()->subHours(3)]);
        $this->peticion($entorno, ['started_at' => now()->subHour()]);

        $contexto = Livewire::test(LogDetailModal::class, ['logId' => $ok->id])
            ->assertOk()
            ->assertDontSee('Ya está resuelto')
            ->viewData('contexto');

        $this->assertFalse($contexto['resuelto']);
    }

    public function test_el_contexto_no_mezcla_productos_que_piden_la_misma_accion(): void
    {
        // `features` la piden TODOS los productos. Agrupando solo por acción, las
        // peticiones de un producto contaminan el recuento del otro y el número deja de
        // decir nada del caso que se está mirando.
        $this->usuario();
        $entorno = $this->entorno();

        $delSepe = $this->sinContenido($entorno, ['started_at' => now()->subHours(2)]);
        $this->peticion($entorno, ['plugin' => 'theme_fresk', 'started_at' => now()->subHour()]);
        $this->peticion($entorno, ['plugin' => 'theme_fresk', 'started_at' => now()->subMinutes(30)]);

        $contexto = Livewire::test(LogDetailModal::class, ['logId' => $delSepe->id])
            ->viewData('contexto');

        $this->assertSame(1, $contexto['total'],
            'solo cuenta las de block_tresipuntsepe, no las de theme_fresk');
        $this->assertFalse($contexto['resuelto'],
            'las de otro producto no resuelven este fallo');
    }

    public function test_la_secuencia_ensena_la_anterior_y_la_siguiente_del_mismo_sitio(): void
    {
        // Lo que explica un fallo suele ser lo que pasó justo antes: un `sync` que va bien
        // y detrás tres `licence` con error dice dónde empezó.
        $this->usuario();
        $entorno = $this->entorno();

        $this->peticion($entorno, ['action' => 'sync', 'plugin' => null, 'started_at' => now()->subHours(4)]);
        $esta = $this->sinContenido($entorno, ['started_at' => now()->subHours(3)]);
        $this->peticion($entorno, ['action' => 'licence', 'started_at' => now()->subHours(2)]);

        $componente = Livewire::test(LogDetailModal::class, ['logId' => $esta->id])->assertOk();

        $this->assertSame('sync', $componente->viewData('anterior')->action);
        $this->assertSame('licence', $componente->viewData('siguiente')->action);
    }

    public function test_una_peticion_sin_sitio_ni_host_no_toma_prestadas_las_de_otros(): void
    {
        // Sin entorno ni host no hay «el mismo sitio». Enseñar las peticiones de
        // cualquiera como si fueran suyas es peor que no enseñar nada.
        $this->usuario();
        $entorno = $this->entorno();
        $this->peticion($entorno, ['started_at' => now()->subHours(2)]);

        $huerfana = $this->peticion(null, ['host' => null, 'started_at' => now()->subHour()]);

        $componente = Livewire::test(LogDetailModal::class, ['logId' => $huerfana->id])->assertOk();

        $this->assertNull($componente->viewData('anterior'));
        $this->assertNull($componente->viewData('siguiente'));
    }

    public function test_enlaza_a_la_ficha_del_entorno(): void
    {
        // El detalle de una petición es donde se descubre que un sitio tiene un problema;
        // el siguiente paso es siempre abrir ese sitio.
        $this->usuario();
        $entorno = $this->entorno();
        $peticion = $this->peticion($entorno);

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertOk()
            ->assertSee(route('environments.show', $entorno), escape: false)
            ->assertSee('Ir a la ficha del sitio');
    }

    public function test_un_host_no_reconocido_se_dice_en_lugar_de_dejar_el_hueco(): void
    {
        // Un host que no resuelve a ningún entorno no es un dato que falte: es «alguien
        // está llamando y no sabemos quién», que es un alta sin terminar o un dominio
        // cambiado.
        $this->usuario();
        $peticion = $this->peticion(null, ['host' => 'https://desconocido.example']);

        Livewire::test(LogDetailModal::class, ['logId' => $peticion->id])
            ->assertOk()
            ->assertSee('no reconocido')
            ->assertSee('desconocido.example');
    }
}
