<?php

declare(strict_types=1);

namespace Tests\Feature;

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

/**
 * Tests de seguridad para POST /api/pwa/auth/login.
 *
 * Cubre:
 * (a) Que la búsqueda por teléfono no usa LIKE parcial — un sufijo corto
 *     no puede autenticar al usuario equivocado.
 * (b) Que el rate limiter devuelve 429 tras 5 intentos fallidos para la
 *     misma combinación de IP + identifier.
 */
class PwaLoginSecurityTest extends TestCase
{
    use DatabaseTransactions;

    private const LOGIN_URL = '/api/pwa/auth/login';

    private const PASSWORD = 'secret1234';

    private Company $company;

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

        // Empresa con suscripción activa para que el login no falle por suscripción.
        $plan = Plan::factory()->create();
        $this->company = Company::factory()->create();
        Subscription::factory()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addMonth(),
        ]);

        // Rol collector disponible para asignar.
        Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);
    }

    // ─── Helpers ─────────────────────────────────────────────────────────────

    /**
     * Crea un usuario collector con teléfono normalizado (solo dígitos).
     */
    private function makeCollector(string $phone): User
    {
        $user = User::factory()->create([
            'company_id' => $this->company->id,
            'phone' => $phone,
            'password' => Hash::make(self::PASSWORD),
        ]);
        $user->assignRole('collector');

        return $user;
    }

    /**
     * Limpia el rate limiter para el identifier dado (IP de test = 127.0.0.1).
     * Necesario porque el rate limiter usa caché, no base de datos, y por tanto
     * no lo revierte DatabaseTransactions.
     */
    private function clearRateLimit(string $identifier): void
    {
        $key = sha1('127.0.0.1|'.strtolower($identifier));
        RateLimiter::clear($key);
    }

    // ─── (a) Búsqueda por teléfono: mínimo 7 dígitos + tolerancia de formato ──

    #[Test]
    public function short_phone_suffix_under_7_digits_is_always_rejected(): void
    {
        // Un sufijo de 4 dígitos podría colisionar con múltiples usuarios.
        // El guard de longitud mínima lo bloquea antes de tocar la DB.
        $this->makeCollector('3001234567');

        $response = $this->postJson(self::LOGIN_URL, [
            'identifier' => '4567',          // 4 dígitos — bajo el mínimo
            'password' => self::PASSWORD,
        ]);

        // 422: la búsqueda devuelve null por longitud insuficiente → credenciales incorrectas
        $response->assertStatus(422);
    }

    #[Test]
    public function short_shared_suffix_under_7_digits_does_not_cross_authenticate(): void
    {
        // Dos usuarios con distinto número completo pero mismo sufijo corto.
        $this->makeCollector('3001110001');
        $this->makeCollector('3009990001');

        // "0001" (4 dígitos) → bloqueado por longitud mínima
        $this->postJson(self::LOGIN_URL, [
            'identifier' => '0001',
            'password' => self::PASSWORD,
        ])->assertStatus(422);
    }

    #[Test]
    public function phone_with_country_code_formatting_authenticates_correctly(): void
    {
        // El teléfono se almacena con formato "+57 300 123-4567".
        // REGEXP_REPLACE en DB → "573001234567".
        // El usuario digita "3001234567" (10 dígitos sin código de país).
        // "573001234567" LIKE '%3001234567' → TRUE.
        $this->makeCollector('+57 300 123-4567');

        $response = $this->postJson(self::LOGIN_URL, [
            'identifier' => '3001234567',
            'password' => self::PASSWORD,
            'device_name' => 'test-device',
        ]);

        $response->assertStatus(200)
            ->assertJsonStructure(['token', 'user']);

        $this->clearRateLimit('3001234567');
    }

    #[Test]
    public function phone_stored_as_plain_digits_authenticates_correctly(): void
    {
        // Teléfono almacenado ya normalizado — match exacto.
        $this->makeCollector('3007654321');

        $response = $this->postJson(self::LOGIN_URL, [
            'identifier' => '3007654321',
            'password' => self::PASSWORD,
            'device_name' => 'test-device',
        ]);

        $response->assertStatus(200)
            ->assertJsonStructure(['token', 'user']);

        $this->clearRateLimit('3007654321');
    }

    #[Test]
    public function phone_suffix_collision_authenticates_the_user_whose_password_matches(): void
    {
        // Dos usuarios con el MISMO teléfono en empresas distintas. El sufijo-LIKE
        // hace match con ambos; el login debe resolver al usuario cuya contraseña
        // coincide, no al primero arbitrario que devolvía ->first() (lo que antes
        // podía impedir el acceso legítimo del segundo usuario).
        $this->makeCollector('3001234567'); // user A en $this->company, password = self::PASSWORD

        // Segunda empresa con suscripción activa y un collector con el mismo teléfono.
        $plan = Plan::factory()->create();
        $companyB = Company::factory()->create();
        Subscription::factory()->create([
            'company_id' => $companyB->id,
            'plan_id' => $plan->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addMonth(),
        ]);

        $passwordB = 'different5678';
        $userB = User::factory()->create([
            'company_id' => $companyB->id,
            'phone' => '3001234567',
            'password' => Hash::make($passwordB),
        ]);
        $userB->assignRole('collector');

        // El login con la contraseña de B debe devolver a B, no a A.
        $response = $this->postJson(self::LOGIN_URL, [
            'identifier' => '3001234567',
            'password' => $passwordB,
            'device_name' => 'test-device',
        ]);

        $response->assertStatus(200)
            ->assertJsonPath('user.id', $userB->id);

        $this->clearRateLimit('3001234567');
    }

    // ─── (b) Rate limiting — 429 tras 5 intentos fallidos ────────────────────

    #[Test]
    public function returns_429_after_five_failed_login_attempts(): void
    {
        $identifier = 'brute@example.com';
        $this->clearRateLimit($identifier);

        // 5 intentos fallidos (contraseña incorrecta)
        for ($i = 0; $i < 5; $i++) {
            $this->postJson(self::LOGIN_URL, [
                'identifier' => $identifier,
                'password' => 'wrong-password-'.$i,
            ])->assertStatus(422); // credenciales incorrectas
        }

        // El sexto intento debe ser bloqueado por el rate limiter
        $response = $this->postJson(self::LOGIN_URL, [
            'identifier' => $identifier,
            'password' => 'wrong-again',
        ]);

        $response->assertStatus(429)
            ->assertJson(['retry_after' => 60]);

        // Limpieza para no afectar otros tests
        $this->clearRateLimit($identifier);
    }

    #[Test]
    public function rate_limit_is_per_identifier_not_per_ip(): void
    {
        // Si el rate limit fuera por IP pura, ambos identificadores se bloquearían
        // juntos. Con IP+identifier, cada uno tiene su propio contador.
        $idA = 'user-a@example.com';
        $idB = 'user-b@example.com';
        $this->clearRateLimit($idA);
        $this->clearRateLimit($idB);

        // Agota el límite para identifier A
        for ($i = 0; $i < 5; $i++) {
            $this->postJson(self::LOGIN_URL, [
                'identifier' => $idA,
                'password' => 'wrong',
            ]);
        }
        $this->postJson(self::LOGIN_URL, [
            'identifier' => $idA,
            'password' => 'wrong',
        ])->assertStatus(429);

        // identifier B, desde la misma IP, aún tiene sus propios 5 intentos
        $responseB = $this->postJson(self::LOGIN_URL, [
            'identifier' => $idB,
            'password' => 'wrong',
        ]);
        // 422 = credenciales incorrectas, NO 429 — el límite es independiente
        $responseB->assertStatus(422);

        $this->clearRateLimit($idA);
        $this->clearRateLimit($idB);
    }

    #[Test]
    public function successful_login_is_not_blocked_before_limit_is_reached(): void
    {
        $user = $this->makeCollector('3005556677');
        $this->clearRateLimit('3005556677');

        // 4 intentos fallidos (uno menos del límite)
        for ($i = 0; $i < 4; $i++) {
            $this->postJson(self::LOGIN_URL, [
                'identifier' => '3005556677',
                'password' => 'bad-pass',
            ])->assertStatus(422);
        }

        // El 5.º intento con la contraseña correcta debe funcionar
        $this->postJson(self::LOGIN_URL, [
            'identifier' => '3005556677',
            'password' => self::PASSWORD,
            'device_name' => 'test-device',
        ])->assertStatus(200);

        $this->clearRateLimit('3005556677');
    }
}
