<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Uso;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Data;
use App\Models\Environments\Environment;
use App\Models\Environments\EnvironmentStat;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Uso del entorno, desde `Data.dc.html`.
 *
 * Lo que se fija: que los tres estados de datos se distingan —y son tres porque **hoy el
 * normal es «sin datos»**—, que un hueco en la serie no se disimule, y que un delta de
 * cero no se pinte como una buena noticia.
 */
class UsoTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);

        foreach (['admin.datas.environment', 'admin.environments.index',
                  'admin.plugins.environment', 'admin.clients.index'] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'datas', 'description' => $nombre]
            ));
        }

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

        Cache::flush();

        return $user;
    }

    private function entorno(array $extra = []): Environment
    {
        $cliente = Client::firstOrCreate(
            ['shortname' => 'uso'],
            ['name' => 'Cliente Uso', 'actived' => true]
        );

        return Environment::create(array_merge([
            'name' => 'Campus Uso',
            'domain' => 'https://campus.uso.test',
            'env' => 'pro',
            'version' => '5.1.1',
            'client_id' => $cliente->id,
            'active' => true,
            'refresh_at' => now()->subHour(),
        ], $extra));
    }

    /** Una foto del día `-$hace` con los valores dados. */
    private function foto(Environment $entorno, int $hace, array $valores): EnvironmentStat
    {
        return EnvironmentStat::create([
            'environment_id' => $entorno->id,
            'date' => now()->subDays($hace)->format('Y-m-d'),
            'data' => $valores,
        ]);
    }

    /* ===================== Los tres estados ===================== */

    public function test_un_entorno_que_nunca_ha_enviado_datos_lo_explica_y_no_parece_una_averia(): void
    {
        // **Es el estado de 20 de los 21 entornos**, así que no es un caso raro: es la
        // pantalla. Y el motivo está en el otro lado —la tarea diaria del plugin—, así
        // que hay que decirlo o alguien buscará el fallo en el Manager.
        $this->admin();
        $entorno = $this->entorno();

        Livewire::test(Uso::class, ['environment' => $entorno])
            ->assertSee('todavía no ha enviado ningún dato de uso')
            ->assertSee('No es un fallo del Manager')
            ->assertSee('nunca')
            // Y no se pintan cifras a cero, que se leerían como «este Moodle está vacío».
            ->assertDontSee('usuarios totales');
    }

    public function test_con_una_sola_foto_se_dice_que_la_serie_empieza_ahora(): void
    {
        // **El estado de partida de todos los entornos el día del despliegue.** Una
        // gráfica con ejes dibujados y sin línea parece una avería, y no lo es: hacen
        // falta dos días para que exista una evolución.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100, 'courses' => 10, 'enrolments' => 200, 'resources' => 5]);
        $this->foto($entorno, 0, ['users' => 100, 'courses' => 10]);

        $componente = Livewire::test(Uso::class, ['environment' => $entorno]);

        $this->assertSame([], $componente->viewData('series'));
        $componente->assertSee('La serie empieza ahora')
            ->assertSee('No es una avería')
            // Las cifras sí se ven: hay foto.
            ->assertSee('usuarios totales');
    }

    public function test_con_varias_fotos_se_dibujan_las_cuatro_series(): void
    {
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 120, 'courses' => 12, 'enrolments' => 240, 'resources' => 8]);

        for ($d = 10; $d >= 0; $d--) {
            $this->foto($entorno, $d, [
                'users' => 100 + (10 - $d),
                'courses' => 12,
                'enrolments' => 200 + (10 - $d) * 4,
                'resources' => 8,
            ]);
        }

        $series = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('series'));

        $this->assertSame(
            ['Usuarios', 'Cursos', 'Matrículas', 'Recursos'],
            $series->pluck('nombre')->all()
        );

        // Cada serie lleva sus opciones de ECharts con un punto por día del rango.
        $usuarios = $series->firstWhere('metrica', 'users');

        $this->assertIsArray($usuarios['opciones']);
        $this->assertCount(31, $usuarios['opciones']['series'][0]['data']);
        $this->assertCount(31, $usuarios['opciones']['xAxis']['data']);
    }

    /* ===================== Los datos que faltan ===================== */

    public function test_los_dias_sin_foto_van_como_nulos_y_no_como_cero(): void
    {
        // **Es la distinción para la que existe esta pantalla.** Un 0 se lee como «ese día
        // no había usuarios»; un hueco dice «ese día el Moodle no nos habló». Con `null`
        // y `connectNulls` en false, la línea se parte y se ve.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 110]);

        // Fotos de hace 10 y 8 días: falta la de hace 9.
        $this->foto($entorno, 10, ['users' => 100]);
        $this->foto($entorno, 8, ['users' => 102]);

        $usuarios = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('series'))
            ->firstWhere('metrica', 'users');

        $datos = $usuarios['opciones']['series'][0]['data'];

        // El rango son 31 puntos y solo dos tienen valor.
        $this->assertCount(31, $datos);
        $this->assertSame(2, count(array_filter($datos, fn ($v) => $v !== null)));

        // El día de en medio es null, no 0.
        $indiceDelHueco = 30 - 9;
        $this->assertNull($datos[$indiceDelHueco]);

        // **Y la línea sí se une por encima del hueco**, a propósito: partirla dejaba
        // la evolución en trozos y la tendencia es lo que se viene a ver. Lo que no se
        // pierde es el dato: el punto sigue siendo `null` —el tooltip dirá «sin dato»—
        // y el recuento de huecos se enseña debajo.
        $this->assertTrue($usuarios['opciones']['series'][0]['connectNulls']);

        // El recuento de huecos se enseña: una línea de dos puntos en un mes no se puede
        // leer igual que una completa.
        $this->assertSame(29, $usuarios['huecos']);
    }

    public function test_una_metrica_que_el_moodle_no_envia_se_marca_como_vacia(): void
    {
        // No todos los Moodle mandan todas las medidas. Una serie sin ningún valor no se
        // dibuja como una línea plana a cero: se dice que no llega.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 110]);

        // Fotos con usuarios pero sin recursos.
        $this->foto($entorno, 2, ['users' => 100]);
        $this->foto($entorno, 1, ['users' => 105]);

        $series = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('series'));

        $this->assertFalse($series->firstWhere('metrica', 'users')['vacia']);
        $this->assertTrue($series->firstWhere('metrica', 'resources')['vacia']);
    }

    /* ===================== La frescura ===================== */

    public function test_un_dato_de_hoy_se_dice_hoy(): void
    {
        // **Este test existe por un bug concreto.** En Carbon 3 `diffInDays` devuelve un
        // float, así que `$dias === 0` era `false` para `0.0` y la frescura de una foto
        // recién llegada caía al último caso de la ternaria: decía «hace 2 días».
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 0, ['users' => 100]);

        $frescura = Livewire::test(Uso::class, ['environment' => $entorno])->viewData('frescura');

        $this->assertSame('hoy', $frescura['texto']);
        $this->assertFalse($frescura['viejo']);
    }

    public function test_un_dato_viejo_se_avisa_con_su_fecha(): void
    {
        // Unas cifras de hace un mes presentadas como actuales son peores que no tener
        // cifras: es lo primero que hay que ver al abrir la pantalla.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 24, ['users' => 100]);

        $componente = Livewire::test(Uso::class, ['environment' => $entorno]);

        $this->assertTrue($componente->viewData('frescura')['viejo']);
        $componente->assertSee('hace 24 días')
            ->assertSee('no son datos de hoy');
    }

    public function test_dos_dias_de_retraso_no_se_marcan_como_viejos(): void
    {
        // **Margen de dos días a propósito.** La foto lleva la fecha del Moodle, no la
        // nuestra: un entorno en otro huso puede enviar la de «ayer» siendo puntual, y
        // marcarlo en ámbar sería un falso positivo diario.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 2, ['users' => 100]);

        $this->assertFalse(
            Livewire::test(Uso::class, ['environment' => $entorno])->viewData('frescura')['viejo']
        );
    }

    public function test_la_frescura_no_sale_de_updated_at_de_la_foto(): void
    {
        // `data.updated_at` cambia cuando `sync` toca las dos columnas de versión, así
        // que diría «hoy» aunque las métricas de uso sean de hace un mes. Es exactamente
        // la mentira que esta pantalla tiene que evitar.
        $this->admin();
        $entorno = $this->entorno();

        // Foto tocada ahora mismo…
        Data::create(['environment_id' => $entorno->id, 'users' => 100, 'moodlerelease' => '5.1.1']);
        // …pero el último dato de uso es de hace 20 días.
        $this->foto($entorno, 20, ['users' => 100]);

        $frescura = Livewire::test(Uso::class, ['environment' => $entorno])->viewData('frescura');

        $this->assertSame('hace 20 días', $frescura['texto']);
        $this->assertTrue($frescura['viejo']);
    }

    /* ===================== Los deltas ===================== */

    public function test_sin_cambios_no_se_pinta_como_una_buena_noticia(): void
    {
        // En el Manager antiguo un delta de 0 salía **en verde**, con dos iconos vacíos
        // al lado. Sin cambio no es una buena noticia: es ninguna noticia.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100, 'courses' => 10]);
        $this->foto($entorno, 31, ['users' => 100]);
        $this->foto($entorno, 0, ['users' => 100]);

        $familias = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('familias'));
        $delta = $familias->firstWhere('nombre', 'Usuarios')['delta'];

        $this->assertStringContainsString('sin cambios', $delta['texto']);
        $this->assertStringNotContainsString('success', $delta['fg']);
    }

    public function test_sin_histórico_con_el_que_comparar_se_dice(): void
    {
        // No se finge un 0: si no hay foto de hace un mes, el delta no existe.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 0, ['users' => 100]);

        $familias = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('familias'));

        $this->assertStringContainsString(
            'sin histórico',
            $familias->firstWhere('nombre', 'Usuarios')['delta']['texto']
        );
    }

    public function test_el_delta_compara_con_la_foto_mas_cercana_hacia_atras(): void
    {
        // Si el Moodle no envió justo el día del corte, comparar contra ese día exacto
        // daría «sin histórico» teniendo datos de un día antes. Y coger la más cercana en
        // cualquier dirección podría usar una posterior y dar un delta invertido.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 150]);
        // Nada el día 30 exacto; la más cercana hacia atrás es la de hace 33 días.
        $this->foto($entorno, 33, ['users' => 100]);
        $this->foto($entorno, 0, ['users' => 150]);

        $familias = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('familias'));

        $this->assertStringContainsString(
            '+50',
            $familias->firstWhere('nombre', 'Usuarios')['delta']['texto']
        );
    }

    public function test_el_rango_cambia_el_delta_y_va_en_la_url(): void
    {
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 200]);
        $this->foto($entorno, 200, ['users' => 50]);
        $this->foto($entorno, 20, ['users' => 190]);
        $this->foto($entorno, 0, ['users' => 200]);

        $componente = Livewire::test(Uso::class, ['environment' => $entorno]);

        // **A un mes no hay con qué comparar.** La única foto anterior al corte es
        // la de hace 200 días, y usarla anunciaría «+150 en 1 mes» cuando ese
        // crecimiento es de más de medio año. Fuera de la ventana [30, 60] días no
        // hay comparación honesta, y se dice.
        $familias = collect($componente->viewData('familias'));
        $this->assertStringContainsString('sin histórico', $familias->firstWhere('nombre', 'Usuarios')['delta']['texto']);

        // A un año sí: la de hace 200 días cae en la ventana [365, 730]… tampoco.
        // Con una foto de hace 400 días sí habría delta de año.
        $this->foto($entorno, 400, ['users' => 50]);
        $componente->call('cambiarRango', 365);
        $familias = collect($componente->viewData('familias'));
        $this->assertStringContainsString('+150', $familias->firstWhere('nombre', 'Usuarios')['delta']['texto']);

        // (el bloque de abajo se sustituye)
    }

    public function test_un_rango_que_no_existe_no_deja_la_pantalla_en_blanco(): void
    {
        // El rango viene de la URL: un enlace viejo o un dedazo. Sin la guarda,
        // `RANGOS[$rango]` no existiría al pintar la etiqueta.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 0, ['users' => 100]);

        $this->get(route('environments.uso', $entorno) . '?rango=999')
            ->assertOk()
            ->assertSee('1 mes');

        // Y tampoco por el método, que llega por /livewire/update.
        $componente = Livewire::test(Uso::class, ['environment' => $entorno])->call('cambiarRango', 999);
        $this->assertSame(30, $componente->get('rango'));
    }

