<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Show;
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 App\Support\SaludDelEntorno;
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;

/**
 * La ficha de un entorno (`/environments/{id}`).
 *
 * **Lo que se vigila aquí es el veredicto**, que es la razón de que la pantalla exista: la
 * frase que dice si un sitio está recibiendo lo que su cliente tiene contratado. Sale de
 * cruzar cuatro cosas —si está encendido, cuándo sincronizó, si tiene licencia y qué dice
 * el registro de la API—, así que es lógica de verdad y no maquetación.
 *
 * Y de todo lo que comprueba, **el test que más importa es
 * `test_sin_contenido_no_es_un_error`**: los códigos `3002`, `4002` y `5002` significan
 * «no hay contenido publicado para esa versión» y son la respuesta correcta a una
 * pregunta legítima. Contarlos como avería es lo que producía el 35 % de error permanente
 * que enseñó a no mirar el panel, y el diseño de esta pantalla los pintaba en rojo. Si
 * alguien vuelve a contar por `http_status >= 400`, ese test cae.
 */
class FichaDelEntornoTest extends TestCase
{
    use RefreshDatabase;

    /** Los dos permisos de lectura que hacen falta para la ficha completa. */
    private const LECTURA = ['admin.environments.index', 'admin.api-logs.index'];

    private function usuario(array $permisos = self::LECTURA): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDePrueba', 'guard_name' => 'web']);

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

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

        Cache::flush();

