<?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 Illuminate\Support\Facades\DB;
use Tests\TestCase;

/**
 * El inventario de plugins que envía cada Moodle en el `sync` (MGR-037).
 *
 * **El límite que manda aquí no es nuestro**: `local_tresipunt` llama con
 * `CURLOPT_TIMEOUT = 4`, así que una respuesta que pase de 4 segundos es un fallo para
 * el Moodle **aunque el Manager termine el trabajo**. Guardar 436 plugins con un
 * `INSERT` por plugin costaba 436 sentencias y 1,4-2,8 segundos medidos en local: con
 * red por medio y una base con carga, un sitio grande se pasaba, el Moodle lo daba por
 * fallido y lo repetía.
 *
 * Se pasó a lotes. Lo que estos tests fijan es que el cambio **no se ha llevado nada
 * por delante**: ni los tipos de los campos, ni la tolerancia a un plugin con datos
 * malos, ni el reemplazo completo del inventario.
 */
class SyncInventoryTest extends TestCase
{
    use RefreshDatabase;

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

    private Environment $entorno;

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

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

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

        $this->entorno = Environment::create([
            'name' => 'Entorno Inventario',
            '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 sincronizar(array $plugins): \Illuminate\Testing\TestResponse
    {
        return $this->withHeaders([
            'Authorization' => 'Bearer ' . self::TOKEN,
            'Accept' => 'application/json',
        ])->postJson('/api/v1', [
            'action' => 'sync',
            '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],
        ]);
    }

    /**
     * @return array<int, array>
     */
    private function inventarioDe(int $cuantos): array
    {
        $plugins = [];

        for ($i = 1; $i <= $cuantos; $i++) {
            $plugins[] = [
                'name' => 'Plugin ' . $i,
                'component' => 'local_plugin' . $i,
                'type' => 'local',
                'versiondisk' => '202601010' . ($i % 10),
                'versiondb' => '202601010' . ($i % 10),
                'versionrequires' => 2024100700,
                'release' => '1.0.' . $i,
            ];
        }

        return $plugins;
    }

    public function test_guarda_el_inventario_completo(): void
    {
        $respuesta = $this->sincronizar($this->inventarioDe(600))->assertOk();

        // 600 pasa del tamaño del lote (500), así que además comprueba que el segundo
        // lote también entra.
        $respuesta->assertJsonPath('data.plugins_count', 600);
        $respuesta->assertJsonPath('data.total', 600);
        $respuesta->assertJsonPath('data.errors', []);

        $this->assertSame(600, Plugin::where('environment_id', $this->entorno->id)->count());
    }

    public function test_lo_hace_en_unas_pocas_sentencias(): void
    {
        // El test que da sentido a todo lo demás: son los INSERT por fila lo que se
        // acercaba al límite de 4 segundos del cliente. Con 436 plugins eran 436
        // sentencias; el umbral se pone holgado para no ser frágil, pero muy por debajo
        // de "una por plugin".
        $sentencias = 0;
        DB::listen(function () use (&$sentencias) {
            $sentencias++;
        });

        $this->sincronizar($this->inventarioDe(436))->assertOk();

        $this->assertLessThan(
            50,
            $sentencias,
            "Se han lanzado {$sentencias} sentencias para 436 plugins: parece que se ha vuelto a insertar fila por fila."
        );
    }

    public function test_conserva_los_campos_json_y_los_booleanos(): void
    {
        // `insert()` no pasa por el modelo, así que el JSON de `dependencies` y
        // `availableupdates` se codifica a mano. Si eso se hace mal, el dato entra como
        // texto y el modelo lo devuelve roto —sin que nada falle a la vista—.
        $dependencias = [
            ['component' => 'mod_forum', 'version' => 2022041900],
        ];
        $actualizaciones = [
            [
                'release' => '4.2.1 (Build: 20231101)',
                'version' => '2023110100',
                'download' => 'https://download.moodle.org/plugins/block_htmlplus_2023110100.zip',
                'maturity' => 200,
            ],
        ];

        $this->sincronizar([
            [
                'name' => 'HTML Plus',
                'component' => 'block_htmlplus',
                'type' => 'block',
                'versiondisk' => '2023100100',
                'versiondb' => '2023100100',
                'dependencies' => $dependencias,
                'availableupdates' => $actualizaciones,
            ],
        ])->assertOk();

        $guardado = Plugin::where('component', 'block_htmlplus')->firstOrFail();

        $this->assertSame($dependencias, $guardado->dependencies, 'Las dependencias no vuelven como array.');
        $this->assertSame($actualizaciones, $guardado->availableupdates, 'Las actualizaciones no vuelven como array.');
        $this->assertTrue($guardado->has_dependencies);
        $this->assertTrue($guardado->has_updates);
    }

    public function test_un_plugin_sin_dependencias_ni_actualizaciones_queda_limpio(): void
    {
        $this->sincronizar([
            ['name' => 'Simple', 'component' => 'local_simple', 'type' => 'local'],
        ])->assertOk();

        $guardado = Plugin::where('component', 'local_simple')->firstOrFail();

        $this->assertNull($guardado->dependencies);
        $this->assertNull($guardado->availableupdates);
        $this->assertFalse($guardado->has_dependencies);
        $this->assertFalse($guardado->has_updates);
    }

    public function test_un_plugin_con_datos_malos_no_tumba_el_resto(): void
    {
        // Es la tolerancia que había gratis cuando cada fila iba en su propia sentencia
        // y que había que conservar al agrupar en lotes. Ver MGR-034 y MGR-037.
        $plugins = $this->inventarioDe(3);
        $plugins[] = ['type' => 'local'];   // sin name ni component: no es un plugin

        $respuesta = $this->sincronizar($plugins)->assertOk();

        $respuesta->assertJsonPath('data.plugins_count', 3);
        $respuesta->assertJsonPath('data.total', 4);

        $this->assertCount(1, $respuesta->json('data.errors'));
        $this->assertSame(3, Plugin::where('environment_id', $this->entorno->id)->count());
    }

    public function test_el_error_del_plugin_malo_no_cuenta_lo_que_ha_fallado_por_dentro(): void
    {
        // MGR-034: `data.errors` llevaba `$e->getMessage()`. Ahora lleva una etiqueta
        // estable más la referencia de la petición, y el detalle va al log.
        $plugins = [['type' => 'local']];

        $error = $this->sincronizar($plugins)->assertOk()->json('data.errors.0');

        $this->assertSame('unknown', $error['plugin']);
        $this->assertStringContainsString('No se pudo registrar el plugin', $error['error']);
        $this->assertStringNotContainsString('name o component', $error['error']);
    }

    public function test_una_segunda_sincronizacion_reemplaza_el_inventario(): void
    {
        // El Moodle manda su inventario completo en cada `sync`: lo que ya no está
        // instalado tiene que desaparecer, no acumularse.
        $this->sincronizar($this->inventarioDe(5))->assertOk();
        $this->assertSame(5, Plugin::where('environment_id', $this->entorno->id)->count());

        $this->sincronizar($this->inventarioDe(2))->assertOk();

        $this->assertSame(2, Plugin::where('environment_id', $this->entorno->id)->count());
        $this->assertSame(0, Plugin::where('component', 'local_plugin5')->count());
    }

    public function test_un_moodle_sin_plugins_no_rompe(): void
    {
        // Caso raro pero posible: un Moodle recién instalado, o un fallo al recoger el
        // inventario en el cliente.
        $this->sincronizar($this->inventarioDe(3))->assertOk();

        // `data.plugins` no puede ir vacío —la regla `required` lo trata como ausente—,
        // así que el caso real es que llegue un único plugin.
        $this->sincronizar([['name' => 'Solo uno', 'component' => 'local_uno']])->assertOk();

        $this->assertSame(1, Plugin::where('environment_id', $this->entorno->id)->count());
    }
}
