<?php

namespace Tests\Feature\ScssCdn;

use App\Models\Products\Product;
use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\ScssCdn\ScssCdnFile;
use App\Services\ScssCdn\PuntosDeEntrada;
use App\Services\ScssCdn\ScssCdnStorageService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;

/**
 * Detectar y marcar los puntos de entrada de un bundle (MGR-093).
 *
 * **El fallo que arregla**: al subir un ZIP, cada fichero entraba con `is_servable = false`
 * y no había ninguna detección. **Nada quedaba servible**, así que la acción `scss-cdn`
 * respondía «no hay ficheros servibles» — que por la norma cero errores **no es un error** y
 * no avisa a nadie—. Subir el ZIP y marcharse dejaba el bundle publicado y **vacío a efectos
 * prácticos**.
 *
 * **La definición es la del lenguaje, no una heurística**: un punto de entrada es un fichero
 * al que nadie importa. Y el dato ya estaba calculado —`findFileInBundle()` resuelve cada
 * `@import` y se usa al servir—, así que aquí se reutiliza: si la detección resolviera por
 * su cuenta, diría una cosa y el servicio haría otra.
 *
 * **Y solo marca cuando hay exactamente uno.** La primera versión marcaba todos los que
 * nadie importa; se corrigió al saber cómo son los bundles de verdad —el de `theme_fresk` en
 * producción tiene **un** solo servible, porque el theme apunta a un punto de entrada—.
 *
 * **Y no porque un servible de más rompa el theme**: eso se afirmó primero y el autor del
 * theme lo corrigió —él solo busca los `@import` de su entrada, así que uno de más lo
 * ignora—. El motivo es que **dos candidatos casi siempre son un fichero suelto en el ZIP**,
 * y marcarlo en silencio esconde ese aviso.
 */
class PuntosDeEntradaTest extends TestCase
{
    use RefreshDatabase;

    private Product $producto;

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

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

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

