<?php

namespace Tests\Feature\Api;

use App\Models\Features\FeatureVersion;
use App\Models\Js\JsVersion;
use App\Models\Products\Product;
use App\Models\Resources\ResourceVersion;
use App\Models\Scss\ScssVersion;
use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\Setups\Setup;
use App\Models\Tutorials\TutorialVersion;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * Qué versión de contenido le toca a un Moodle.
 *
 * **Es la regla de negocio central del Manager**: la API no sirve la última versión
 * publicada, sirve **la más alta que no supere la versión del plugin instalado en ese
 * sitio**. De ella depende qué recibe cada cliente.
 *
 * Estaba escrita **siete veces**, una por tipo de contenido, con el mismo cuerpo palabra
 * por palabra (MGR-024). Ahora es una sola —`ResuelveVersionCompatible`— y este test
 * comprueba que **los siete se comportan igual**, que es lo que no se podía garantizar
 * antes: cualquier matiz había que aplicarlo siete veces y una divergencia no la avisaba
 * nada. De hecho había una: solo `Setup` filtraba por estado (MGR-023).
 */
class ResolucionDeVersionTest extends TestCase
{
    use RefreshDatabase;

    private Product $producto;

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

        $this->producto = Product::create([
            'name' => 'Producto de la resolución',
            'slug' => 'resolucion',
            'active' => true,
        ]);
    }

    /**
     * Los seis tipos sin estado, que deben resolver **exactamente igual**.
     *
     * @return array<string, array{class-string}>
     */
    public static function tiposDeContenido(): array
    {
        return [
            'features' => [FeatureVersion::class],
            'tutoriales' => [TutorialVersion::class],
            'recursos' => [ResourceVersion::class],
            'scss' => [ScssVersion::class],
            'js' => [JsVersion::class],
            'bundles de cdn' => [ScssCdnBundle::class],
        ];
    }

    /** Una versión de cualquiera de los seis: todos comparten `product_id` y `version`. */
    private function version(string $clase, string $version): object
    {
        return $clase::create([
            'product_id' => $this->producto->id,
            'version' => $version,
            'description' => 'v' . $version,
        ]);
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_sirve_la_mas_alta_que_no_supere_al_plugin(string $clase): void
    {
        $this->version($clase, '2026010100');
        $laQueToca = $this->version($clase, '2026060100');
        $demasiadoNueva = $this->version($clase, '2026120100');

        $resuelta = $clase::findCompatibleVersion($this->producto, '2026070100');

        $this->assertNotNull($resuelta);
        $this->assertSame($laQueToca->id, $resuelta->id);
        $this->assertNotSame($demasiadoNueva->id, $resuelta->id,
            'una versión más alta que el plugin no se sirve nunca');
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_la_coincidencia_exacta_gana(string $clase): void
    {
        $this->version($clase, '2026010100');
        $exacta = $this->version($clase, '2026060100');

        $resuelta = $clase::findCompatibleVersion($this->producto, '2026060100');

        $this->assertSame($exacta->id, $resuelta->id);
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_si_el_plugin_es_mas_antiguo_que_todo_no_recibe_nada(string $clase): void
    {
        // **No es un error, y es el caso que confunde**: el plugin recibe un `*002`, que
        // significa «no hay contenido publicado para esa versión». Aquí se comprueba que
        // el resolvedor devuelve null y no la versión más baja.
        $this->version($clase, '2026060100');
        $this->version($clase, '2026120100');

        $this->assertNull($clase::findCompatibleVersion($this->producto, '2025010100'));
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_sin_ninguna_version_publicada_devuelve_null(string $clase): void
    {
        // El estado normal del catálogo: 2 de 7 tipos tienen alguna versión.
        $this->assertNull($clase::findCompatibleVersion($this->producto, '2026070100'));
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_no_se_lleva_las_versiones_de_otro_producto(string $clase): void
    {
        $otro = Product::create(['name' => 'Otro', 'slug' => 'otro', 'active' => true]);

        $clase::create([
            'product_id' => $otro->id,
            'version' => '2026060100',
            'description' => 'del otro producto',
        ]);

        $this->assertNull($clase::findCompatibleVersion($this->producto, '2026070100'));
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_compara_por_numero_y_no_por_texto(string $clase): void
    {
        // **La trampa que trae resolver en SQL.** `version` es una columna de texto, y en
        // sqlite un entero y un texto no se comparan por valor: los enteros van antes que
        // cualquier texto, así que un `where('version','<=',2026070100)` devolvería
        // cualquier cosa. Por eso la consulta usa `version + 0`.
        //
        // Y en orden de texto, «2026120100» va antes que «2026070100»: si se ordenara como
        // cadena, la respuesta sería la versión de diciembre, que es justo la que **no**
        // hay que servir.
        $this->version($clase, '2026120100');
        $laQueToca = $this->version($clase, '2026070100');

        $resuelta = $clase::findCompatibleVersion($this->producto, '2026070100');

        $this->assertSame($laQueToca->id, $resuelta->id);
    }

    /**
     * @dataProvider tiposDeContenido
     */
    public function test_acepta_el_producto_por_id_y_por_modelo(string $clase): void
    {
        // Las siete copias aceptaban las dos formas, y hay llamadas de los dos tipos.
        $version = $this->version($clase, '2026060100');

        $porModelo = $clase::findCompatibleVersion($this->producto, '2026070100');
        $porId = $clase::findCompatibleVersion($this->producto->id, '2026070100');

        $this->assertSame($version->id, $porModelo->id);
        $this->assertSame($version->id, $porId->id);
    }

    // ==========================================================
    // Setups: el único tipo con estado
    // ==========================================================

    public function test_un_setup_deprecado_no_se_sirve(): void
    {
        // **La única divergencia legítima entre los siete** (MGR-023): `setups` es el
        // único con columna de estado. Un `deprecated` no se sirve, y contarlo como
        // servible es lo contrario de lo que significa deprecarlo.
        Setup::create([
            'product_id' => $this->producto->id,
            'version' => '2026060100',
            'yaml' => 'clave: valor',
            'status' => 'deprecated',
        ]);

        $activo = Setup::create([
            'product_id' => $this->producto->id,
            'version' => '2026010100',
            'yaml' => 'clave: valor',
            'status' => 'active',
        ]);

        $resuelto = Setup::findCompatibleVersion($this->producto, '2026070100');

        $this->assertSame($activo->id, $resuelto->id,
            'con el más alto deprecado, se sirve el activo anterior');
    }

    public function test_un_setup_deprecado_tampoco_gana_por_coincidencia_exacta(): void
    {
        // El atajo de la coincidencia exacta va antes que el orden numérico, así que tiene
        // que respetar el estado igual: si no, deprecar una versión no serviría de nada
        // para el sitio que pide justo esa.
        Setup::create([
            'product_id' => $this->producto->id,
            'version' => '2026070100',
            'yaml' => 'clave: valor',
            'status' => 'deprecated',
        ]);

        $this->assertNull(Setup::findCompatibleVersion($this->producto, '2026070100'));
    }

    public function test_el_setup_activo_se_resuelve_como_los_demas(): void
    {
        Setup::create([
            'product_id' => $this->producto->id,
            'version' => '2026010100',
            'yaml' => 'clave: valor',
            'status' => 'active',
        ]);

        $laQueToca = Setup::create([
            'product_id' => $this->producto->id,
            'version' => '2026060100',
            'yaml' => 'clave: valor',
            'status' => 'active',
        ]);

        $this->assertSame($laQueToca->id, Setup::findCompatibleVersion($this->producto, '2026070100')->id);
    }
}
