<?php

namespace Tests\Feature\Environments;

use App\Livewire\Dashboard\Index as Dashboard;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Data;
use App\Models\Environments\Environment;
use App\Models\Environments\Type;
use App\Support\VersionDeMoodle;
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;

/**
 * Qué versión de Moodle tiene cada entorno, y qué se puede afirmar de ella.
 *
 * **El problema que se arregla.** `environments.version` contiene dos cosas distintas
 * según cuándo se escribió: la release —`5.1.1`— si el entorno sincronizó con el Manager
 * actual, y el versiondb que dejó el Manager antiguo —`2025100603.08`— si no. El panel
 * pintaba las dos igual, con «Moodle » delante, así que en el listado salía
 * `Moodle LMS 2025100603.08` y el dashboard agrupaba por «rama» partiendo por el punto:
 * cinco ramas inventadas que además desplazaban del top 5 a las de verdad.
 *
 * Y aparte, el versiondb **llega en cada petición** —el plugin lo manda en `site.version`
 * junto a `site.release`, incluso en las que fallan— y `SyncAction` no lo leía: se
 * descartaba, así que no se podía enseñar aunque se quisiera.
 *
 * Lo que se fija aquí: que un número no se presente como algo que no es, y que lo que no
 * se sabe se diga.
 */
class VersionDeMoodleTest extends TestCase
{
    use RefreshDatabase;

    /* ===================== Distinguir los dos números ===================== */

    public function test_un_versiondb_no_se_confunde_con_una_release(): void
    {
        // Un versiondb empieza por el año en cuatro cifras; una release por el major, de
        // una o dos. La longitud del primer tramo es lo que los separa.
        foreach (['2025100603.08', '2025100606', '2025100605.02', '2026061200'] as $versiondb) {
            $this->assertTrue(VersionDeMoodle::esVersiondb($versiondb), "no detecto $versiondb");
        }

        foreach (['5.1.1', '4.5.3', '3.12', '5.0.2', '10.0'] as $release) {
            $this->assertFalse(VersionDeMoodle::esVersiondb($release), "confundio $release");
        }
    }

    public function test_la_rama_sale_de_una_release_y_nunca_de_un_versiondb(): void
    {
        // **Es el fallo del dashboard.** `explode('.')` sobre `2025100605.02` daba la
        // «rama» `2025100605.02`, y sobre `2025100606` daba `2025100606`.
        $this->assertSame('5.1', VersionDeMoodle::rama('5.1.1'));
        $this->assertSame('4.5', VersionDeMoodle::rama('4.5.3'));
        $this->assertSame('3.12', VersionDeMoodle::rama('3.12'));

        $this->assertNull(VersionDeMoodle::rama('2025100605.02'));
        $this->assertNull(VersionDeMoodle::rama('2025100606'));
        $this->assertNull(VersionDeMoodle::rama(null));
        $this->assertNull(VersionDeMoodle::rama(''));
    }

    public function test_el_build_se_saca_de_la_cadena_cruda_de_moodle(): void
    {
        $this->assertSame('20251219', VersionDeMoodle::build('5.1.1+ (Build: 20251219)'));
        $this->assertSame('20260903', VersionDeMoodle::build('5.1.6+ (Build: 20260903)'));
        $this->assertNull(VersionDeMoodle::build('5.1.1'));
        $this->assertNull(VersionDeMoodle::build(null));
    }

    /* ===================== Lo que se dice en pantalla ===================== */

    private function entorno(?string $version, ?string $versiondb = null, ?string $cruda = null): Environment
    {
        $cliente = Client::firstOrCreate(['shortname' => 'v'], ['name' => 'V', 'actived' => true]);

        $entorno = Environment::create([
            'name' => 'Sitio',
            'domain' => 'https://s' . (Environment::count() + 1) . '.test',
            'version' => $version,
            'versiondb' => $versiondb,
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ]);

        if ($cruda !== null) {
            Data::create(['environment_id' => $entorno->id, 'moodlerelease' => $cruda]);
            $entorno->load('data');
        }

        return $entorno;
    }

    public function test_en_la_linea_va_la_release_y_el_versiondb_solo_en_el_detalle(): void
    {
        // **Los dos datos siguen estando; lo que cambia es dónde.** En la línea competían:
        // `5.1.6 · versiondb 2025100606.02` se lee como un identificador largo y el ojo no
        // sabe cuál de los dos es la versión. El `versiondb` lo mira Sistemas una vez al
        // mes y la release la mira todo el mundo siempre, así que uno se escanea y el otro
        // se consulta — y eso son dos sitios distintos, no dos trozos de la misma línea.
        $entorno = $this->entorno('5.1.6', '2025100606.02', '5.1.6+ (Build: 20260903)');

        $etiqueta = VersionDeMoodle::etiqueta($entorno);
        $detalle = VersionDeMoodle::detalle($entorno);

        $this->assertStringContainsString('5.1.6', $etiqueta);
        $this->assertStringNotContainsString('2025100606.02', $etiqueta, 'el versiondb no va en la línea');

        // Y no se pierde: sigue entero en el `title`, con el build.
        $this->assertStringContainsString('2025100606.02', $detalle);
        $this->assertStringContainsString('20260903', $detalle);
    }

