<?php

namespace Tests\Feature\Environments;

use App\Livewire\Environments\Index as EnvironmentsIndex;
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 Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * En qué orden sale el listado de entornos.
 *
 * Tres órdenes y no una flecha por cabecera: de las siete columnas solo tres responden a
 * una pregunta que alguien se hace —qué se ha movido, qué hemos tocado nosotros y dónde
 * está tal sitio—.
 *
 * Lo que se fija aquí:
 *
 * 1. **Que «última edición» no sea «sincronización» con otro nombre.** Es la trampa de
 *    esta pantalla: `updated_at` parece el campo obvio y no lo es, porque `SyncAction`
 *    escribe en la fila del entorno en cada sincronización. Ordenar por él daría casi la
 *    misma lista que el otro botón. El dato bueno es el registro de acciones del panel.
 * 2. **Que los entornos sin fecha vayan al final.** Un `NULL` no es «lo más reciente»: es
 *    que no hay dato, y cada motor lo coloca donde le parece según la dirección.
 * 3. **Que el orden sea total.** Sin desempate, dos entornos con la misma fecha pueden
 *    intercambiarse entre una página y la siguiente, y en una lista paginada eso significa
 *    que uno sale dos veces y otro ninguna.
 */
class OrdenDelListadoTest extends TestCase
{
    use RefreshDatabase;

