<?php

namespace Tests\Feature\Products;

use App\Livewire\Products\Create as CrearProducto;
use App\Livewire\Products\Edit as EditarProducto;
use App\Models\Auth\User;
use App\Models\Products\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;

/**
 * El icono del producto y el tipo como etiqueta libre.
 *
 * **Dos huecos del mismo formulario, encontrados a la vez:**
 *
 * 1. **El icono no se podía poner.** La columna `products.image` existe desde enero, el
 *    accessor `image_url` estaba escrito y **tres vistas ya lo pintaban** —el listado, la
 *    ficha y la tarjeta, con `placeholder.png` de reserva—, pero ni el alta ni la edición
 *    tenían el campo. La décima aparición del patrón en este release: *funcionalidad que
 *    parecía existir y no tenía puerta*.
 * 2. **El tipo era un `<select>` de cinco opciones y obligatorio**, cuando la columna es
 *    nullable a propósito —«tipo dentro de Moodle; en otros tech se quedará null» dice la
 *    migración—. Un producto que no es un plugin —`fresk_premium`, un agrupamiento, un
 *    ecosistema— no se podía guardar sin ponerle un tipo falso. Y en la pantalla de edición
 *    el `<select>` **no tenía opción vacía**, así que un producto sin tipo se llevaba
 *    `theme` con solo abrir el formulario y guardar.
 *
 * Ni `type` ni `tech` deciden nada: no los lee la API, no emparejan con nada y no eligen
 * qué se sirve —`isMoodle()` e `isWordPress()` están en el modelo y no se llaman desde
 * ningún sitio—. Se pintan y se acabó. Por eso la respuesta es una etiqueta libre con
 * memoria y no un catálogo con su CRUD: cerrar un vocabulario que no gatea nada solo
 * añadiría una pantalla que mantener.
 */
class IconoYTipoLibreTest extends TestCase
{
    use RefreshDatabase;

    /** @param  list<string>  $permisos */
    private function usuario(array $permisos): User
    {
        foreach ($permisos as $permiso) {
            // La tabla `permissions` de este proyecto tiene `model` NOT NULL.
            Permission::firstOrCreate(
                ['name' => $permiso, 'guard_name' => 'web'],
                ['model' => 'products', 'description' => $permiso]
            );
        }

        $usuario = User::factory()->create();
        $usuario->givePermissionTo($permisos);
        $this->actingAs($usuario);

        Cache::flush();

        return $usuario;
    }

    private function producto(array $extra = []): Product
    {
        return Product::create(array_merge([
            'slug' => 'local_tresipunt' . uniqid(),
            'name' => 'Un producto',
            'summary' => 'Resumen',
            'tech' => 'moodle',
            'type' => 'local',
            'actived' => true,
            'enabled' => true,
        ], $extra));
    }

    /* ===================== El icono ===================== */

    public function test_se_puede_poner_un_icono_al_crear(): void
    {
        Storage::fake('public');
        $this->usuario(['admin.products.create']);

        Livewire::test(CrearProducto::class)
            ->set('slug', 'theme_fresk')
            ->set('name', 'Fresk')
            ->set('summary', 'El tema de la casa')
            ->set('image', UploadedFile::fake()->image('fresk.png'))
            ->call('save')
            ->assertHasNoErrors();

        $producto = Product::where('slug', 'theme_fresk')->firstOrFail();

        $this->assertNotNull($producto->image, 'sin esto, el producto se queda con el icono de reserva para siempre');
        $this->assertStringStartsWith('images/products/', $producto->image,
            'en su propia carpeta: `images_static` es la de los iconos que vienen con la aplicación');
        Storage::disk('public')->assertExists($producto->image);
        $this->assertNotNull($producto->image_url, 'el accessor que las tres vistas ya usaban');
    }

    public function test_se_puede_cambiar_el_icono_y_el_anterior_se_borra(): void
    {
        Storage::fake('public');
        $this->usuario(['admin.products.edit']);

        $producto = $this->producto();

        Livewire::test(EditarProducto::class, ['product' => $producto])
            ->set('image', UploadedFile::fake()->image('uno.png'))
            ->call('update');

        $primero = $producto->fresh()->image;

        Livewire::test(EditarProducto::class, ['product' => $producto->fresh()])
            ->set('image', UploadedFile::fake()->image('dos.png'))
            ->call('update');

        $segundo = $producto->fresh()->image;

        $this->assertNotSame($primero, $segundo);
        Storage::disk('public')->assertMissing($primero);
        Storage::disk('public')->assertExists($segundo);
    }

    public function test_guardar_sin_tocar_el_icono_no_lo_borra(): void
    {
        // **El fallo que se evita.** El campo de fichero llega vacío en cada guardado
        // normal, y `validate()` devuelve la clave con null: un `update` a ciegas dejaría
        // el producto sin icono al cambiar cualquier otra cosa.
        Storage::fake('public');
        $this->usuario(['admin.products.edit']);

        $producto = $this->producto();

        Livewire::test(EditarProducto::class, ['product' => $producto])
            ->set('image', UploadedFile::fake()->image('icono.png'))
            ->call('update');

        $icono = $producto->fresh()->image;
        $this->assertNotNull($icono);

        Livewire::test(EditarProducto::class, ['product' => $producto->fresh()])
            ->set('name', 'Otro nombre')
            ->call('update');

        $this->assertSame($icono, $producto->fresh()->image);
        $this->assertSame('Otro nombre', $producto->fresh()->name);
        Storage::disk('public')->assertExists($icono);
    }