    public function test_el_mas_de_la_release_se_conserva(): void
    {
        // El `+` de `5.1.6+` dice que el sitio lleva parches por encima de la etiqueta.
        // Sin el, dos sitios con codigo distinto se leen como si tuvieran el mismo.
        $entorno = $this->entorno('5.1.6', '2025100606.02', '5.1.6+ (Build: 20260903)');

        $this->assertStringContainsString('5.1.6+', VersionDeMoodle::etiqueta($entorno));
    }

    public function test_cuando_version_arrastra_un_versiondb_se_dice_que_la_release_no_se_sabe(): void
    {
        // **El caso de pre.** El entorno lleva 123 dias sin senal y su `version` es lo
        // que dejo el Manager antiguo. Antes se pintaba `Moodle LMS 2025100603.08`, que
        // afirma una version de Moodle que nadie ha comprobado.
        $entorno = $this->entorno('2025100603.08');

        $etiqueta = VersionDeMoodle::etiqueta($entorno);

        // Lo que la línea dice es que NO se sabe, que es lo único honesto. El número que
        // provocó la duda está en el `title`, para quien vaya a investigarlo.
        $this->assertStringContainsString('release desconocida', $etiqueta);
        $this->assertStringNotContainsString('2025100603.08', $etiqueta);
        $this->assertStringContainsString('versiondb 2025100603.08', VersionDeMoodle::detalle($entorno));

        $datos = VersionDeMoodle::de($entorno);
        $this->assertNull($datos['release'], 'un versiondb no es una release');
        $this->assertSame('2025100603.08', $datos['versiondb']);
    }

    public function test_sin_ningun_dato_se_dice_que_no_se_sabe(): void
    {
        // Cadena vacia y no null: `environments.version` es NOT NULL, asi que un entorno
        // sin version se guarda como `''`. Es el estado de un entorno creado a mano al
        // que nadie ha rellenado la version y que todavia no ha llamado a la API.
        $this->assertSame('versión desconocida', VersionDeMoodle::etiqueta($this->entorno('')));
    }

    /* ===================== El dashboard ===================== */

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

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

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

        Cache::flush();

