<?php

namespace Tests\Feature\ScssCdn;

use App\Models\Products\Product;
use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\ScssCdn\ScssCdnFile;
use App\Services\ScssCdn\ScssCdnImportParser;
use App\Services\ScssCdn\ScssCdnStorageService;
use App\Services\ScssCdn\ScssCdnUrlService;
use App\Services\System\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;

/**
 * Las direcciones firmadas del CDN y su caducidad.
 *
 * **La pregunta que hay detrás de estos tests**: ¿pueden fallar de golpe los estilos de
 * todos los entornos un día, cuando caduque algo?
 *
 * La respuesta tiene que ser **no, y por construcción**. Lo que recibe un cliente son
 * direcciones firmadas **acabadas de generar en esa misma petición**, con caducidad corta,
 * y el contenido sustituido **no se guarda en ninguna parte**. Nada envejece porque nada se
 * conserva.
 *
 * Lo que sí podía envejecer —y estuvo a punto— era el contenido **guardado** con las
 * direcciones dentro: había tres funciones que hacían justo eso, firmando a 365 días. Se
 * borraron, y estos tests son el cierre para que no vuelvan.
 */
class CaducidadDeLasUrlsTest extends TestCase
{
    use RefreshDatabase;

    private Product $producto;

    private ScssCdnBundle $bundle;

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

        Storage::fake(config('filesystems.default'));

        $this->producto = Product::create([
            'name' => 'Producto con estilos',
            'slug' => 'theme_prueba',
            'actived' => true,
        ]);

