import { randomUUID } from "node:crypto";
import { describe, expect, it } from "vitest";
import { db } from "@/server/db";
import { AUTH_CONFIG } from "@/server/auth/config";
import { login, logout } from "@/server/auth/login-service";
import { changeOwnPassword } from "@/server/auth/password-service";
import { validateSessionToken } from "@/server/auth/session-service";
import { TEST_PASSWORD, createTestUser } from "./fixtures";

/** IP aleatoria por intento, para que el límite por IP no interfiera entre pruebas. */
const octet = () => Math.floor(Math.random() * 254) + 1;
const uniqueIp = () => `10.${octet()}.${octet()}.${octet()}`;

async function attempt(email: string, password: string, ip = uniqueIp(), now = new Date()) {
  return login({ email, password, ip, userAgent: "vitest" }, now);
}

describe("login", () => {
  it("éxito: crea sesión, limpia contadores, registra auditoría e intento", async () => {
    const user = await createTestUser("ADMIN");
    await db.user.update({ where: { id: user.id }, data: { failedLoginCount: 3 } });

    const result = await attempt(user.email, TEST_PASSWORD);
    expect(result.ok).toBe(true);
    if (!result.ok) return;

    const session = await validateSessionToken(result.token);
    expect(session?.user.id).toBe(user.id);

    const updated = await db.user.findUniqueOrThrow({ where: { id: user.id } });
    expect(updated.failedLoginCount).toBe(0);
    expect(updated.lastLoginAt).not.toBeNull();

    const audit = await db.auditLog.findFirst({ where: { userId: user.id, action: "auth.login" } });
    expect(audit).not.toBeNull();
    expect(JSON.stringify(audit)).not.toContain(TEST_PASSWORD);
    expect(JSON.stringify(audit)).not.toContain(result.token);
  });

  it("contraseña incorrecta: mensaje genérico y contador", async () => {
    const user = await createTestUser("ADVISOR");
    const result = await attempt(user.email, "incorrecta-123");
    expect(result).toEqual({ ok: false, code: "INVALID_CREDENTIALS" });
    const updated = await db.user.findUniqueOrThrow({ where: { id: user.id } });
    expect(updated.failedLoginCount).toBe(1);
  });

  it("email inexistente responde igual que contraseña incorrecta (no revela usuarios)", async () => {
    const result = await attempt(`nadie-${randomUUID()}@test.chesco.local`, "x-123456789");
    expect(result).toEqual({ ok: false, code: "INVALID_CREDENTIALS" });
  });

  it("usuario inactivo no puede ingresar", async () => {
    const user = await createTestUser("ADVISOR", { isActive: false });
    expect(await attempt(user.email, TEST_PASSWORD)).toEqual({
      ok: false,
      code: "INVALID_CREDENTIALS",
    });
  });

  it("bloqueo tras 5 fallos: ni la contraseña correcta entra hasta que vence el bloqueo", async () => {
    const user = await createTestUser("ADVISOR");
    const now = new Date();
    for (let i = 0; i < AUTH_CONFIG.maxFailedAttemptsPerAccount; i++) {
      await attempt(user.email, `mala-${i}-123456`, uniqueIp(), now);
    }
    const locked = await db.user.findUniqueOrThrow({ where: { id: user.id } });
    expect(locked.lockedUntil?.getTime()).toBeGreaterThan(now.getTime());
    expect(
      await db.auditLog.count({ where: { userId: user.id, action: "auth.account_locked" } }),
    ).toBe(1);

    expect(await attempt(user.email, TEST_PASSWORD, uniqueIp(), now)).toEqual({
      ok: false,
      code: "INVALID_CREDENTIALS",
    });

    // Vencido el bloqueo, y sin superar el límite por email en la ventana, vuelve a entrar.
    const later = new Date(now.getTime() + AUTH_CONFIG.rateLimitWindowMs + 60_000);
    const result = await attempt(user.email, TEST_PASSWORD, uniqueIp(), later);
    expect(result.ok).toBe(true);
  });

  it("rate limiting por IP: tras 20 fallos desde la misma IP se rechaza incluso un login válido", async () => {
    const ip = `192.0.2.${Math.floor(Math.random() * 250) + 1}`;
    await db.loginAttempt.deleteMany({ where: { ip } });
    const now = new Date();
    for (let i = 0; i < AUTH_CONFIG.maxFailedAttemptsPerIp; i++) {
      await attempt(`x-${randomUUID()}@test.chesco.local`, "x-123456789", ip, now);
    }
    const user = await createTestUser("ADMIN");
    expect(await attempt(user.email, TEST_PASSWORD, ip, now)).toEqual({
      ok: false,
      code: "RATE_LIMITED",
    });
    // Otra IP no está afectada.
    expect((await attempt(user.email, TEST_PASSWORD, uniqueIp(), now)).ok).toBe(true);
  });

  it("regenera la sesión: el token anterior queda revocado tras un nuevo login", async () => {
    const user = await createTestUser("ADMIN");
    const first = await attempt(user.email, TEST_PASSWORD);
    if (!first.ok) throw new Error("login inicial falló");
    const second = await login({
      email: user.email,
      password: TEST_PASSWORD,
      ip: uniqueIp(),
      userAgent: "vitest",
      currentToken: first.token,
    });
    if (!second.ok) throw new Error("segundo login falló");
    expect(second.token).not.toBe(first.token);
    expect(await validateSessionToken(first.token)).toBeNull();
    expect(await validateSessionToken(second.token)).not.toBeNull();
  });

  it("logout revoca la sesión y deja auditoría", async () => {
    const user = await createTestUser("SUPERVISOR");
    const result = await attempt(user.email, TEST_PASSWORD);
    if (!result.ok) throw new Error("login falló");
    await logout(result.token, { ip: "203.0.113.5", userAgent: "vitest" });
    expect(await validateSessionToken(result.token)).toBeNull();
    expect(await db.auditLog.count({ where: { userId: user.id, action: "auth.logout" } })).toBe(1);
  });
});

describe("cambio de contraseña", () => {
  it("exige la actual, quita mustChangePassword y cierra las OTRAS sesiones", async () => {
    const user = await createTestUser("ADMIN", { mustChangePassword: true });
    const a = await attempt(user.email, TEST_PASSWORD);
    const b = await attempt(user.email, TEST_PASSWORD);
    if (!a.ok || !b.ok) throw new Error("login falló");
    const current = await validateSessionToken(a.token);
    if (!current) throw new Error("sesión inválida");

    const wrong = await changeOwnPassword({
      userId: user.id,
      currentSessionId: current.sessionId,
      currentPassword: "no-es-la-actual-1",
      newPassword: "Nueva-clave-2026",
      ip: "203.0.113.5",
      userAgent: null,
    });
    expect(wrong).toEqual({ ok: false, code: "WRONG_CURRENT" });

    const ok = await changeOwnPassword({
      userId: user.id,
      currentSessionId: current.sessionId,
      currentPassword: TEST_PASSWORD,
      newPassword: "Nueva-clave-2026",
      ip: "203.0.113.5",
      userAgent: null,
    });
    expect(ok).toEqual({ ok: true });
    expect(await validateSessionToken(a.token)).not.toBeNull();
    expect(await validateSessionToken(b.token)).toBeNull();
    const updated = await db.user.findUniqueOrThrow({ where: { id: user.id } });
    expect(updated.mustChangePassword).toBe(false);
    expect((await attempt(user.email, "Nueva-clave-2026")).ok).toBe(true);
  });
});
