<?php

declare(strict_types=1);

namespace Tests\Feature\Auth;

use App\Models\Company;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Un token vivo por dispositivo, no uno por inicio de sesion.
 *
 * La PWA casi nunca cierra sesion, asi que cada login dejaba vivo el token
 * anterior durante sus 30 dias. En produccion un solo cobrador acumulo 495
 * credenciales validas simultaneas: al perder un telefono no habia forma
 * practica de saber cual revocar.
 */
class TokenPorDispositivoTest extends TestCase
{
    use DatabaseTransactions;

    private User $user;

    protected function setUp(): void
    {
        parent::setUp();

        Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);

        $company = Company::factory()->create();

        Subscription::factory()->create([
            'company_id' => $company->id,
            'plan_id' => Plan::factory()->create()->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addYear(),
        ]);

        $this->user = User::factory()->create([
            'company_id' => $company->id,
            'password' => bcrypt('clave-de-prueba'),
        ]);
        $this->user->assignRole('collector');
    }

    private function login(string $device, ?string $deviceId = null): string
    {
        $response = $this->postJson('/api/pwa/auth/login', array_filter([
            'identifier' => $this->user->email,
            'password' => 'clave-de-prueba',
            'device_name' => $device,
            'device_id' => $deviceId,
        ]));

        $response->assertOk();

        return $response->json('token');
    }

    #[Test]
    public function entrar_de_nuevo_desde_el_mismo_dispositivo_no_acumula_tokens(): void
    {
        $this->login('PWA-telefono-de-alex');
        $this->login('PWA-telefono-de-alex');
        $this->login('PWA-telefono-de-alex');

        $this->assertSame(
            1,
            $this->user->tokens()->where('name', 'PWA-telefono-de-alex')->count(),
            'cada inicio de sesion dejaba vivo el token anterior'
        );
    }

    #[Test]
    public function el_token_viejo_del_mismo_dispositivo_deja_de_servir(): void
    {
        $viejo = $this->login('PWA-telefono-de-alex');
        $this->login('PWA-telefono-de-alex');

        $this->withHeader('Authorization', "Bearer {$viejo}")
            ->getJson('/api/pwa/dashboard')
            ->assertUnauthorized();
    }

    #[Test]
    public function cada_dispositivo_conserva_su_propio_token(): void
    {
        // Un cobrador con telefono y tablet no debe perder uno al usar el otro.
        $telefono = $this->login('PWA-telefono');
        $tablet = $this->login('PWA-tablet');

        $this->assertSame(2, $this->user->tokens()->count());

        $this->withHeader('Authorization', "Bearer {$telefono}")
            ->getJson('/api/pwa/dashboard')
            ->assertOk();

        $this->withHeader('Authorization', "Bearer {$tablet}")
            ->getJson('/api/pwa/dashboard')
            ->assertOk();
    }

    #[Test]
    public function dos_equipos_identicos_no_se_expulsan_entre_si(): void
    {
        // El caso real: en produccion el cobrador y el dueno tenian el MISMO
        // device_name — `PWA-Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac O` —
        // porque el userAgent recortado a 50 caracteres no distingue dos iPhone
        // con la misma version de iOS. Con la revocacion por nombre, entrar desde
        // el segundo equipo cerraba la sesion del primero.
        $mismoNombre = 'PWA-Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac O';

        $telefono = $this->login($mismoNombre, 'pwa-aaaaaaaa-1111');
        $tablet = $this->login($mismoNombre, 'pwa-bbbbbbbb-2222');

        $this->assertSame(2, $this->user->tokens()->count());

        $this->withHeader('Authorization', "Bearer {$telefono}")
            ->getJson('/api/pwa/dashboard')
            ->assertOk();

        $this->withHeader('Authorization', "Bearer {$tablet}")
            ->getJson('/api/pwa/dashboard')
            ->assertOk();
    }

    #[Test]
    public function el_mismo_device_id_sigue_dejando_un_solo_token(): void
    {
        $viejo = $this->login('PWA-iPhone', 'pwa-aaaaaaaa-1111');
        $this->login('PWA-iPhone', 'pwa-aaaaaaaa-1111');

        $this->assertSame(1, $this->user->tokens()->count());

        $this->withHeader('Authorization', "Bearer {$viejo}")
            ->getJson('/api/pwa/dashboard')
            ->assertUnauthorized();
    }

    #[Test]
    public function el_token_se_registra_con_el_device_id(): void
    {
        $this->login('PWA-Mozilla/5.0 (iPhone...)', 'pwa-1234-5678');

        $this->assertSame('pwa-1234-5678', $this->user->tokens()->firstOrFail()->name);
    }

    #[Test]
    public function los_tokens_del_esquema_viejo_se_descartan_al_entrar(): void
    {
        // Nombrados con el userAgent: ambiguos entre dispositivos, imposibles de
        // revocar con precision. Se limpian en el primer login tras el cambio.
        $this->user->createToken('PWA-Mozilla/5.0 (iPhone; CPU iPhone OS 18_7', ['pwa:collector'], now()->addDays(30));
        $this->user->createToken('PWA-Mozilla/5.0 (Linux; Android 14)', ['pwa:collector'], now()->addDays(30));

        $this->assertSame(2, $this->user->tokens()->count());

        $this->login('PWA-iPhone', 'pwa-nuevo-1111');

        $this->assertSame(1, $this->user->tokens()->count());
        $this->assertSame('pwa-nuevo-1111', $this->user->tokens()->firstOrFail()->name);
    }

    #[Test]
    public function el_token_nace_con_caducidad(): void
    {
        // Sin fecha de caducidad el token vive para siempre: 346 filas en
        // produccion no la tenian.
        $this->login('PWA-telefono');

        $token = $this->user->tokens()->firstOrFail();

        $this->assertNotNull($token->expires_at);
        $this->assertTrue($token->expires_at->isFuture());
        $this->assertLessThanOrEqual(30, (int) now()->diffInDays($token->expires_at, absolute: true));
    }
}