        $this->bundle = ScssCdnBundle::create([
            'product_id' => $this->producto->id,
            'version' => '2026010100',
            'description' => 'Bundle de prueba',
        ]);
    }

    private function fichero(string $ruta, string $contenido): ScssCdnFile
    {
        ScssCdnStorageService::saveFile($this->producto, $this->bundle, $ruta, $contenido);

        return ScssCdnFile::create([
            'scss_cdn_bundle_id' => $this->bundle->id,
            'relative_path' => $ruta,
            'filename' => pathinfo($ruta, PATHINFO_FILENAME),
            'is_servable' => false,
            'secure_token' => Str::random(32),
        ]);
    }

    /* ============ Nada de lo que caduca se guarda ============ */

    public function test_no_se_puede_guardar_un_scss_con_una_direccion_firmada_dentro(): void
    {
        // **El cierre de la bomba.** El SCSS se guarda como lo escribió su autor; la
        // sustitución se hace al servir y en memoria. Guardar el resultado sustituido deja
        // direcciones que mueren solas dentro del contenido.
        $entrada = $this->fichero('main.scss', "@import 'variables';");
        $variables = $this->fichero('_variables.scss', '$rojo: red;');

        $servido = ScssCdnImportParser::processImportsForApi(
            "@import 'variables';",
            $this->bundle,
            'main.scss'
        );

        // Lo servido lleva la dirección firmada: es lo correcto para la respuesta…
        $this->assertStringContainsString('/cdn/scss/', $servido);
        $this->assertStringContainsString('signature=', $servido);

        // …y guardarlo es justo lo que no puede pasar.
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessageMatches('/caduca/');

        ScssCdnStorageService::saveFile($this->producto, $this->bundle, 'main.scss', $servido);
    }

    public function test_un_scss_normal_se_guarda_sin_problema(): void
    {
        // El guardián no puede estorbar al contenido de verdad.
        $this->assertTrue(ScssCdnStorageService::saveFile(
            $this->producto,
            $this->bundle,
            'main.scss',
            "@import 'variables';\n@import url('https://fonts.googleapis.com/css2?family=Inter');\nbody { color: red; }"
        ));
    }

    public function test_una_mencion_al_cdn_en_un_comentario_no_estorba(): void
    {
        // Solo se rechaza la ruta del CDN **y** una firma juntas: la ruta sola puede
        // aparecer en un comentario explicando cómo funciona esto.
        $this->assertTrue(ScssCdnStorageService::saveFile(
            $this->producto,
            $this->bundle,
            'main.scss',
            "// El Manager sustituye estos imports por /cdn/scss/… al servir\n@import 'variables';"
        ));
    }

    public function test_el_parser_ya_no_tiene_las_funciones_que_guardaban(): void
    {
        // **El barrido.** Borrar las tres funciones no impide que alguien las vuelva a
        // escribir; esto lo caza, y el guardián de arriba impide el daño mientras.
        foreach (['replaceImports', 'processBundle', 'processFile'] as $muerta) {
            $this->assertFalse(
                method_exists(ScssCdnImportParser::class, $muerta),
                "ScssCdnImportParser::{$muerta}() sustituía los imports y GUARDABA el resultado "
                . 'con una caducidad de 365 días. No debe volver.'
            );
        }
    }

    /* ============ Y lo que se sirve se firma corto y de nuevo cada vez ============ */

    public function test_cada_peticion_firma_una_direccion_distinta(): void
    {
        // Es la razón por la que nada envejece: la dirección de una petición no es la de la
        // siguiente, así que no hay nada que pueda caducar «para todos a la vez».
        $fichero = $this->fichero('_variables.scss', '$rojo: red;');

        $primera = ScssCdnUrlService::getFileUrlForApi($fichero);
        $this->travel(2)->seconds();
        $segunda = ScssCdnUrlService::getFileUrlForApi($fichero);

        $this->assertNotSame($primera, $segunda, 'Cada petición tiene que firmar de nuevo.');
    }

    public function test_la_caducidad_de_la_api_sale_del_ajuste(): void
    {
        $fichero = $this->fichero('_variables.scss', '$rojo: red;');

        // Por defecto, una hora: lo que llevaba funcionando.
        $this->assertSame(3600, app(Settings::class)->segundosDeUrlDeApi());

        app(Settings::class)->set(Settings::SCSSCDN_URL_API_SEGUNDOS, '120');
        app(Settings::class)->olvidar();

        $this->assertSame(120, app(Settings::class)->segundosDeUrlDeApi());

        $url = ScssCdnUrlService::getFileUrlForApi($fichero);

        parse_str((string) parse_url($url, PHP_URL_QUERY), $parametros);

        // El margen es por el segundo que puede pasar entre firmar y comprobar.
        $this->assertLessThanOrEqual(now()->addSeconds(121)->timestamp, (int) $parametros['expires']);
        $this->assertGreaterThan(now()->addSeconds(60)->timestamp, (int) $parametros['expires']);
    }

    public function test_el_minimo_de_la_caducidad_de_la_api_se_respeta(): void
    {
        // **Una ventana demasiado corta es el riesgo real**, no una larga: esa dirección
        // viaja dentro del SCSS que Moodle compila, y si caduca antes de que siga cada
        // `@import`, el sitio se queda sin parte de los estilos y no falla nada visible.
        app(Settings::class)->set(Settings::SCSSCDN_URL_API_SEGUNDOS, '1');
        app(Settings::class)->olvidar();

        $this->assertSame(30, app(Settings::class)->segundosDeUrlDeApi());
    }

    public function test_la_del_panel_sale_de_su_propio_ajuste(): void
    {
        // Es otra cosa: el enlace que se pinta para inspeccionar un fichero. **No afecta a
        // lo que reciben los clientes**, y por eso puede ser larga sin consecuencias.
        $this->assertSame(365, app(Settings::class)->diasDeUrlDelPanel());

        app(Settings::class)->set(Settings::SCSSCDN_URL_PANEL_DIAS, '7');
        app(Settings::class)->olvidar();

        $this->assertSame(7, app(Settings::class)->diasDeUrlDelPanel());
    }

    /* ============ El tope del ZIP, en un solo sitio ============ */

    public function test_el_tope_del_zip_sale_del_ajuste_y_alimenta_las_dos_comprobaciones(): void
    {
        // Estaba escrito dos veces con el mismo 50: en la validación del formulario y en el
        // servicio que abre el ZIP. Es el patrón que ya se pagó con los límites de subida.
        $this->assertSame(50 * 1024, app(Settings::class)->zipDeBundleEnKb());
        $this->assertSame(50 * 1024 * 1024, app(Settings::class)->zipDeBundleEnBytes());

        app(Settings::class)->set(Settings::SCSSCDN_ZIP_MB, '10');
        app(Settings::class)->olvidar();

        $this->assertSame(10 * 1024, app(Settings::class)->zipDeBundleEnKb());
        $this->assertSame(10 * 1024 * 1024, app(Settings::class)->zipDeBundleEnBytes());
    }

    public function test_los_tres_ajustes_estan_en_el_catalogo(): void
    {
        // Si no están en el catálogo no salen en la pantalla, y un ajuste que no se puede
        // ver es una constante con pasos extra.
        $delGrupo = array_keys(Settings::CATALOGO['contenido']['ajustes']);

        foreach ([
            Settings::SCSSCDN_ZIP_MB,
            Settings::SCSSCDN_URL_API_SEGUNDOS,
            Settings::SCSSCDN_URL_PANEL_DIAS,
        ] as $clave) {
            $this->assertContains($clave, $delGrupo);
        }
    }
}
