<?php

namespace Tests\Feature\Users;

use App\Livewire\Users\Index as UsersIndex;
use App\Models\Auth\User;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
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;

/**
 * El «último acceso» del listado de Usuarios (MGR-017).
 *
 * **La historia, porque el fallo cambió de forma dos veces.** `User::$fillable` declaraba
 * `last_login_at` sin que la columna existiera, así que la pantalla no tenía de dónde sacar
 * el dato y la celda salía vacía. Se resolvió a medias sacándolo de `sessions` con un
 * `MAX(last_activity)`… y eso contesta **«¿está dentro ahora?»**, no «¿cuándo entró por
 * última vez?»: las filas de `sessions` se borran a los `SESSION_LIFETIME` minutos —120—.
 * Resultado: la pantalla afirmaba **«Nunca ha entrado — alta sin estrenar»** de cualquiera
 * que no tuviera una pestaña abierta en ese momento. De columna vacía a columna que miente.
 *
 * Ahora son dos fuentes y tres estados, y lo que se prueba aquí es justo eso.
 */
class UltimoAccesoTest extends TestCase
{
    use RefreshDatabase;

    private function mirando(): User
    {
        $rol = Role::firstOrCreate(['name' => 'RolDeUsuarios', 'guard_name' => 'web']);
        $rol->givePermissionTo(Permission::firstOrCreate(
            ['name' => 'admin.users.index', 'guard_name' => 'web'],
            ['model' => 'users', 'description' => 'admin.users.index']
        ));

        $user = User::factory()->create(['name' => 'Quien mira']);
        $user->assignRole($rol);
        $this->actingAs($user);

        Cache::flush();

        return $user;
    }

    /** La fila de `sessions` que tendría alguien con el navegador abierto. */
    private function sesionViva(User $user, Carbon $cuando): void
    {
        DB::table('sessions')->insert([
            'id' => 'sesion-' . $user->id,
            'user_id' => $user->id,
            'ip_address' => '127.0.0.1',
            'user_agent' => 'tests',
            'payload' => '',
            'last_activity' => $cuando->getTimestamp(),
        ]);
    }

    /** @return array<string, mixed>|null */
    private function filaDe(string $nombre): ?array
    {
        $personas = Livewire::test(UsersIndex::class)->viewData('personas');

        return collect($personas)->firstWhere('nombre', $nombre);
    }

    public function test_entrar_deja_la_fecha_guardada(): void
    {
        // El listener del evento `Login`, que es lo que no existía: nadie apuntaba el
        // acceso en ningún sitio.
        $user = User::factory()->create(['last_login_at' => null]);

        event(new Login('web', $user, false));

        $this->assertNotNull($user->fresh()->last_login_at,
            'entrar tiene que dejar rastro: sin esto el dato no existe en ninguna parte');
    }

    public function test_entrar_no_cuenta_como_modificar_la_cuenta(): void
    {
        // `withoutTimestamps`: si el login moviera `updated_at`, la columna «Modificado»
        // diría que a todo el mundo le han cambiado algo cada mañana.
        $user = User::factory()->create();
        $antes = $user->updated_at;

        $this->travel(2)->hours();
        event(new Login('web', $user, false));

        $this->assertEquals(
            $antes->timestamp,
            $user->fresh()->updated_at->timestamp,
            'entrar no es editar la cuenta'
        );
    }

    public function test_el_acceso_sobrevive_a_que_se_cierre_la_sesion(): void
    {
        // **El fallo que se arregla.** Sin fila en `sessions` —caducó, se hizo logout— el
        // dato tenía que seguir estando: es lo que distingue «entró ayer» de «nunca».
        $this->mirando();

        User::factory()->create([
            'name' => 'Entro ayer',
            'last_login_at' => now()->subDay(),
        ]);

        $fila = $this->filaDe('Entro ayer');

        $this->assertFalse($fila['nunca'], 'entró ayer: no puede salir como «nunca ha entrado»');
        $this->assertFalse($fila['sinRegistro']);
        $this->assertNotNull($fila['actividad']);
    }