public function test_al_cambiar_el_rango_se_repintan_las_graficas(): void
    {
        // **Este test existe por un bug que no daba ningún error.** Las gráficas viven
        // dentro de un `wire:ignore` —hace falta, o Livewire borra el canvas en cada
        // interacción—, y por tanto un `render()` nuevo no las toca: al cambiar de «1 mes»
        // a «1 año», los deltas y las cifras se actualizaban y las cuatro líneas seguían
        // dibujando el mes anterior. Lo vio el usuario.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 200, 'courses' => 20, 'enrolments' => 400, 'resources' => 10]);

        for ($d = 40; $d >= 0; $d -= 2) {
            $this->foto($entorno, $d, [
                'users' => 150 + (40 - $d),
                'courses' => 20,
                'enrolments' => 300 + (40 - $d) * 2,
                'resources' => 10,
            ]);
        }

        $componente = Livewire::test(Uso::class, ['environment' => $entorno])
            ->call('cambiarRango', 365);

        // Una por serie, con el nombre de la métrica: es el evento que escucha el
        // componente `<x-grafica>`.
        foreach (['users', 'courses', 'enrolments', 'resources'] as $metrica) {
            $componente->assertDispatched('grafica:' . $metrica);
        }
    }

    public function test_las_graficas_no_llevan_barra_de_recorte(): void
    {
        // Se descartó: los cuatro botones de rango ya hacen ese trabajo, el tooltip da el
        // valor de cualquier día, y el zoom con la rueda se disparaba al hacer scroll por
        // la página. Y al quitarla se dejó de importar `DataZoomComponent`, que son 36 KB
        // menos en el chunk de ECharts: si alguien la vuelve a poner en las opciones sin
        // registrar el componente, la gráfica se rompería en silencio.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 3, ['users' => 90]);
        $this->foto($entorno, 0, ['users' => 100]);

        $series = Livewire::test(Uso::class, ['environment' => $entorno])->viewData('series');

        foreach ($series as $serie) {
            if ($serie['vacia']) {
                continue;
            }

            $this->assertSame([], $serie['opciones']['dataZoom']);
        }
    }

    public function test_el_eje_vertical_arranca_en_cero(): void
    {
        // **Se probó recortarlo y se descartó.** Ajustar el eje al rango de los datos hacía
        // que la curva se notara —1.601 a 1.648 sobre un eje de 0 a 1.650 es casi una
        // recta—, pero distorsiona: ese 3 % dibujado entre 1.575 y 1.675 parece que el
        // cliente ha duplicado usuarios, y de estas gráficas se hacen capturas que van a
        // reuniones. El crecimiento se lee en el delta, que va al lado con su número.
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 1648]);
        $this->foto($entorno, 5, ['users' => 1601]);
        $this->foto($entorno, 0, ['users' => 1648]);

        $usuarios = collect(Livewire::test(Uso::class, ['environment' => $entorno])->viewData('series'))
            ->firstWhere('metrica', 'users');

        $this->assertSame(0, $usuarios['opciones']['yAxis']['min']);

        // Y el techo en un número redondo, no en «1.813»: el eje tiene que poder leerse.
        $techo = $usuarios['opciones']['yAxis']['max'];
        $this->assertGreaterThan(1648, $techo);
        $this->assertSame(0, $techo % 50, 'El techo del eje no es un número redondo: ' . $techo);
    }

    /* ===================== Permisos ===================== */

    public function test_hace_falta_el_permiso_de_datos_del_entorno(): void
    {
        // `admin.datas.environment` estaba en el seeder desde el principio y **no lo usaba
        // ninguna pantalla**: era una de las pistas del roadmap. Ahora es el permiso de
        // esta ruta.
        $rol = Role::firstOrCreate(['name' => 'Mirón', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.environments.index', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'ver']
        ));

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

        $entorno = $this->entorno();

        $this->get(route('environments.uso', $entorno))->assertForbidden();
    }

    /* ===================== El entorno de baja ===================== */

    public function test_un_entorno_de_baja_conserva_su_historico_y_lo_dice(): void
    {
        // Los datos no se borran con la baja: la última foto y la serie hasta ese día
        // siguen siendo la respuesta a «cómo estaba este cliente cuando se fue».
        $this->admin();
        $entorno = $this->entorno();

        Data::create(['environment_id' => $entorno->id, 'users' => 100]);
        $this->foto($entorno, 5, ['users' => 100]);
        $this->foto($entorno, 4, ['users' => 100]);

        $entorno->delete();

        $componente = Livewire::test(Uso::class, ['environment' => $entorno->fresh() ?? Environment::withTrashed()->find($entorno->id)]);

        $componente->assertSee('dado de baja')
            ->assertSee('Los datos históricos se conservan');

        // Y la frescura no se marca en ámbar: el dato no es viejo, es que el entorno se
        // fue. Marcarlo como retraso haría buscar una avería que no existe.
        $this->assertFalse($componente->viewData('frescura')['viejo']);
    }
}
