<?php

namespace Tests\Feature\Products;

use App\Support\RevisionDeCodigo;
use Tests\TestCase;

/**
 * La revisión sencilla de un archivo antes de guardarlo.
 *
 * **Por qué solo esto.** No hay compilador de SASS ni motor de JavaScript en el servidor, y
 * meterlos era una decisión más grande de la que hacía falta. Esto mira lo que se puede mirar
 * leyendo el texto, que resulta ser lo que más veces rompe un archivo de verdad: algo sin
 * cerrar, y un `@import` que apunta a un archivo que no está.
 *
 * Lo que **no** hace también importa, y estos tests lo fijan: no se inventa desbalances por
 * una llave dentro de una cadena ni por una dentro de un comentario. Un aviso falso se deja
 * de leer a la segunda vez, y entonces sobra el aviso entero.
 */
class RevisionDeCodigoTest extends TestCase
{
    /** @return array<int, string> */
    private function mensajes(array $avisos, ?string $tipo = null): array
    {
        return array_values(array_map(
            fn (array $aviso) => $aviso['mensaje'],
            $tipo === null ? $avisos : array_filter($avisos, fn ($a) => $a['tipo'] === $tipo)
        ));
    }

    /* ===================== Lo que cierra y lo que no ===================== */

    public function test_un_scss_correcto_no_dice_nada(): void
    {
        $codigo = <<<'SCSS'
        $primario: #ff6a00;

        .boton {
            color: $primario;

            &:hover {
                color: darken($primario, 10%);
            }
        }
        SCSS;

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS));
    }

    public function test_una_llave_sin_cerrar_es_un_error_y_dice_la_linea(): void
    {
        // Es el fallo más caro: un SCSS que no compila deja al cliente **sin nada** de ese
        // producto, no solo sin la regla mal escrita.
        $codigo = ".boton {\n    color: red;\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertCount(1, $avisos);
        $this->assertStringContainsString('Falta cerrar la llave', $avisos[0]['mensaje']);
        $this->assertSame(1, $avisos[0]['linea']);
    }

    public function test_una_llave_de_mas_tambien(): void
    {
        $codigo = ".boton {\n    color: red;\n}\n}\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertStringContainsString('Sobra un «}»', $avisos[0]['mensaje']);
        $this->assertSame(4, $avisos[0]['linea']);
    }

    public function test_cerrar_con_el_signo_que_no_toca_se_dice_con_las_dos_lineas(): void
    {
        $codigo = ".boton {\n    color: rgb(1, 2, 3;\n}\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertStringContainsString('Se cierra con «}» un «(» de la línea 2', $avisos[0]['mensaje']);
    }

    public function test_un_comentario_sin_cerrar_se_dice_donde_empieza(): void
    {
        // Es la causa habitual del desbalance, y el desbalance aparece a cien líneas de
        // donde está el fallo de verdad. Se dice el sitio bueno.
        $codigo = ".a { color: red; }\n/* esto se me olvidó cerrar\n.b { color: blue; }\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertStringContainsString('comentario', $avisos[0]['mensaje']);
        $this->assertSame(2, $avisos[0]['linea']);
    }

    public function test_una_comilla_sin_cerrar_se_dice(): void
    {
        $codigo = ".a { content: 'hola; }\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertStringContainsString('comilla', $avisos[0]['mensaje']);
    }

    /* ===================== Lo que NO debe avisar ===================== */

    public function test_una_llave_dentro_de_una_cadena_no_cuenta(): void
    {
        // **El aviso falso es peor que no avisar**: a la segunda vez se deja de leer.
        $codigo = ".a { content: \"{\"; }\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS));
    }

    public function test_una_llave_dentro_de_un_comentario_tampoco(): void
    {
        $codigo = "/* .vieja { color: red; */\n.a { color: blue; }\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS));
    }

    public function test_una_llave_comentada_con_dos_barras_tampoco(): void
    {
        $codigo = "// .vieja {\n.a { color: blue; }\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS));
    }

    public function test_una_barra_escapada_dentro_de_una_cadena_no_la_cierra_antes(): void
    {
        $codigo = ".a { content: \"comilla \\\" dentro\"; }\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS));
    }

    /* ===================== Los @import ===================== */

    public function test_un_import_que_no_esta_en_la_version_se_avisa_sin_frenar(): void
    {
        // **El fallo propio de este panel**: los archivos se suben de uno en uno y es fácil
        // publicar el que importa antes que el importado. Ningún analizador de sintaxis lo
        // ve, porque la sintaxis está bien.
        $codigo = "@import 'variables';\n.a { color: red; }\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, ['mixins']);

        $this->assertFalse(RevisionDeCodigo::hayErrores($avisos), 'Es un aviso, no frena el guardado.');
        $this->assertCount(1, $avisos);
        $this->assertStringContainsString('variables', $avisos[0]['mensaje']);
        $this->assertSame(1, $avisos[0]['linea']);
    }

    public function test_el_guion_bajo_del_parcial_no_cuenta(): void
    {
        // `@import "variables"` trae el archivo `_variables`: es como funciona SASS, y
        // avisar de esto sería un aviso falso en el caso más normal que hay.
        $codigo = "@import 'variables';\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, ['_variables']));
    }

    public function test_la_extension_escrita_tampoco(): void
    {
        $codigo = "@import 'variables.scss';\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, ['variables']));
    }

    public function test_varios_en_la_misma_linea(): void
    {
        $codigo = "@import 'variables', 'mixins', 'colores';\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, ['variables']);

        $this->assertCount(2, $avisos);
    }

    public function test_el_use_con_alias_mira_solo_el_archivo(): void
    {
        $codigo = "@use 'variables' as v;\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, ['variables']));
    }

    public function test_una_ruta_o_una_url_no_se_comprueba(): void
    {
        // No se puede saber si existe, así que callarse es lo honesto.
        $codigo = "@import 'sass/base';\n@import url('https://fonts.example/x.css');\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::SCSS, []));
    }

    /* ===================== JS ===================== */

    public function test_un_js_correcto_no_dice_nada(): void
    {
        $codigo = <<<'JS'
        function saludar(nombre) {
            return `Hola, ${nombre}`;
        }
        JS;

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::JS));
    }

    public function test_en_js_tambien_se_ve_lo_que_no_cierra(): void
    {
        $codigo = "function a() {\n    if (b) {\n        c();\n    }\n";

        $avisos = RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::JS);

        $this->assertTrue(RevisionDeCodigo::hayErrores($avisos));
        $this->assertSame(1, $avisos[0]['linea']);
    }

    public function test_una_plantilla_con_saltos_de_linea_es_valida_en_js(): void
    {
        // Las comillas normales no saltan de línea, pero el acento grave sí: tratarlas
        // igual daría un error falso en cualquier plantilla de varias líneas.
        $codigo = "const html = `\n  <div>\n    hola\n  </div>\n`;\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::JS));
    }

    public function test_los_import_no_se_miran_en_js(): void
    {
        // La comprobación de importaciones es de SCSS: en JS un `@import` no es eso, y un
        // `import` de módulo resuelve de otra forma.
        $codigo = "@import 'variables';\n";

        $this->assertSame([], RevisionDeCodigo::revisar($codigo, RevisionDeCodigo::JS, []));
    }
}