    /** Crea el fichero en la base y su contenido en el almacenamiento, como la subida. */
    private function fichero(ScssCdnBundle $bundle, string $ruta, string $contenido): ScssCdnFile
    {
        ScssCdnStorageService::saveFile($this->producto, $bundle, $ruta, $contenido);

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

    public function test_marca_el_que_nadie_importa_y_deja_los_demas(): void
    {
        // El árbol de siempre: un punto de entrada que importa variables y mixins.
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';\n@import 'mixins';\nbody { color: red; }");
        $variables = $this->fichero($bundle, 'variables.scss', '$rojo: red;');
        $mixins = $this->fichero($bundle, 'mixins.scss', '@mixin nada {}');

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertSame(1, $resultado['marcados']);
        $this->assertSame(3, $resultado['total']);
        $this->assertFalse($resultado['ninguno']);

        $this->assertTrue($main->fresh()->is_servable, 'main es el punto de entrada');
        $this->assertFalse($variables->fresh()->is_servable, 'variables lo importa main');
        $this->assertFalse($mixins->fresh()->is_servable);
    }

    public function test_resuelve_los_partials_con_guion_bajo(): void
    {
        // **La convención de SCSS**: `@import 'variables'` apunta a `_variables.scss`. Si la
        // detección no lo resolviera, marcaría el partial como punto de entrada.
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';");
        $partial = $this->fichero($bundle, '_variables.scss', '$rojo: red;');

        PuntosDeEntrada::marcar($bundle);

        $this->assertTrue($main->fresh()->is_servable);
        $this->assertFalse($partial->fresh()->is_servable, 'el partial no es punto de entrada');
    }

    public function test_resuelve_los_imports_en_carpetas(): void
    {
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'utils/helpers';");
        $helper = $this->fichero($bundle, 'utils/helpers.scss', '// nada');

        PuntosDeEntrada::marcar($bundle);

        $this->assertTrue($main->fresh()->is_servable);
        $this->assertFalse($helper->fresh()->is_servable);
    }

    public function test_con_varios_candidatos_no_marca_ninguno_y_los_nombra(): void
    {
        // **El caso que cambió el diseño.** Antes se marcaban los dos. Un theme apunta a
        // **un** punto de entrada en su configuración —en producción, el bundle de
        // `theme_fresk` tiene un solo servible—, así que dos candidatos casi siempre
        // significan que en el ZIP hay un fichero que nadie importa y que no debería estar:
        // una hoja de pruebas, o uno que se quedó suelto al refactorizar.
        //
        // **Lo que NO se afirma aquí es que un servible de más rompa el theme.** Se dijo
        // primero y el autor del theme lo corrigió: él solo busca los `@import` de su
        // entrada. El Manager lo entrega en la respuesta y lo que haga el consumidor con
        // lo que le sobra no lo decide esta detección. Lo que se protege es el aviso:
        // marcarlo en silencio esconde que el ZIP lleva un fichero suelto.
        $bundle = $this->bundle();

        $pantalla = $this->fichero($bundle, 'pantalla.scss', "@import 'base';");
        $impresion = $this->fichero($bundle, 'impresion.scss', "@import 'base';");
        $base = $this->fichero($bundle, 'base.scss', 'body {}');

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertSame(0, $resultado['marcados'], 'Con dos candidatos no se marca ninguno.');
        $this->assertTrue($resultado['ambiguo']);
        // **No es «ninguno»**, que es otra situación: aquí candidatos hay: sobran.
        $this->assertFalse($resultado['ninguno']);

        // Y se nombran, para que la pantalla pueda decir cuáles revisar.
        $this->assertSame(['impresion.scss', 'pantalla.scss'], $resultado['candidatos']);

        $this->assertFalse($pantalla->fresh()->is_servable);
        $this->assertFalse($impresion->fresh()->is_servable);
        $this->assertFalse($base->fresh()->is_servable);
    }

    public function test_el_caso_de_verdad_es_uno_solo(): void
    {
        // El árbol de un theme: una entrada y sus parciales. Es lo que hay en producción, y
        // es el caso en el que no hay nada que decidir.
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';\n@import 'mixins';\n@import 'componentes/botones';");
        $variables = $this->fichero($bundle, '_variables.scss', '$rojo: red;');
        $mixins = $this->fichero($bundle, '_mixins.scss', '@mixin nada {}');
        $botones = $this->fichero($bundle, 'componentes/_botones.scss', '.btn {}');

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertSame(1, $resultado['marcados']);
        $this->assertFalse($resultado['ambiguo']);
        $this->assertTrue($main->fresh()->is_servable);

        foreach ([$variables, $mixins, $botones] as $parcial) {
            $this->assertFalse($parcial->fresh()->is_servable);
        }
    }

    public function test_si_todos_se_importan_entre_si_no_marca_nada_y_lo_dice(): void
    {
        // **El caso donde adivinar sería peor que no hacer nada.** Un ciclo, o un bundle
        // que es todo partials: elegir uno a dedo dejaría servible algo que quizá no debe.
        $bundle = $this->bundle();

        $a = $this->fichero($bundle, 'a.scss', "@import 'b';");
        $b = $this->fichero($bundle, 'b.scss', "@import 'a';");

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertSame(0, $resultado['marcados']);
        $this->assertTrue($resultado['ninguno'], 'hay que poder avisar de que no se ha deducido');
        $this->assertFalse($a->fresh()->is_servable);
        $this->assertFalse($b->fresh()->is_servable);
    }

    public function test_no_toca_lo_que_ya_estaba_marcado_a_mano(): void
    {
        // Si alguien ajustó las marcas y vuelve a subir, la subida no debe deshacérselo.
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';");
        $variables = $this->fichero($bundle, 'variables.scss', '$rojo: red;');

        // El autor decidió que variables también se sirve. Nadie lo desmarca: la detección
        // solo añade, y su opinión sobre su propio bundle vale más que esta comprobación.
        $variables->update(['is_servable' => true]);

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertTrue($main->fresh()->is_servable);
        $this->assertTrue($variables->fresh()->is_servable, 'lo puesto a mano se respeta');
        $this->assertSame(1, $resultado['marcados'], 'solo cuenta los que ha marcado él');
    }

    public function test_si_la_entrada_ya_estaba_marcada_no_se_cuenta_dos_veces(): void
    {
        // Volver a subir el mismo ZIP no marca nada nuevo, y el mensaje de la pantalla
        // depende de este número: con 0 dice «ya estaba marcado» en vez de «marcado 1».
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';");
        $this->fichero($bundle, '_variables.scss', '$rojo: red;');

        $main->update(['is_servable' => true]);

        $resultado = PuntosDeEntrada::marcar($bundle);

        $this->assertSame(0, $resultado['marcados']);
        $this->assertFalse($resultado['ambiguo']);
        $this->assertTrue($main->fresh()->is_servable);
    }

    public function test_un_bundle_de_un_solo_fichero_lo_marca(): void
    {
        $bundle = $this->bundle();
        $solo = $this->fichero($bundle, 'estilos.scss', 'body { margin: 0; }');

        PuntosDeEntrada::marcar($bundle);

        $this->assertTrue($solo->fresh()->is_servable);
    }

    public function test_un_bundle_vacio_no_revienta(): void
    {
        $resultado = PuntosDeEntrada::marcar($this->bundle());

        $this->assertSame(0, $resultado['marcados']);
        $this->assertSame(0, $resultado['total']);
        $this->assertTrue($resultado['ninguno']);
        $this->assertFalse($resultado['ambiguo']);
    }

    public function test_un_fichero_que_no_se_puede_leer_no_para_la_deteccion(): void
    {
        // Puede pasar: fila en la base y contenido que no está. Se salta y se sigue, en vez
        // de dejar el bundle entero sin marcar.
        $bundle = $this->bundle();

        $main = $this->fichero($bundle, 'main.scss', "@import 'variables';");
        $variables = $this->fichero($bundle, 'variables.scss', '$rojo: red;');

        // Una fila sin contenido en el almacenamiento.
        ScssCdnFile::create([
            'scss_cdn_bundle_id' => $bundle->id,
            'relative_path' => 'fantasma.scss',
            'filename' => 'fantasma',
            'is_servable' => false,
            'secure_token' => Str::random(32),
        ]);

        $resultado = PuntosDeEntrada::marcar($bundle);

        // El fantasma no lo importa nadie, así que también sale como candidato — lo que no
        // se puede leer no se puede descartar—. Y con dos candidatos **no se marca
        // ninguno**: es justo el caso en el que hay que mirarlo, porque uno de los dos es
        // una fila sin contenido en el almacenamiento.
        $this->assertTrue($resultado['ambiguo']);
        $this->assertSame(0, $resultado['marcados']);
        $this->assertFalse($main->fresh()->is_servable);
        $this->assertFalse($variables->fresh()->is_servable);
        $this->assertContains('fantasma.scss', $resultado['candidatos']);
    }
}