    private function usuario(): void
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeOrden', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.environments.index', 'guard_name' => 'web'],
            ['model' => 'environments', 'description' => 'ver']
        ));

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

        Cache::flush();
    }

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

        return Environment::create(array_merge([
            'name' => $nombre,
            'version' => '5.1.1',
            'domain' => 'https://' . str()->slug($nombre) . '.test',
            'env' => 'pro',
            'active' => true,
            'client_id' => $cliente->id,
        ], $atributos));
    }

    /** Una edición anotada en el registro del panel, que es de donde sale el orden. */
    private function anotarEdicion(Environment $entorno, string $cuando): void
    {
        DB::table('logs')->insert([
            'level' => 'info',
            'code' => '16005',
            'entity' => 'Environment',
            'entity_id' => (string) $entorno->id,
            'msg' => 'Entorno editado',
            'created_at' => $cuando,
            'updated_at' => $cuando,
        ]);
    }

    /** @return list<string> */
    private function nombres(array $parametros = []): array
    {
        $componente = Livewire::test(EnvironmentsIndex::class);

        foreach ($parametros as $propiedad => $valor) {
            $componente->set($propiedad, $valor);
        }

        return $componente->viewData('environments')->pluck('name')->all();
    }

    public function test_por_defecto_manda_la_fecha_de_sincronizacion(): void
    {
        $this->usuario();

        $this->entorno('Antiguo', ['refresh_at' => now()->subDays(10)]);
        $this->entorno('Reciente', ['refresh_at' => now()->subHour()]);
        $this->entorno('Nunca ha hablado', ['refresh_at' => null]);

        // Lo más reciente arriba, y el que no ha sincronizado nunca **al final**: es un
        // alta sin terminar, no actividad de hace un momento.
        $this->assertSame(['Reciente', 'Antiguo', 'Nunca ha hablado'], $this->nombres());
    }

    public function test_la_ultima_edicion_no_es_lo_mismo_que_la_sincronizacion(): void
    {
        // **El test que justifica que el orden salga del registro del panel.** Se monta a
        // propósito el caso en que los dos criterios dan listas opuestas: el que sincronizó
        // hace un momento es el que se editó hace más tiempo. Si «última edición» leyera
        // `updated_at` —que `SyncAction` mueve en cada sincronización— las dos listas
        // saldrían iguales y este test caería.
        $this->usuario();

        $recienSincronizado = $this->entorno('Sincroniza a diario', ['refresh_at' => now()->subMinutes(5)]);
        $recienEditado = $this->entorno('Lo tocamos hoy', ['refresh_at' => now()->subDays(30)]);

        $this->anotarEdicion($recienSincronizado, now()->subMonths(6)->toDateTimeString());
        $this->anotarEdicion($recienEditado, now()->subHour()->toDateTimeString());

        $this->assertSame(['Sincroniza a diario', 'Lo tocamos hoy'], $this->nombres());

        $this->assertSame(['Lo tocamos hoy', 'Sincroniza a diario'], $this->nombres(['orden' => 'editado']));
    }

    public function test_un_entorno_que_nadie_ha_editado_va_al_final(): void
    {
        $this->usuario();

        $editado = $this->entorno('Con historial');
        $this->entorno('Sin historial');

        $this->anotarEdicion($editado, now()->subYear()->toDateTimeString());

        // Aunque la edición sea de hace un año: «no consta» va después de «consta», porque
        // el registro del panel se purga por fecha y un hueco no es una fecha antigua.
        $this->assertSame(['Con historial', 'Sin historial'], $this->nombres(['orden' => 'editado']));
    }

    public function test_por_nombre_ordena_alfabeticamente(): void
    {
        $this->usuario();

        $this->entorno('Zaragoza', ['refresh_at' => now()]);
        $this->entorno('Albacete', ['refresh_at' => now()->subYear()]);

        $this->assertSame(['Albacete', 'Zaragoza'], $this->nombres(['orden' => 'nombre']));
    }

    public function test_el_boton_cambia_el_orden_de_verdad(): void
    {
        // **Los tests de arriba fijan la consulta; este fija el botón.** Entre «la lista
        // sabe ordenarse» y «el botón la ordena» hay un hueco —el `wire:click`— donde cabe
        // una pantalla que no hace nada al pulsarla.
        $this->usuario();

        $this->entorno('Zaragoza', ['refresh_at' => now()]);
        $this->entorno('Albacete', ['refresh_at' => now()->subYear()]);

        $componente = Livewire::test(EnvironmentsIndex::class);

        $this->assertSame(['Zaragoza', 'Albacete'], $componente->viewData('environments')->pluck('name')->all());

        $componente->call('ordenarPor', 'nombre');

        $this->assertSame('nombre', $componente->get('orden'));
        $this->assertSame(['Albacete', 'Zaragoza'], $componente->viewData('environments')->pluck('name')->all());

        // Y los tres botones que pinta la vista son exactamente los que acepta la acción.
        $componente->assertSee('Ordenar por');

        foreach (EnvironmentsIndex::ORDENES as $etiqueta) {
            $componente->assertSee($etiqueta);
        }
    }

    public function test_un_orden_que_no_existe_cae_en_el_de_por_defecto(): void
    {
        // `orden` viaja en la URL, así que un enlace retocado a mano puede traer cualquier
        // cosa. Sin esto la consulta se quedaría **sin `ORDER BY`**, y en una lista
        // paginada eso significa que un entorno puede salir en dos páginas y otro en
        // ninguna: el motor no garantiza el mismo recorrido entre dos `LIMIT/OFFSET`.
        $this->usuario();

        $this->entorno('Antiguo', ['refresh_at' => now()->subDays(10)]);
        $this->entorno('Reciente', ['refresh_at' => now()->subHour()]);

        $componente = Livewire::test(EnvironmentsIndex::class)->set('orden', 'lo-que-sea');

        $this->assertSame('sincro', $componente->instance()->ordenValido());
        $this->assertSame(['Reciente', 'Antiguo'], $componente->viewData('environments')->pluck('name')->all());
    }

    public function test_con_la_lista_de_pendientes_manda_el_nivel_y_el_orden_desempata(): void
    {
        // El único filtro de la pantalla que ordena por gravedad. Si el orden elegido se lo
        // comiera, dejaría de servir para lo que existe: atender primero lo urgente.
        $this->usuario();

        $urgente = $this->entorno('Urgente pero viejo', ['refresh_at' => now()->subYear()]);
        $aviso = $this->entorno('Aviso recién sincronizado', ['refresh_at' => now()]);

        $urgente->update(['review_level' => 'urgente', 'review_note' => 'x', 'review_flagged_at' => now()]);
        $aviso->update(['review_level' => 'aviso', 'review_note' => 'y', 'review_flagged_at' => now()]);

        $nombres = $this->nombres(['onlyPendingReview' => true]);

        $this->assertSame('Urgente pero viejo', $nombres[0],
            'el nivel manda sobre el orden elegido cuando se miran los pendientes');
    }
}