        return $user;
    }

    private function entorno(array $atributos = []): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'ficha'],
            ['name' => 'Cliente de la ficha', 'actived' => true]
        );

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

    /** Una petición en el log, con la severidad que la clasifica el propio modelo. */
    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,
            'action' => 'licence',
            'http_status' => 200,
            'error_code' => 0,
            'duration_ms' => 110,
            'started_at' => now()->subMinutes(10),
            'ended_at' => now()->subMinutes(10),
        ], $atributos));
    }

    // ==========================================================
    // Acceso
    // ==========================================================

    public function test_sin_el_permiso_de_lectura_no_se_puede_abrir(): void
    {
        // La ruta lleva `permission:admin.environments.index`, pero el `authorize()` del
        // componente es el que aguanta un POST a /livewire/update (MGR-005).
        $this->usuario(['admin.clients.index']);
        $entorno = $this->entorno();

        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertForbidden();
    }

    public function test_con_el_permiso_de_lectura_se_abre_entera(): void
    {
        $this->usuario();
        $entorno = $this->entorno();

        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertOk()
            ->assertSee('Campus de pruebas')
            ->assertSee('Servicio')
            ->assertSee('Identidad técnica');
    }

    public function test_quien_solo_lee_no_ve_los_botones_de_tocar(): void
    {
        // Support tiene los permisos de lectura y no los de escritura: la ficha tiene que
        // aguantarlo sin ofrecer botones que van a dar 403.
        $this->usuario();
        $entorno = $this->entorno();

        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertOk()
            ->assertDontSee('Apagar…')
            ->assertDontSee('Sincronizar ahora');
    }

    // ==========================================================
    // El veredicto
    // ==========================================================

    public function test_un_entorno_que_llama_y_recibe_dice_que_va_bien(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);
        // Con licencia: sin ella el veredicto correcto es otro, y ese es su propio test.
        $entorno->update(['license_token_id' => $this->licencia($entorno)]);
        $this->peticion($entorno);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('normal', $salud['clave']);
        $this->assertSame('ok', $salud['tono']);
        $this->assertStringContainsString('ninguna con error', $salud['detalle']);
    }

    public function test_sin_contenido_no_es_un_error(): void
    {
        // **El test que fija la norma.** Un `5002` es «este producto no tiene features
        // publicadas»: una lista vacía, no una avería. Si se contara como error, la ficha
        // de un entorno perfectamente sano diría «Llama y recibe errores» y alguien
        // saldría a arreglar algo que funciona.
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);
        $entorno->update(['license_token_id' => $this->licencia($entorno)]);

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

        $peticion = ApiRequestLog::where('action', 'features')->firstOrFail();
        $this->assertSame(Severidad::SIN_CONTENIDO, $peticion->severity,
            'un *002 debe clasificarse como sin_contenido, no como error');

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('normal', $salud['clave'],
            'una petición sin contenido publicado no puede dar el veredicto de errores');
        $this->assertStringContainsString('no es un fallo', $salud['detalle'],
            'se cuenta, y se dice que no es un fallo');
    }

    public function test_un_error_de_verdad_si_cambia_el_veredicto(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);

        $this->peticion($entorno, [
            'action' => 'licence',
            'http_status' => 401,
            'error_code' => 2002,
            'error' => 'Environment not found for this host',
            'started_at' => now()->subHours(2),
        ]);

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

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('errores', $salud['clave']);
        $this->assertSame('malo', $salud['tono']);
        // Y se explica en castellano, no con el código a secas.
        $this->assertStringContainsString('el alta de este entorno está sin terminar', $salud['detalle']);
    }

    public function test_un_entorno_recien_creado_dice_que_nunca_ha_sincronizado(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => null]);

        $salud = SaludDelEntorno::de($entorno);

        $this->assertSame('nunca', $salud['clave']);
        $this->assertSame('aviso', $salud['tono']);
        $this->assertStringContainsString('no ha llamado todavía', $salud['detalle']);
    }

    public function test_llamar_y_no_haber_sincronizado_nunca_son_dos_cosas_distintas(): void
    {
        // `refresh_at` lo escribe SOLO la acción `sync`. Un sitio puede estar pidiendo
        // licencia a diario sin haber mandado nunca un sync, y entonces lo que falta es
        // todo lo que llega en ese payload: versión, tamaño e inventario. Decir «nunca ha
        // sincronizado» sin más se lee como «no llama», que es falso.
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => null]);
        $this->peticion($entorno);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('sin_sync', $salud['clave']);
        $this->assertStringContainsString('nunca un', $salud['detalle']);
    }

    public function test_un_entorno_que_dejo_de_llamar_dice_cuanto_hace(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subDays(21)]);

        $salud = SaludDelEntorno::de($entorno);

        $this->assertSame('sin_senal', $salud['clave']);
        $this->assertStringContainsString('21 días', $salud['titulo']);
    }

    public function test_un_entorno_sano_sin_licencia_dice_que_no_recibe_nada(): void
    {
        // «Recibe con normalidad» en un entorno sin licencia sería falso: sincroniza, sí,
        // pero no recibe ningún producto.
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour(), 'license_token_id' => null]);
        $this->peticion($entorno);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('sin_licencia', $salud['clave']);
        $this->assertStringContainsString('no tiene ninguna licencia', $salud['detalle']);
    }

    // ==========================================================
    // Apagado
    // ==========================================================

    public function test_un_entorno_apagado_ensena_el_motivo_y_quien(): void
    {
        $usuario = $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);

        $entorno->apagar('Migrado al campus nuevo', $usuario->id);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('apagado', $salud['clave']);
        $this->assertSame('malo', $salud['tono']);
        $this->assertStringContainsString('Migrado al campus nuevo', $salud['detalle']);
        $this->assertStringContainsString($usuario->name, $salud['detalle']);
        $this->assertStringContainsString('403', $salud['detalle']);

        Livewire::test(Show::class, ['environment' => $entorno->fresh()])
            ->assertOk()
            ->assertSee('Migrado al campus nuevo');
    }

    public function test_un_entorno_apagado_que_sigue_llamando_lo_avisa(): void
    {
        // Apagar el entorno no desinstala el plugin: ese Moodle seguirá pidiendo cada día
        // y llevándose un 403. Es una situación distinta de un apagado tranquilo, y lo
        // que hay que hacer también.
        $usuario = $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subDays(2)]);

        $entorno->apagar('Se desmantela', $usuario->id);
        // Apagado ayer: las peticiones de hoy son POSTERIORES, que es el caso real.
        $entorno->update(['deactivated_at' => now()->subDay()]);

        $this->peticion($entorno->fresh(), [
            'http_status' => 403,
            'error_code' => 403,
            'error' => 'Entorno inactivo',
            'started_at' => now()->subMinutes(5),
        ]);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertStringContainsString('sigue llamando', $salud['detalle']);
        $this->assertStringContainsString('desinstalar el plugin', $salud['detalle']);
    }

    public function test_un_entorno_apagado_sin_motivo_lo_dice_en_lugar_de_callarse(): void
    {
        // Se puede apagar desde la casilla del formulario de edición, que no pide motivo.
        // El hueco en blanco se lee como que no pasa nada.
        $this->usuario();
        $entorno = $this->entorno();

        $entorno->update(['active' => false]);

        $salud = SaludDelEntorno::de($entorno->fresh());

        $this->assertSame('apagado', $salud['clave']);
        // Se sabe QUIÉN —lo pone el modelo—, y lo que falta es el motivo: se dice.
        $this->assertStringContainsString('sin motivo anotado', $salud['detalle']);
        // Y aun así queda la fecha, que la pone el modelo pase el cambio por donde pase.
        $this->assertNotNull($entorno->fresh()->deactivated_at);
    }

    // ==========================================================
    // El bloque de la API
    // ==========================================================

    public function test_el_resumen_separa_los_errores_de_lo_que_no_lo_es(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);

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

        $resumen = Livewire::test(Show::class, ['environment' => $entorno])
            ->viewData('resumenApi');

        $this->assertSame(3, $resumen['total']);
        $this->assertSame(1, $resumen['ok']);
        $this->assertSame(1, $resumen['sinContenido']);
        $this->assertSame(1, $resumen['negocio']);
        $this->assertSame(0, $resumen['error'], 'ninguna de las tres es un error de verdad');
    }

    public function test_el_registro_de_la_api_va_por_accion_y_no_por_orden(): void
    {
        // **Por qué agrupado y no cronológico.** `licence` es el 70 % de las peticiones de
        // un sitio: un listado de las últimas seis enseñaba seis `licence` seguidas y las
        // acciones que fallan —las de contenido— no aparecían nunca. Y cada acción es un
        // servicio distinto: la licencia puede responder bien mientras el contenido no
        // llega, que es exactamente el caso de este test.
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);

        foreach (range(1, 8) as $i) {
            $this->peticion($entorno, ['started_at' => now()->subMinutes($i)]);
        }

        // La que importa, y la más antigua de todas: en un listado cronológico de seis se
        // quedaría fuera.
        $this->peticion($entorno, [
            'action' => 'features',
            'http_status' => 404,
            'error_code' => 5002,
            'error' => 'No compatible version found',
            'started_at' => now()->subHours(3),
        ]);

        $filas = Livewire::test(Show::class, ['environment' => $entorno])
            ->viewData('peticiones');

        $this->assertCount(2, $filas, 'una fila por acción, no una por petición');

        $porAccion = $filas->keyBy('accion');

        $this->assertSame(8, $porAccion['licence']['total']);
        $this->assertSame(Severidad::OK, $porAccion['licence']['severidad']);

        $this->assertSame(1, $porAccion['features']['total']);
        $this->assertSame(Severidad::SIN_CONTENIDO, $porAccion['features']['severidad']);
        $this->assertSame(1, $porAccion['features']['desglose'][Severidad::SIN_CONTENIDO]);

        // Y se ve en pantalla, que es de lo que se trataba.
        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertOk()
            ->assertSee('features')
            ->assertSee('Sin contenido');
    }

    public function test_el_enlace_al_visor_va_filtrado_por_este_entorno(): void
    {
        // Un enlace al visor sin filtro obliga a repetir a mano el filtro que ya se sabe.
        $this->usuario();
        $entorno = $this->entorno();
        $this->peticion($entorno);

        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertOk()
            ->assertSee('environmentId=' . $entorno->id, escape: false);
    }

    public function test_sin_ninguna_peticion_explica_que_comprobar(): void
    {
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => null]);

        Livewire::test(Show::class, ['environment' => $entorno])
            ->assertOk()
            ->assertSee('no ha llamado nunca')
            ->assertSee($entorno->domain);
    }

    // ==========================================================
    // Lo que no llega no se afirma
    // ==========================================================

    public function test_no_se_afirma_lo_que_el_plugin_no_manda(): void
    {
        // **El caso real de la base de desarrollo.** `policyagreed` y `contactable` están
        // a `0` en la tabla y **no vienen en el envío**: ese 0 es el valor por defecto de
        // la columna. Y `contactname` está a null. La ficha llegó a decir «Admin del
        // sitio: sin nombre», «privacidad sin aceptar» y «no contactable» de un cliente
        // del que no sabemos ninguna de las tres cosas.
        //
        // El envío diario del plugin trae 15 claves y todas son de volumen, así que este
        // es el estado normal, no un caso raro.
        $this->usuario();
        $entorno = $this->entorno(['refresh_at' => now()->subHour()]);

        \App\Models\Environments\Data::create([
            'environment_id' => $entorno->id,
            'contactname' => null,
            'contactemail' => 'admin@cliente.test',
            'contactphone' => null,
            'countrycode' => null,
            'policyagreed' => 0,
            'contactable' => 0,
            'language' => 'es',
            'users' => 1648,
            'courses' => 124,
            'enrolments' => 4230,
        ]);

        \App\Models\Environments\EnvironmentStat::create([
            'environment_id' => $entorno->id,
            'date' => now()->toDateString(),
            // Las claves que manda de verdad: solo volumen.
            'data' => [
                'users' => 1648, 'courses' => 124, 'enrolments' => 4230,
                'activeusers' => 612, 'posts' => 0, 'questions' => 0,
            ],
        ]);

        Livewire::test(Show::class, ['environment' => $entorno->fresh()])
            ->assertOk()
            ->assertDontSee('privacidad sin aceptar')
            ->assertDontSee('no contactable')
            // **Y el correo tampoco**, aunque esté en la tabla: no viene en el último
            // envío, así que es un arrastre. Enseñarlo como el contacto actual es
            // exactamente el fallo que preguntó el usuario —«¿qué nombre?»— al ver un
            // correo de hacía cuatro meses presentado como dato de hoy.
            ->assertDontSee('admin@cliente.test')
            ->assertSee('no rellena el contacto')
            // Lo que sí trae el envío se enseña.
            ->assertSee('1.648');
    }

    // ==========================================================
    // La puerta: que se llegue a la pantalla
    // ==========================================================

    public function test_la_fila_del_listado_enlaza_a_la_ficha(): void
    {
        // **La regla de MGR-070**: el test de una pantalla nueva empieza por comprobar
        // que se llega a ella. Seis de siete veces en este release el código estaba bien
        // y la puerta no existía.
        $this->usuario();
        $entorno = $this->entorno();

        // `client` se pasa como lo pasa el listado. Sin él, Livewire resuelve un
        // `Client` VACÍO del contenedor —no un null—, y el enlace de «asociar
        // licencia» revienta por falta de id.
        Livewire::test(\App\Livewire\Environments\Components\EnvironmentCard::class, [
            'environment' => $entorno,
            'client' => $entorno->client,
        ])
            ->assertOk()
            ->assertSee(route('environments.show', $entorno), escape: false);
    }

    public function test_la_fila_ofrece_apagar_a_quien_puede_editar(): void
    {
        // La modal de apagar y su vista de 193 líneas se escribieron y **ningún botón la
        // abría**: el método estaba, la pantalla estaba, y era inalcanzable. Esto lo
        // vigila.
        $this->usuario([...self::LECTURA, 'admin.environments.edit']);
        $entorno = $this->entorno();

        Livewire::test(\App\Livewire\Environments\Components\EnvironmentCard::class, [
            'environment' => $entorno,
            'client' => $entorno->client,
        ])
            ->assertOk()
            ->assertSee('Apagar entorno')
            ->call('abrirCambioDeEstado')
            ->assertDispatched('openModal');
    }

    /** Una licencia mínima asociada al cliente del entorno, para los casos que la piden. */
    private function licencia(Environment $entorno): int
    {
        return \App\Models\Products\LicenseToken::create([
            'name' => 'Licencia de prueba',
            'client_id' => $entorno->client_id,
            'token' => Str::random(40),
            'active' => true,
            'start_at' => now()->subMonth(),
            'end_at' => now()->addYear(),
        ])->id;
    }
}
