<?php

namespace Tests\Feature\Products;

use App\Livewire\Features\VersionEdit as FeaturesVersionEdit;
use App\Livewire\Js\FileUpload as JsFileUpload;
use App\Livewire\Scss\FileUpload as ScssFileUpload;
use App\Livewire\ScssCdn\BundleShow;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\Features\FeatureVersion;
use App\Models\Js\JsFile;
use App\Models\Js\JsVersion;
use App\Models\Products\LicenseToken;
use App\Models\Products\Product;
use App\Models\Scss\ScssFile;
use App\Models\Scss\ScssVersion;
use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\ScssCdn\ScssCdnFile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Revisión de los formularios de contenido versionado: subir, editar y ver.
 *
 * Lo que se fija son **tres agujeros que la revisión encontró** y que ningún test cubría:
 * dos de acceso a datos de otro producto, y uno de una acción cuya consecuencia no se veía.
 *
 * El test de seguridad que ya había —`LivewireAuthorizationTest`— comprueba que ningún
 * método que **escribe** se quede sin `authorize()`. No veía nada de esto: dos de los tres
 * casos tienen su `authorize()` puesto, y el problema es que el id del recurso llega del
 * navegador **sin acotarlo a su padre** (MGR-006). El tercero solo lee.
 */
class ContenidoRevisionTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        $rol = Role::firstOrCreate(['name' => 'Admin', 'guard_name' => 'web']);

        foreach ([
            'admin.products.index', 'admin.products.show',
            'admin.scss.index', 'admin.scss.file.create', 'admin.scss.file.destroy', 'admin.scss.version.edit',
            'admin.js.index', 'admin.js.file.create', 'admin.js.file.destroy', 'admin.js.version.edit',
            'admin.scss-cdn.index', 'admin.scss-cdn.bundle.show', 'admin.scss-cdn.bundle.edit',
            'admin.features.index', 'admin.features.version.edit',
        ] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'products', 'description' => $nombre]
            ));
        }

        $user = User::factory()->create();
        $user->assignRole($rol);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

    private function producto(string $slug): Product
    {
        return Product::create([
            'slug' => $slug,
            'name' => ucfirst($slug),
            'tech' => 'moodle',
            'actived' => true,
            'enabled' => true,
        ]);
    }

    /* ===================== El IDOR de la subida de archivos ===================== */

    public function test_al_sobrescribir_no_se_puede_borrar_un_archivo_de_otro_producto(): void
    {
        // **`fileData` es una propiedad pública**: viaja en el snapshot y vuelve del
        // navegador. Con `ScssFile::find($id)` a secas se podía mandar el id de un archivo
        // de otra versión y de otro producto, y este código lo **borraba de la base de
        // datos** —además de intentar borrar del disco un archivo del producto actual con
        // el nombre del ajeno, descuadrando disco y base—.
        $this->admin();

        $mio = $this->producto('theme_fresk');
        $ajeno = $this->producto('block_tresipuntsepe');

        $miVersion = ScssVersion::create(['product_id' => $mio->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);
        $suVersion = ScssVersion::create(['product_id' => $ajeno->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);

        $suArchivo = ScssFile::create([
            'scss_version_id' => $suVersion->id,
            'filename' => 'main',
            'content' => '// el suyo',
            'created_by' => 1,
        ]);

        // El atacante entra en su propia versión y manda el id del archivo ajeno.
        Livewire::test(ScssFileUpload::class, ['product' => $mio, 'scssVersion' => $miVersion])
            ->set('fileData', [[
                'originalName' => 'main.scss',
                'filename' => 'main',
                'size' => 10,
                'content' => '// intruso',
                'isDuplicate' => true,
                'action' => 'overwrite',
                'existingFileId' => $suArchivo->id,
            ]])
            ->call('confirmUpload');

        // El archivo del otro producto sigue ahí.
        $this->assertNotNull(ScssFile::find($suArchivo->id), 'se ha borrado un archivo de otro producto');
        $this->assertSame('// el suyo', ScssFile::find($suArchivo->id)->content);
    }

    public function test_lo_mismo_en_la_subida_de_js(): void
    {
        // El gemelo de SCSS tenía el mismo agujero, palabra por palabra.
        $this->admin();

        $mio = $this->producto('theme_fresk');
        $ajeno = $this->producto('block_tresipuntsepe');

        $miVersion = JsVersion::create(['product_id' => $mio->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);
        $suVersion = JsVersion::create(['product_id' => $ajeno->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);

        $suArchivo = JsFile::create([
            'js_version_id' => $suVersion->id,
            'filename' => 'init',
            'content' => '// el suyo',
            'created_by' => 1,
        ]);

        Livewire::test(JsFileUpload::class, ['product' => $mio, 'jsVersion' => $miVersion])
            ->set('fileData', [[
                'originalName' => 'init.js',
                'filename' => 'init',
                'size' => 10,
                'content' => '// intruso',
                'isDuplicate' => true,
                'action' => 'overwrite',
                'existingFileId' => $suArchivo->id,
            ]])
            ->call('confirmUpload');

        $this->assertNotNull(JsFile::find($suArchivo->id), 'se ha borrado un archivo de otro producto');
    }

    /* ===================== La fuga de la URL firmada ===================== */

    public function test_no_se_puede_sacar_la_url_de_un_archivo_de_otro_bundle(): void
    {
        // En Livewire **cualquier método público es invocable desde el navegador**, y este
        // devolvía la URL de cualquier archivo por su id. Esa URL lleva su `secure_token`:
        // con ella el archivo se descarga. No borraba nada, pero exponía contenido de otros
        // productos a cualquiera con sesión.
        $this->admin();

        $mio = $this->producto('theme_fresk');
        $ajeno = $this->producto('block_tresipuntsepe');

        $miBundle = ScssCdnBundle::create(['product_id' => $mio->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);
        $suBundle = ScssCdnBundle::create(['product_id' => $ajeno->id, 'version' => '2026010100', 'created_by' => 1, 'updated_by' => 1]);

        $suArchivo = ScssCdnFile::create([
            'scss_cdn_bundle_id' => $suBundle->id,
            'relative_path' => 'secreto.scss',
            'filename' => 'secreto.scss',
            'is_servable' => true,
            'secure_token' => 'un-token-que-no-deberia-salir',
            'created_by' => 1,
            'updated_by' => 1,
        ]);

        $componente = Livewire::test(BundleShow::class, ['product' => $mio, 'bundle' => $miBundle]);

        $this->assertNull(
            $componente->instance()->getFileUrl($suArchivo->id),
            'se ha devuelto la URL firmada de un archivo de otro producto'
        );
    }

    /* ===================== El aviso al cambiar el número de versión ===================== */

    /** Un entorno con licencia del producto y el plugin instalado en la versión dada. */
    private function cliente(Product $producto, string $instalada): Environment
    {
        $cliente = Client::firstOrCreate(['shortname' => 'rev'], ['name' => 'Cliente Revisión', 'actived' => true]);

        $token = LicenseToken::create([
            'token' => '3IP-REVI-' . str_pad((string) (LicenseToken::count() + 1), 4, '0', STR_PAD_LEFT) . '-REVI-REVI',
            'client_id' => $cliente->id,
            'active' => true,
        ]);
        $token->products()->attach($producto->id);

        $entorno = Environment::create([
            'name' => 'Campus ' . $instalada,
            'domain' => 'https://c' . $instalada . '.rev.test',
            'env' => 'pro',
            'version' => '5.1.1',
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
            'active' => true,
            'refresh_at' => now()->subHour(),
        ]);

        Plugin::create([
            'environment_id' => $entorno->id,
            'name' => $producto->name,
            'component' => $producto->slug,
            'type' => explode('_', $producto->slug)[0],
            'versiondisk' => $instalada,
            'versiondb' => $instalada,
            'versionrequires' => '2024100700',
            'release' => '1.0.0',
            'has_updates' => false,
            'has_dependencies' => false,
        ]);

        return $entorno;
    }

    public function test_subir_el_numero_por_encima_del_plugin_avisa_de_que_se_queda_sin_nada(): void
    {
        // **Es la acción con la consecuencia más grande y más invisible de estas
        // pantallas.** El número de una versión es lo único que decide a quién le llega su
        // contenido: subirlo por encima del plugin de un cliente lo deja sin ninguna
        // versión compatible, y la API le devuelve un error. No falla, no avisa.
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');
        $this->cliente($producto, '2026080702');

        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $consecuencia = Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version])
            ->set('version', '2030010100')
            ->viewData('consecuencia');

        $this->assertSame(1, $consecuencia['sinNada']);
        $this->assertTrue($consecuencia['grave']);
    }

    public function test_renombrarla_sin_cambiar_de_manos_no_avisa_de_nada(): void
    {
        // **El caso que hacía mentir al aviso en su primera versión.** Al renombrar, el
        // número cambia pero el contenido es el mismo: si el plugin del cliente sigue
        // alcanzando la versión, no ha perdido nada. Comparar los números a secas contaba
        // eso como una pérdida, y era el caso más común —corregir un número sin querer
        // cambiar nada de sitio—.
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');
        $this->cliente($producto, '2026080702');

        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $consecuencia = Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version])
            ->set('version', '2020010100')
            ->viewData('consecuencia');

        $this->assertNull($consecuencia);
    }

    public function test_sin_cambiar_el_numero_no_hay_aviso(): void
    {
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');
        $this->cliente($producto, '2026080702');

        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $this->assertNull(
            Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version])
                ->viewData('consecuencia')
        );
    }

    public function test_un_numero_a_medio_escribir_no_avisa_en_cada_tecla(): void
    {
        // El aviso se recalcula al teclear: con un número incompleto diría una cosa
        // distinta en cada pulsación, y eso es ruido, no información.
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');
        $this->cliente($producto, '2026080702');

        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $componente = Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version]);

        foreach (['2', '20', '2030', '203001010'] as $aMedias) {
            $this->assertNull($componente->set('version', $aMedias)->viewData('consecuencia'), "avisó con {$aMedias}");
        }
    }

    public function test_un_cliente_que_pasa_a_recibirla_se_cuenta_aparte_y_no_es_grave(): void
    {
        // Dar contenido a quien no lo veía no rompe nada, así que no se pinta como una
        // alarma: si todo lo que se toca sale en rojo, el rojo deja de significar algo.
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');
        // Su plugin no alcanza la 2026080702, pero sí alcanzaría una más baja.
        $this->cliente($producto, '2026010100');

        FeatureVersion::create(['product_id' => $producto->id, 'version' => '2025010100', 'created_by' => 1, 'updated_by' => 1]);
        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $consecuencia = Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version])
            ->set('version', '2026010000')
            ->viewData('consecuencia');

        $this->assertSame(1, $consecuencia['ganan']);
        $this->assertSame(0, $consecuencia['sinNada']);
        $this->assertFalse($consecuencia['grave']);
    }

    public function test_los_clientes_sin_el_plugin_no_entran_en_la_cuenta(): void
    {
        // Si entraran, el aviso diría que el cambio afecta a 19 clientes a los que no les
        // llega nada, y sería falso.
        $this->admin();
        $producto = $this->producto('block_tresipuntsepe');

        // Con licencia y sin el plugin instalado.
        $cliente = Client::firstOrCreate(['shortname' => 'rev'], ['name' => 'Cliente Revisión', 'actived' => true]);
        $token = LicenseToken::create(['token' => '3IP-SINP-LUGI-NNNN-NNNN', 'client_id' => $cliente->id, 'active' => true]);
        $token->products()->attach($producto->id);
        Environment::create([
            'name' => 'Sin el plugin',
            'domain' => 'https://sinplugin.rev.test',
            'env' => 'pro',
            'version' => '5.1.1',
            'client_id' => $cliente->id,
            'license_token_id' => $token->id,
            'active' => true,
            'refresh_at' => now()->subHour(),
        ]);

        $version = FeatureVersion::create(['product_id' => $producto->id, 'version' => '2026080702', 'created_by' => 1, 'updated_by' => 1]);

        $this->assertNull(
            Livewire::test(FeaturesVersionEdit::class, ['product' => $producto, 'featureVersion' => $version])
                ->set('version', '2030010100')
                ->viewData('consecuencia')
        );
    }
}
