<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\CreateFromClient;
use App\Livewire\Environments\CreateWizard;
use App\Livewire\Environments\Edit;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
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;

/**
 * El valor de `env`, y por qué no es una etiqueta.
 *
 * `environments.domain_unique` es una **columna generada STORED** definida como
 * `CASE WHEN env = 'local' THEN NULL ELSE domain END`, y el índice único del dominio cuelga
 * de ella: los sitios locales están exentos porque varios desarrolladores comparten
 * `moodle.test` (fixes/PRODSECU-192).
 *
 * Eso convierte el valor de `env` en parte del esquema: **`'Local'` no es `'local'` para
 * MySQL**, así que un entorno con el valor mal escrito entra en el índice único y **pierde
 * la exención sin que nada avise**. Dos de las cinco puertas que crean o editan entornos
 * validaban `env` como `required|string` a secas (MGR-010).
 */
class ValorDeEnvTest extends TestCase
{
    use RefreshDatabase;

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

        foreach (['admin.environments.index', 'admin.environments.create', 'admin.environments.edit'] as $nombre) {
            $rol->givePermissionTo(Permission::firstOrCreate(
                ['name' => $nombre, 'guard_name' => 'web'],
                ['model' => 'environments', 'description' => $nombre]
            ));
        }

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

        Cache::flush();

        return $user;
    }

    private function cliente(): Client
    {
        return Client::firstOrCreate(
            ['shortname' => 'env'],
            ['name' => 'Cliente de env', 'actived' => true]
        );
    }

    /**
     * Los valores que hay que rechazar, y por qué cada uno.
     *
     * @return array<string, array{string}>
     */
    public static function valoresQueNoValen(): array
    {
        return [
            'con mayúscula' => ['Local'],
            'todo mayúsculas' => ['LOCAL'],
            'parecido pero otro' => ['localhost'],
            'inventado' => ['staging'],
            'vacío disfrazado' => [' '],
        ];
    }

    /**
     * @dataProvider valoresQueNoValen
     */
    public function test_el_asistente_rechaza_un_env_que_no_esta_en_la_lista(string $malo): void
    {
        // **Una de las dos puertas que estaban abiertas.**
        //
        // Este test apuntaba a `Environments\Create`, que se borró el 2026-09-08 por no
        // tener ruta (MGR-098). El alta viva es el asistente, así que la comprobación se
        // repunta ahí: lo que hay que sujetar es que **ninguna puerta de alta acepte un
        // `env` fuera de la lista**, no que un componente concreto exista.
        $this->admin();
        $cliente = $this->cliente();

        Livewire::test(CreateWizard::class)
            ->call('selectClient', $cliente->id)
            ->set('formData.name', 'Sitio nuevo')
            ->set('formData.domain', 'https://sitio-nuevo.test')
            ->set('formData.version', '5.1.1')
            ->set('formData.env', $malo)
            ->call('saveEnvironment')
            ->assertHasErrors('formData.env');

        $this->assertDatabaseMissing('environments', ['env' => $malo]);
    }

    /**
     * @dataProvider valoresQueNoValen
     */
    public function test_el_alta_desde_un_cliente_tambien_lo_rechaza(string $malo): void
    {
        // La otra puerta de alta: `/environments/clients/{client}/create`. Son dos
        // componentes distintos y los dos tienen que aplicar la misma lista — es
        // exactamente el tipo de cosa que se queda a medias.
        $this->admin();
        $cliente = $this->cliente();

        Livewire::test(CreateFromClient::class, ['client' => $cliente])
            ->set('name', 'Sitio nuevo')
            ->set('domain', 'https://sitio-desde-cliente.test')
            ->set('version', '5.1.1')
            ->set('env', $malo)
            ->call('save')
            ->assertHasErrors('env');

        $this->assertDatabaseMissing('environments', ['env' => $malo]);
    }

    /**
     * @dataProvider valoresQueNoValen
     */
    public function test_editar_rechaza_un_env_que_no_esta_en_la_lista(string $malo): void
    {
        // **La otra.** Y aquí es peor: un entorno que ya existe y estaba bien puede
        // quedarse mal por una edición.
        $this->admin();
        $cliente = $this->cliente();

        $entorno = Environment::create([
            'name' => 'Sitio existente',
            'domain' => 'https://sitio-existente.test',
            'version' => '5.1.1',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ]);

        Livewire::test(Edit::class, ['environment' => $entorno])
            ->set('env', $malo)
            ->call('update')
            ->assertHasErrors('env');

        $this->assertSame('pro', $entorno->fresh()->env, 'el valor bueno no se toca');
    }

    public function test_los_cuatro_valores_buenos_se_aceptan(): void
    {
        $this->admin();
        $cliente = $this->cliente();

        foreach (Environment::entornos() as $bueno) {
            $entorno = Environment::create([
                'name' => 'Sitio ' . $bueno,
                'domain' => 'https://sitio-' . $bueno . '.test',
                'version' => '5.1.1',
                'env' => 'pro',
                'active' => true,
                'client_id' => $cliente->id,
            ]);

            Livewire::test(Edit::class, ['environment' => $entorno])
                ->set('env', $bueno)
                ->call('update')
                ->assertHasNoErrors('env');

            $this->assertSame($bueno, $entorno->fresh()->env);
        }
    }

    public function test_local_sigue_estando_en_la_lista(): void
    {
        // **Parece tonto y no lo es.** La columna generada de la base compara contra la
        // cadena literal `'local'`. Si ese valor desaparece de la lista, los locales dejan
        // de estar exentos del índice único del dominio y dos desarrolladores no pueden
        // compartir `moodle.test` — con un error de clave duplicada que no menciona `env`
        // en ninguna parte.
        //
        // Ahora la lista es un catálogo editable, así que esto vale doble: lo que garantiza
        // que la fila siga ahí es que el catálogo **no la deja borrar ni renombrar**. Ver
        // `CatalogoDeUsosTest`.
        $this->assertContains(Environment::USO_EXENTO_DEL_INDICE, Environment::entornos());
        $this->assertSame('local', Environment::USO_EXENTO_DEL_INDICE);
    }

    public function test_la_regla_es_la_misma_para_todas_las_puertas(): void
    {
        // Las cinco leen `Environment::reglaDeEnv()`. Si alguien vuelve a escribir la lista
        // a mano en una de ellas, esto no lo caza —no puede—, pero sí caza que la regla
        // deje de exigir lo que tiene que exigir.
        $regla = Environment::reglaDeEnv();

        $this->assertStringContainsString('required', $regla);

        foreach (Environment::entornos() as $valor) {
            $this->assertStringContainsString($valor, $regla);
        }
    }
}
