<?php

namespace Tests\Feature\Api;

use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\Products\LicenseToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * Un plugin **ausente del disco** enviado desde `local_tresipunt`.
 *
 * Moodle lo llama «Missing from disk»: el plugin sigue registrado en la base de datos y
 * sus ficheros no están, así que **no hay `versiondisk` que enviar**. La pregunta era si
 * el orquestador puede mandarlos, y la respuesta era no: `type`, `versiondisk` y
 * `versiondb` son NOT NULL en la tabla `plugins`, la acción `plugins` escribía `null` en
 * ellas y la petición entera reventaba con SQLSTATE 23000 — un sitio con un solo plugin
 * ausente no conseguía enviar su inventario.
 *
 * Lo que fijan estos tests es que **el payload incompleto entra** y que la ausencia se
 * guarda de una forma que la pantalla del entorno sabe leer.
 */
class PluginAusenteDelDiscoTest extends TestCase
{
    use RefreshDatabase;

    private const TOKEN = '3IP-TEST-AUSENTE';
    private const DOMAIN = 'https://ausente.test';

    private Environment $entorno;

    protected function setUp(): void
    {
        parent::setUp();

        $cliente = Client::create(['name' => 'Cliente Ausente', 'shortname' => 'aus', 'actived' => true]);

        $token = LicenseToken::create([
            'client_id' => $cliente->id,
            'name' => 'Licencia Ausente',
            'token' => self::TOKEN,
            'active' => true,
        ]);

        $this->entorno = Environment::create([
            'name' => 'Entorno Ausente',
            'domain' => self::DOMAIN,
            'version' => '5.1.1',
            'env' => 'local',
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
            'active' => true,
        ]);
    }

    /**
     * @param  array<int, array>  $plugins
     */
    private function enviar(string $accion, array $plugins): \Illuminate\Testing\TestResponse
    {
        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => $accion,
            'host' => self::DOMAIN,
            'plugin' => 'local_tresipunt',
            'version' => '2026080604',
            'site' => [
                'version' => '2025100601.03',
                'release' => '5.1.1+ (Build: 20251219)',
                'type' => 'moodle',
                'env' => 'local',
            ],
            'data' => ['plugins' => $plugins],
        ]);
    }

    /**
     * El plugin tal y como lo manda `local_tresipunt`, verificado contra su código el
     * 2026-09-10 (Moodle 4.5 y 5.1: el core los incluye en `get_plugins()` y
     * `get_plugins_payload()` no filtra por estado).
     *
     * **Las claves llegan con `null`, no ausentes**, y la diferencia importa: un `??` salva
     * las dos cosas, un `array_key_exists()` solo la segunda. Así que el test manda `null`
     * explícito, que es el caso real y el más estricto.
     *
     * Y **el `name` es el nombre corto**, no uno bonito: Moodle no puede leer el `lang/` de
     * un plugin cuyos ficheros no están, así que no tiene de dónde sacarlo.
     */
    private function ausente(): array
    {
        return [
            'name' => 'moodlecheck',
            'component' => 'local_moodlecheck',
            'type' => 'local',
            'versiondisk' => null,
            'versiondb' => '2024032700',
            // `integer NOT NULL` en la tabla: sin el `?? 0` de la acción, esto sería otro
            // SQLSTATE 23000 como MGR-107.
            'versionrequires' => null,
            // Cadena vacía y no ausente: es lo que manda el plugin.
            'release' => '',
        ];
    }

    public function test_la_accion_plugins_acepta_un_plugin_sin_version_de_disco(): void
    {
        $this->enviar('plugins', [$this->ausente()])->assertSuccessful();

        $plugin = Plugin::where('component', 'local_moodlecheck')->first();

        $this->assertNotNull($plugin, 'El plugin ausente del disco no se ha guardado.');
        $this->assertSame('2024032700', $plugin->versiondb);
        // `'0'` es la forma de decir «esta versión no la sabemos» que ya usaba `SyncAction`
        // y la que `desfase()` lee para dar el estado «ausente del disco».
        $this->assertSame('0', $plugin->versiondisk);
    }

    public function test_el_sync_tambien_lo_acepta(): void
    {
        $this->enviar('sync', [$this->ausente()])->assertSuccessful();

        $plugin = Plugin::where('component', 'local_moodlecheck')->first();

        $this->assertNotNull($plugin);
        $this->assertSame('0', $plugin->versiondisk);
        $this->assertSame('2024032700', $plugin->versiondb);
    }

    public function test_el_release_vacio_llega_como_nulo_y_no_como_cadena(): void
    {
        // **No hace falta normalizarlo aquí y conviene saber por qué.** El plugin manda
        // `release: ""`, pero el middleware `ConvertEmptyStringsToNull` del stack de Laravel
        // lo convierte a `null` antes de que la acción lo vea. Este test lo fija: si algún
        // día ese middleware se quita del grupo `api`, la columna empezaría a guardar
        // cadenas vacías y este test lo diría en vez de descubrirse en una pantalla.
        $this->enviar('plugins', [$this->ausente()])->assertSuccessful();

        $this->assertNull(Plugin::where('component', 'local_moodlecheck')->value('release'));
    }

    public function test_el_nombre_corto_es_el_que_llega_y_se_guarda_tal_cual(): void
    {
        // Moodle no puede dar el nombre bonito de un plugin sin ficheros. El Manager **no
        // se lo inventa**: guarda lo que llega. Para los nuestros hay catálogo con el nombre
        // que hemos puesto nosotros; para un plugin de terceros no hay de dónde sacarlo, y
        // enseñar `moodlecheck` es correcto.
        $this->enviar('plugins', [$this->ausente()])->assertSuccessful();

        $this->assertSame('moodlecheck', Plugin::where('component', 'local_moodlecheck')->value('name'));
    }

    public function test_un_plugin_sin_tipo_no_tumba_el_envio(): void
    {
        // `type` es NOT NULL igual, y un payload al que le falta un campo no puede
        // impedir que se guarden los demás plugins del sitio.
        $sinTipo = $this->ausente();
        unset($sinTipo['type']);

        $this->enviar('plugins', [
            $sinTipo,
            ['name' => 'Otro', 'component' => 'mod_otro', 'type' => 'mod', 'versiondisk' => '2026010100', 'versiondb' => '2026010100'],
        ])->assertSuccessful();

        $this->assertSame('', Plugin::where('component', 'local_moodlecheck')->value('type'));
        $this->assertNotNull(Plugin::where('component', 'mod_otro')->first());
    }
}
