<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Index as EnvironmentsIndex;
use App\Livewire\Environments\RevisionModal;
use App\Models\Auth\User;
use App\Models\Clients\Client;
use App\Models\Environments\Environment;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Support\RevisionDeEntorno;
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;

/**
 * La observación de un entorno: qué hay pendiente de revisar, quién lo vio y con qué nivel.
 *
 * **Es lo único del parque que escribe una persona.** Todo lo demás que sabe el panel de un
 * entorno lo declara su propio Moodle al sincronizar; esto lo anota alguien que ha visto que
 * algo está mal. Antes no tenía sitio: se quedaba en la cabeza de quien lo vio o en un chat.
 *
 * Lo que se fija aquí, por orden de lo que costaría descubrir tarde:
 *
 * 1. Que **Soporte pueda anotar**. Es quien detecta estas cosas y **no tiene**
 *    `admin.environments.edit`: colgar la función de ese permiso la habría dejado sin la
 *    gente que la necesita.
 * 2. Que **editar una observación no la rejuvenezca**. La antigüedad es con lo que se decide
 *    qué se atiende, y si se refrescara al añadir una coma dejaría de servir.
 * 3. Que **resolver no borre el rastro**: la ficha se limpia —si no, el listado seguiría
 *    diciendo «pendiente»— pero el texto queda en Acciones del panel.
 */
class ObservacionDelEntornoTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(array $permisos): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeRevision', 'guard_name' => 'web']);

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

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

        Cache::flush();

        return $usuario;
    }

    private function entorno(array $atributos = []): Environment
    {
        $cliente = Client::firstOrCreate(['shortname' => 'rev'], ['name' => 'Cliente', 'actived' => true]);

        return Environment::create(array_merge([
            'name' => 'Campus del cliente',
            'version' => '5.1.1',
            'domain' => 'https://campus-' . (Environment::withTrashed()->count() + 1) . '.test',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ], $atributos));
    }

    /* ===================== Quién puede anotar ===================== */

    public function test_soporte_puede_anotar_aunque_no_pueda_configurar_el_entorno(): void
    {
        // **El motivo de que esto tenga permiso propio.** Soporte tiene `index` y `refresh`,
        // no `edit`, y es justo quien habla con el cliente y ve que algo está mal. Con la
        // función colgada de `edit`, la gente que detecta los problemas no podía escribirlos.
        $usuario = $this->usuario(['admin.environments.index', 'admin.environments.review']);
        $entorno = $this->entorno();

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])
            ->set('nota', 'El plugin del SEPE está en 1.8 y el resto del parque en 2.1.')
            ->set('nivel', RevisionDeEntorno::IMPORTANTE)
            ->call('guardar');

        $entorno->refresh();

        $this->assertTrue($entorno->estaPendienteDeRevision());
        $this->assertSame(RevisionDeEntorno::IMPORTANTE, $entorno->review_level);
        $this->assertSame($usuario->id, $entorno->review_flagged_by);
    }

    public function test_sin_el_permiso_no_se_abre_ni_llamando_al_metodo(): void
    {
        // MGR-005: esconder el botón no es protegerlo. El método va a /livewire/update,
        // donde el gate de la ruta no se reaplica.
        $this->usuario(['admin.environments.index']);
        $entorno = $this->entorno();

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])->assertForbidden();

        $this->assertFalse($entorno->fresh()->estaPendienteDeRevision());
    }

    /* ===================== Lo que se escribe ===================== */

    public function test_una_nota_de_dos_palabras_no_vale(): void
    {
        // «revisar» no dice nada dentro de tres semanas, y quien lo lea tendrá que preguntar
        // a quien lo escribió — que es exactamente lo que esto viene a evitar.
        $this->usuario(['admin.environments.review']);
        $entorno = $this->entorno();

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])
            ->set('nota', 'revisar')
            ->call('guardar')
            ->assertHasErrors('nota');

        $this->assertFalse($entorno->fresh()->estaPendienteDeRevision());
    }

    public function test_un_nivel_inventado_no_entra(): void
    {
        // `nivel` es una propiedad pública: llega del navegador y hay que validarla (MGR-018).
        $this->usuario(['admin.environments.review']);
        $entorno = $this->entorno();

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])
            ->set('nota', 'Algo que mirar con calma en este entorno.')
            ->set('nivel', 'apocaliptico')
            ->call('guardar')
            ->assertHasErrors('nivel');
    }

    public function test_editar_la_nota_no_rejuvenece_la_observacion(): void
    {
        // **La antigüedad es el dato con el que se decide qué se atiende.** Si se refrescara
        // al añadir una coma, un pendiente de hace tres semanas parecería de hoy cada vez que
        // alguien lo toca, y la lista dejaría de poder ordenarse por lo que lleva esperando.
        $autor = $this->usuario(['admin.environments.review']);
        $entorno = $this->entorno();

        $entorno->marcarParaRevisar('Lo primero que se anotó de este entorno.', RevisionDeEntorno::AVISO, $autor->id);
        $entorno->update(['review_flagged_at' => now()->subDays(20)]);
        $cuando = $entorno->fresh()->review_flagged_at;

        // Otra persona del equipo edita el texto y sube el nivel.
        $otro = User::factory()->create();
        $otro->assignRole(Role::where('name', 'RolDeRevision')->first());
        $this->actingAs($otro);

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])
            ->set('nota', 'Sigue igual y ya afecta al cliente: hay que atenderlo.')
            ->set('nivel', RevisionDeEntorno::URGENTE)
            ->call('guardar');

        $entorno->refresh();

        $this->assertSame(RevisionDeEntorno::URGENTE, $entorno->review_level, 'el nivel sí cambia');
        $this->assertEquals($cuando, $entorno->review_flagged_at, 'la fecha no se toca');
        $this->assertSame($autor->id, $entorno->review_flagged_by, 'y quien la vio primero, tampoco');
    }

    /* ===================== Resolver ===================== */

    public function test_resolver_limpia_la_ficha_pero_deja_el_texto_en_el_log(): void
    {
        // Las dos mitades. Si la nota se quedara, el listado seguiría diciendo «pendiente»
        // en un sitio que ya no lo está; si no quedara rastro, nadie podría saber qué pasó.
        $usuario = $this->usuario(['admin.environments.review']);
        $entorno = $this->entorno();
        $entorno->marcarParaRevisar('El dominio va a cambiar en octubre.', RevisionDeEntorno::AVISO, $usuario->id);

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])->call('resolver');

        $entorno->refresh();

        $this->assertFalse($entorno->estaPendienteDeRevision());
        $this->assertNull($entorno->review_note);
        $this->assertNull($entorno->review_level);

        $apunte = MonitoringLog::where('code', '16044')->latest('id')->first();

        $this->assertNotNull($apunte, 'el borrado deja apunte');
        $this->assertStringContainsString('El dominio va a cambiar en octubre', $apunte->msg);
        $this->assertSame($usuario->id, $apunte->user_id);
    }

    public function test_marcar_tambien_queda_apuntado_con_su_nivel(): void
    {
        $this->usuario(['admin.environments.review']);
        $entorno = $this->entorno();

        Livewire::test(RevisionModal::class, ['environmentId' => $entorno->id])
            ->set('nota', 'Está llamando con un token de otra licencia.')
            ->set('nivel', RevisionDeEntorno::URGENTE)
            ->call('guardar');

        $apunte = MonitoringLog::where('code', '16043')->latest('id')->first();

        $this->assertNotNull($apunte);
        $this->assertStringContainsString('Urgente', $apunte->msg);
        $this->assertStringContainsString('token de otra licencia', $apunte->msg);
        // En rojo en el Dashboard cuando es urgente, y en ámbar el resto.
        $this->assertSame('danger', $apunte->level);
    }

    /* ===================== Dónde se ve ===================== */

    public function test_la_ficha_del_entorno_ensena_la_nota_entera(): void
    {
        // **Es la pantalla donde se viene a leerla.** En el listado solo sale la etiqueta
        // —una nota de tres líneas en una fila empuja al resto y truncada no se entiende—;
        // aquí va completa, con quién la anotó y cuándo, que es lo que permite cerrarla.
        $usuario = $this->usuario(['admin.environments.index', 'admin.environments.review']);
        $entorno = $this->entorno();

        $entorno->marcarParaRevisar(
            'No está sincronizando y no podemos entrar a revisar el plugin local_tresipunt.',
            RevisionDeEntorno::URGENTE,
            $usuario->id
        );

        Livewire::test(\App\Livewire\Environments\Show::class, ['environment' => $entorno])
            ->assertSee('URGENTE')
            ->assertSee('Pendiente de revisar')
            ->assertSee('no podemos entrar a revisar el plugin')
            ->assertSee($usuario->name);
    }

    public function test_el_listado_solo_ensena_la_etiqueta_y_no_el_texto(): void
    {
        // La otra mitad, y la que se rompió primero: la nota entera en una fila hacía que
        // ese entorno pareciera otra cosa. En la lista basta con saber **que hay algo y con
        // qué urgencia**; el qué está a un clic, en la modal.
        $usuario = $this->usuario(['admin.environments.index', 'admin.environments.review']);
        $entorno = $this->entorno();

        $entorno->marcarParaRevisar(
            'No está sincronizando y no podemos entrar a revisar el plugin local_tresipunt.',
            RevisionDeEntorno::URGENTE,
            $usuario->id
        );

        Livewire::test(EnvironmentsIndex::class)
            ->assertSee('Urgente')
            ->assertDontSee('no podemos entrar a revisar el plugin');
    }

    /* ===================== El listado ===================== */

    public function test_el_listado_filtra_por_pendientes_y_pone_lo_urgente_primero(): void
    {
        // El nivel existe para ordenar una lista de trabajo. Ordenado en SQL porque la
        // lista pagina: ordenar después de paginar ordena la página, no la lista.
        $usuario = $this->usuario(['admin.environments.index', 'admin.environments.review']);

        $tranquilo = $this->entorno(['name' => 'Sitio tranquilo']);
        $aviso = $this->entorno(['name' => 'Sitio con aviso']);
        $urgente = $this->entorno(['name' => 'Sitio urgente']);

        $aviso->marcarParaRevisar('Algo que mirar con calma.', RevisionDeEntorno::AVISO, $usuario->id);
        $urgente->marcarParaRevisar('El cliente no recibe nada.', RevisionDeEntorno::URGENTE, $usuario->id);

        $nombres = Livewire::test(EnvironmentsIndex::class)
            ->set('onlyPendingReview', true)
            ->viewData('environments')
            ->pluck('name')
            ->all();

        $this->assertSame(['Sitio urgente', 'Sitio con aviso'], $nombres);
        $this->assertNotContains($tranquilo->name, $nombres);
    }

    public function test_el_filtro_no_mete_un_orden_en_las_consultas_agregadas(): void
    {
        // **El fallo que esto fija, y que solo se ve en producción.** El orden por nivel
        // estaba dentro del constructor de filtros, y ese constructor lo reutilizan las
        // facetas: `select type_id, count(*) … group by type_id`. Un `ORDER BY` por una
        // columna que no está en el `GROUP BY` revienta en MySQL con `only_full_group_by`:
        //
        //   SQLSTATE[42000]: 1055 Expression #1 of ORDER BY clause is not in GROUP BY
        //   clause … 'environments.review_level'
        //
        // En sqlite —que es donde corre esta suite— pasa inadvertido, así que no vale con
        // ejecutar la consulta: hay que mirar el SQL. Filtrar y ordenar son dos cosas
        // distintas y solo una de las dos vale para un agregado.
        $this->usuario(['admin.environments.index', 'admin.environments.review']);

        \Illuminate\Support\Facades\DB::enableQueryLog();

        $componente = Livewire::test(EnvironmentsIndex::class)->set('onlyPendingReview', true);

        \Illuminate\Support\Facades\DB::flushQueryLog();

        // `paraOfrecer()` es privado: se llega por la faceta pública que lo usa, que es
        // además la consulta que reventaba de verdad.
        $componente->instance()->tiposDisponibles();

        $sql = \Illuminate\Support\Facades\DB::getQueryLog();
        $agregada = collect($sql)->first(fn (array $q) => str_contains(strtolower($q['query']), 'group by'));

        $this->assertNotNull($agregada, 'la faceta hace su group by');
        $sql = $agregada['query'];

        $this->assertStringNotContainsString('order by', strtolower($sql));
        // Y el filtro sí está: lo que sobra es el orden, no la condición.
        $this->assertStringContainsString('review_flagged_at', $sql);
    }

    public function test_un_entorno_apagado_con_observacion_sigue_contando(): void
    {
        // **Al revés que el resto de recuentos**, y es a propósito: una observación puede ser
        // justamente «hay que apagar esto» o «revisar antes de darlo de baja», y esconderla
        // al apagar el sitio la perdería justo cuando hace falta.
        $usuario = $this->usuario(['admin.environments.index', 'admin.environments.review']);

        $apagado = $this->entorno(['name' => 'Sitio apagado', 'active' => false]);
        $apagado->marcarParaRevisar('Dado de baja pero el cliente quiere recuperarlo.', RevisionDeEntorno::AVISO, $usuario->id);

        Cache::forget('environments:resumen');

        $this->assertSame(1, Livewire::test(EnvironmentsIndex::class)->viewData('resumen')['porRevisar']);
    }
}