    public function test_la_sesion_viva_manda_sobre_la_fecha_del_login(): void
    {
        // Quien lleva ocho horas trabajando sin volver a hacer login tiene el acceso
        // «hace un minuto», que es la verdad, y no «esta mañana».
        $this->mirando();

        $trabajando = User::factory()->create([
            'name' => 'Lleva el dia dentro',
            'last_login_at' => now()->subHours(8),
        ]);

        $this->sesionViva($trabajando, now()->subMinute());

        $acceso = User::query()
            ->addSelect(['last_activity_at' => DB::table('sessions')
                ->selectRaw('MAX(last_activity)')
                ->whereColumn('sessions.user_id', 'users.id')])
            ->find($trabajando->id)
            ->last_access_at;

        $this->assertTrue($acceso->greaterThan(now()->subHour()),
            'se usa la más reciente de las dos fuentes');
    }

    public function test_sin_el_addselect_el_dato_sigue_saliendo_de_la_columna(): void
    {
        // Con las sesiones en Redis o en cookie, `last_activity_at` no existe. Antes eso
        // dejaba el accessor a null; ahora la columna responde igual.
        $user = User::factory()->create(['last_login_at' => now()->subDays(3)]);

        $this->assertNotNull($user->last_access_at);
    }

    public function test_una_cuenta_nueva_sin_acceso_es_un_alta_sin_estrenar(): void
    {
        $this->mirando();

        User::factory()->create([
            'name' => 'Recien creado',
            'created_at' => now(),
            'last_login_at' => null,
        ]);

        $fila = $this->filaDe('Recien creado');

        $this->assertTrue($fila['nunca'], 'creado después de que se guarde el dato: sí es un alta sin estrenar');
        $this->assertFalse($fila['sinRegistro']);
    }

    public function test_una_cuenta_antigua_sin_acceso_es_sin_registro_y_no_nunca(): void
    {
        // **El tercer estado.** «No lo sabemos» y «no ha entrado» se pintaban igual, y a
        // quien entra todos los días se le decía «alta sin estrenar»: afirmar lo que no se
        // sabe, que es la lección de MGR-076.
        $this->mirando();

        User::factory()->create([
            'name' => 'De antes del registro',
            'created_at' => Carbon::parse(User::REGISTRO_DE_ACCESOS_DESDE)->subMonths(6),
            'last_login_at' => null,
        ]);

        $fila = $this->filaDe('De antes del registro');

        $this->assertTrue($fila['sinRegistro']);
        $this->assertFalse($fila['nunca'], 'no se puede afirmar que no ha entrado nunca');
    }

    public function test_la_pantalla_dice_sin_registro_en_lugar_de_nunca(): void
    {
        // Que el estado llegue a la celda, no solo al array.
        $this->mirando();

        User::factory()->create([
            'name' => 'De antes del registro',
            'created_at' => Carbon::parse(User::REGISTRO_DE_ACCESOS_DESDE)->subYear(),
            'last_login_at' => null,
        ]);

        Livewire::test(UsersIndex::class)
            ->assertSee('Sin registro')
            ->assertSee('no se guardaba antes');
    }

    public function test_un_acceso_de_hace_meses_sigue_marcandose_como_dudoso(): void
    {
        // Lo que ya funcionaba y no se puede romper: el ámbar de «¿sigue en el equipo?».
        $this->mirando();

        User::factory()->create([
            'name' => 'Se fue hace meses',
            'last_login_at' => now()->subMonths(4),
        ]);

        $fila = $this->filaDe('Se fue hace meses');

        $this->assertTrue($fila['viejo']);
        $this->assertFalse($fila['nunca']);
        $this->assertFalse($fila['sinRegistro']);
    }

    public function test_la_columna_no_se_puede_escribir_desde_un_formulario(): void
    {
        // Fuera de `$fillable` a propósito: la escribe solo el listener.
        $user = User::factory()->create(['last_login_at' => null]);

        $user->fill(['last_login_at' => now()]);

        $this->assertNull($user->last_login_at,
            'un `fill()` con datos de un formulario no puede tocar el último acceso');
    }
}