        return $user;
    }

    private function sitio(string $version, Type $tipo, Client $cliente): void
    {
        Environment::create([
            'name' => 'S' . $version,
            'domain' => 'https://s-' . md5($version) . '.test',
            'version' => $version,
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'type_id' => $tipo->id,
        ]);
    }

    public function test_el_dashboard_no_se_inventa_ramas_a_partir_de_versiondb(): void
    {
        // **El test del fallo tal como se vio.** Con tres sitios sin actualizar salian
        // tres filas «Moodle 2025100606», «Moodle 2025100605.02»... y las ramas reales
        // se quedaban fuera del top 5.
        $this->admin();

        $moodle = Type::firstOrCreate(['shortname' => 'moodle'], ['name' => 'Moodle LMS']);
        $cliente = Client::firstOrCreate(['shortname' => 'v'], ['name' => 'V', 'actived' => true]);

        foreach (['5.1.1', '5.1.6', '4.5.3', '2025100606', '2025100605.02', '2025100603.08'] as $version) {
            $this->sitio($version, $moodle, $cliente);
        }

        $parque = Livewire::test(Dashboard::class)->viewData('parque');
        $nombres = array_column($parque['versiones'], 'nombre');

        // Ninguna fila con pinta de versiondb.
        foreach ($nombres as $nombre) {
            $this->assertDoesNotMatchRegularExpression(
                '/Moodle \d{8,}/',
                $nombre,
                "el dashboard sigue inventando una rama: «{$nombre}»"
            );
        }

        // Las ramas de verdad, agrupadas.
        $this->assertContains('Moodle 5.1', $nombres);
        $this->assertContains('Moodle 4.5', $nombres);

        // Y los tres sin release, juntos y dichos como lo que son.
        $this->assertContains('Sin release conocida', $nombres);

        $sinRelease = collect($parque['versiones'])->firstWhere('nombre', 'Sin release conocida');
        $this->assertSame(3, $sinRelease['n']);
        $this->assertFalse($sinRelease['conocida']);
    }

    public function test_los_porcentajes_cuentan_tambien_los_de_release_desconocida(): void
    {
        // Si el total solo sumara las ramas conocidas, un sitio de 5.1 sobre dos
        // entornos se ensenaria como el 100 %.
        $this->admin();

        $moodle = Type::firstOrCreate(['shortname' => 'moodle'], ['name' => 'Moodle LMS']);
        $cliente = Client::firstOrCreate(['shortname' => 'v'], ['name' => 'V', 'actived' => true]);

        $this->sitio('5.1.1', $moodle, $cliente);
        $this->sitio('2025100606', $moodle, $cliente);

        $parque = Livewire::test(Dashboard::class)->viewData('parque');
        $porNombre = collect($parque['versiones'])->keyBy('nombre');

        $this->assertSame(50, $porNombre['Moodle 5.1']['pct']);
        $this->assertSame(50, $porNombre['Sin release conocida']['pct']);
    }

    /* ===================== El sync guarda el versiondb ===================== */

    public function test_el_sync_guarda_el_versiondb_que_manda_el_sitio(): void
    {
        // **El dato que se tiraba.** El contrato dice que el sitio manda `site.version`
        // junto a `site.release`, y `SyncAction` solo leia la release. Los numeros de
        // este payload son reales: copiados del detalle de una peticion de pre.
        $cliente = Client::create(['name' => 'Cliente Sync', 'shortname' => 'sync', 'actived' => true]);

        $token = \App\Models\Products\LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia Sync',
            'token' => '3IP-TEST-VERSIONDB',
            'active' => true,
        ]);

        $entorno = Environment::create([
            'name' => 'Sitio',
            'domain' => 'https://quinientosdieciseis.es',
            // Arrastra un versiondb en `version`, como los entornos de pre que no han
            // sincronizado con el codigo actual.
            'version' => '2025100606',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
        ]);

        $this->withHeaders([
            'Authorization' => 'Bearer 3IP-TEST-VERSIONDB',
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            'host' => 'https://quinientosdieciseis.es',
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                'version' => '2025100606.02',
                'release' => '5.1.6+ (Build: 20260903)',
                'type' => 'moodle',
                'env' => 'pro',
            ],
            // `data.plugins` es obligatorio en `sync`, y `required` rechaza un array
            // vacio, asi que va un plugin de relleno: lo que se prueba aqui es la
            // version del sitio, no el inventario.
            'data' => ['plugins' => [[
                'name' => 'Tresipunt',
                'component' => 'local_tresipunt',
                'type' => 'local',
                'versiondisk' => '2026080604',
                'versiondb' => '2026080604',
                'versionrequires' => 2024100700,
                'release' => '1.0.0',
            ]]],
        ])->assertOk();

        $entorno->refresh();

        $this->assertSame('2025100606.02', $entorno->versiondb, 'el versiondb se sigue tirando');
        // Y la release sustituye al versiondb que arrastraba `version`, asi que la
        // columna vuelve a significar una sola cosa.
        $this->assertSame('5.1.6', $entorno->version);
    }

    public function test_un_plugin_que_no_manda_el_versiondb_no_borra_el_ultimo_conocido(): void
    {
        // Un sitio con el plugin antiguo no debe hacer olvidar el numero que ya se sabia:
        // «no me lo has dicho» no es «ya no lo tengo».
        $cliente = Client::create(['name' => 'Cliente Viejo', 'shortname' => 'viejo', 'actived' => true]);

        $token = \App\Models\Products\LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia Vieja',
            'token' => '3IP-TEST-SINVERSIONDB',
            'active' => true,
        ]);

        $entorno = Environment::create([
            'name' => 'Sitio viejo',
            'domain' => 'https://viejo.test',
            'version' => '5.1.1',
            'versiondb' => '2025100601.03',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
        ]);

        $this->withHeaders([
            'Authorization' => 'Bearer 3IP-TEST-SINVERSIONDB',
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            'host' => 'https://viejo.test',
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                // Sin `version`: es lo que manda un plugin sin actualizar.
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'pro',
            ],
            // `data.plugins` es obligatorio en `sync`, y `required` rechaza un array
            // vacio, asi que va un plugin de relleno: lo que se prueba aqui es la
            // version del sitio, no el inventario.
            'data' => ['plugins' => [[
                'name' => 'Tresipunt',
                'component' => 'local_tresipunt',
                'type' => 'local',
                'versiondisk' => '2026080604',
                'versiondb' => '2026080604',
                'versionrequires' => 2024100700,
                'release' => '1.0.0',
            ]]],
        ])->assertOk();

        $this->assertSame('2025100601.03', $entorno->refresh()->versiondb);
    }
}