    public function test_se_puede_quitar_el_icono(): void
    {
        // Un campo de fichero vacío significa «no lo cambies», así que sin este botón
        // poner un icono sería irreversible.
        Storage::fake('public');
        $this->usuario(['admin.products.edit']);

        $producto = $this->producto();

        Livewire::test(EditarProducto::class, ['product' => $producto])
            ->set('image', UploadedFile::fake()->image('icono.png'))
            ->call('update');

        $icono = $producto->fresh()->image;

        Livewire::test(EditarProducto::class, ['product' => $producto->fresh()])
            ->call('quitarIcono');

        $this->assertNull($producto->fresh()->image);
        $this->assertNull($producto->fresh()->image_url);
        Storage::disk('public')->assertMissing($icono);
    }

    public function test_un_icono_de_los_que_vienen_con_la_aplicacion_no_se_borra_del_disco(): void
    {
        // `images_static/` son los iconos que trae la aplicación: los comparten varios
        // productos y no son de nadie en particular. Quitarle el icono a un producto no
        // puede llevarse por delante el de los demás.
        Storage::fake('public');
        $this->usuario(['admin.products.edit']);

        Storage::disk('public')->put('images_static/compartido.png', 'contenido');
        $producto = $this->producto(['image' => 'images_static/compartido.png']);

        Livewire::test(EditarProducto::class, ['product' => $producto])->call('quitarIcono');

        // El producto deja de usarlo, pero el fichero se queda: `assertExists` no admite
        // mensaje —su segundo argumento es el contenido esperado—.
        $this->assertNull($producto->fresh()->image);
        Storage::disk('public')->assertExists('images_static/compartido.png');
    }

    public function test_el_svg_no_se_acepta_como_icono(): void
    {
        // MGR-020: un SVG puede llevar código y se serviría desde el propio dominio del
        // panel. Lo vigila además el barrido de `ImageUploadTest`.
        Storage::fake('public');
        $this->usuario(['admin.products.create']);

        Livewire::test(CrearProducto::class)
            ->set('slug', 'theme_fresk')
            ->set('name', 'Fresk')
            ->set('summary', 'El tema de la casa')
            ->set('image', UploadedFile::fake()->create('icono.svg', 10, 'image/svg+xml'))
            ->call('save')
            ->assertHasErrors(['image' => 'mimes']);

        $this->assertSame(0, Product::where('slug', 'theme_fresk')->count());
    }

    /* ===================== El tipo, libre y opcional ===================== */

    public function test_un_producto_que_no_es_un_plugin_se_puede_guardar_sin_tipo(): void
    {
        // El caso literal: `fresk_premium` no es un plugin de Moodle y no tiene tipo.
        $this->usuario(['admin.products.create']);

        Livewire::test(CrearProducto::class)
            ->set('slug', 'fresk_premium')
            ->set('name', 'Fresk Premium')
            ->set('summary', 'El paquete comercial')
            ->call('save')
            ->assertHasNoErrors();

        $producto = Product::where('slug', 'fresk_premium')->firstOrFail();

        $this->assertNull($producto->type, 'vacío es null, no cadena vacía: son el mismo estado');
        $this->assertNull($producto->tech);
    }

    public function test_se_puede_escribir_un_tipo_que_no_estaba_en_la_lista(): void
    {
        $this->usuario(['admin.products.create']);

        Livewire::test(CrearProducto::class)
            ->set('slug', 'ecosistema_tresipunt')
            ->set('name', 'Ecosistema')
            ->set('summary', 'Un agrupamiento comercial')
            ->set('type', 'agrupamiento')
            ->call('save')
            ->assertHasNoErrors();

        $this->assertSame('agrupamiento', Product::where('slug', 'ecosistema_tresipunt')->value('type'));
    }

    public function test_el_tipo_escrito_una_vez_se_ofrece_la_siguiente(): void
    {
        // La «memoria» del campo: es lo que sustituye a la lista cerrada. Sin esto, cada
        // alta tendría que recordar de memoria cómo se escribió la vez anterior, y
        // acabaría habiendo «agrupamiento» y «Agrupamientos».
        $this->producto(['type' => 'agrupamiento']);

        $this->assertContains('agrupamiento', Product::tiposSugeridos());
        $this->assertContains('theme', Product::tiposSugeridos(), 'y los de siempre siguen ahí');
    }

    public function test_el_tipo_de_un_producto_borrado_sigue_en_las_sugerencias(): void
    {
        // Si alguien usó «ecosistema» y ese producto se retiró, la palabra sigue siendo la
        // de la casa.
        $this->producto(['type' => 'ecosistema'])->delete();

        $this->assertContains('ecosistema', Product::tiposSugeridos());
    }

    public function test_editar_un_producto_sin_tipo_no_le_pone_uno(): void
    {
        // **El fallo de la pantalla de edición.** Su `<select>` no tenía opción vacía, así
        // que el primer valor —`theme`— salía seleccionado y guardar cualquier otro cambio
        // le ponía un tipo que nadie había elegido.
        $this->usuario(['admin.products.edit']);

        $producto = $this->producto(['type' => null, 'tech' => null]);

        Livewire::test(EditarProducto::class, ['product' => $producto])
            ->set('name', 'Otro nombre')
            ->call('update')
            ->assertHasNoErrors();

        $this->assertNull($producto->fresh()->type);
        $this->assertSame('Otro nombre', $producto->fresh()->name);
    }
}
