/**
 * Unit tests for MFA procedure functions in user-self-service-procedure-support.ts.
 *
 * Tests cover:
 *   - generateTotpSecret: secret creation and DB write
 *   - verifyAndEnableTotp: valid/invalid token, guard conditions
 *   - verifyTotp (login): valid/invalid token, not-enabled guard, rate limiting
 *   - getCurrentMfaStatus: status read
 *
 * otpauth is mocked so tests are deterministic and do not depend on
 * time-based code generation.
 * totpRateLimiter is mocked to allow/block independently of real Redis.
 */

import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

// ─── otpauth mock ────────────────────────────────────────────────────────────
const totpValidateMock = vi.hoisted(() => vi.fn<() => number | null>());

vi.mock("otpauth", () => {
  class Secret {
    base32: string;
    constructor() { this.base32 = "TESTBASE32SECRET"; }
    static fromBase32(v: string) { return v; }
  }
  class TOTP {
    validate(_args: { token: string; window: number }) { return totpValidateMock(); }
    toString() { return "otpauth://totp/CapaKraken:test@example.com?secret=TESTBASE32SECRET"; }
  }
  return { Secret, TOTP };
});

// ─── rate-limit mock ──────────────────────────────────────────────────────────
// Default: rate limit allows all requests. Override in specific tests.
const totpRateLimiterMock = vi.hoisted(() => vi.fn(async (_key: string) => ({
  allowed: true,
  remaining: 9,
  resetAt: new Date(Date.now() + 30_000),
})));

vi.mock("../middleware/rate-limit.js", () => ({
  totpRateLimiter: totpRateLimiterMock,
  apiRateLimiter: vi.fn(async () => ({ allowed: true, remaining: 99, resetAt: new Date() })),
  authRateLimiter: vi.fn(async () => ({ allowed: true, remaining: 4, resetAt: new Date() })),
}));

// ─── import after mock setup ─────────────────────────────────────────────────
import {
  generateTotpSecret,
  verifyAndEnableTotp,
  verifyTotp,
  getCurrentMfaStatus,
} from "../router/user-self-service-procedure-support.js";

// ─── context helpers ─────────────────────────────────────────────────────────

function makeSelfServiceCtx(dbOverrides: Record<string, unknown> = {}) {
  return {
    db: {
      user: {
        findUnique: vi.fn(),
        update: vi.fn().mockResolvedValue({}),
        ...((dbOverrides.user as object | undefined) ?? {}),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({ id: "audit_1" }),
        ...((dbOverrides.auditLog as object | undefined) ?? {}),
      },
    },
    dbUser: { id: "user_1", systemRole: "ADMIN" as const, permissionOverrides: null },
    session: {
      user: { email: "test@example.com", name: "Test User", image: null },
      expires: "2027-01-01T00:00:00.000Z",
    },
    userId: "user_1",
    userRole: "ADMIN" as const,
    permissions: new Set<string>(),
    roleDefaults: null,
  };
}

function makePublicCtx(dbOverrides: Record<string, unknown> = {}) {
  return {
    db: {
      user: {
        findUnique: vi.fn(),
        ...((dbOverrides.user as object | undefined) ?? {}),
      },
    },
  };
}

// ─── generateTotpSecret ───────────────────────────────────────────────────────

describe("generateTotpSecret", () => {
  beforeEach(() => { vi.clearAllMocks(); });

  it("writes the base32 secret to the user record", async () => {
    const ctx = makeSelfServiceCtx();
    await generateTotpSecret(ctx as Parameters<typeof generateTotpSecret>[0]);
    expect(ctx.db.user.update).toHaveBeenCalledWith({
      where: { id: "user_1" },
      data: { totpSecret: "TESTBASE32SECRET" },
    });
  });

  it("returns the base32 secret and an otpauth URI", async () => {
    const ctx = makeSelfServiceCtx();
    const result = await generateTotpSecret(ctx as Parameters<typeof generateTotpSecret>[0]);
    expect(result.secret).toBe("TESTBASE32SECRET");
    expect(result.uri).toMatch(/^otpauth:\/\/totp\//);
  });
});

// ─── verifyAndEnableTotp ──────────────────────────────────────────────────────

describe("verifyAndEnableTotp", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    totpValidateMock.mockReset();
  });

  const baseUser = {
    id: "user_1",
    name: "Test User",
    email: "test@example.com",
    totpSecret: "TESTBASE32SECRET",
    totpEnabled: false,
  };

  it("enables TOTP and returns { enabled: true } when token is valid", async () => {
    totpValidateMock.mockReturnValue(0); // delta 0 = current window
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue(baseUser) },
    });
    const result = await verifyAndEnableTotp(
      ctx as Parameters<typeof verifyAndEnableTotp>[0],
      { token: "123456" },
    );
    expect(result).toEqual({ enabled: true });
    expect(ctx.db.user.update).toHaveBeenCalledWith({
      where: { id: "user_1" },
      data: { totpEnabled: true },
    });
  });

  it("throws BAD_REQUEST when token is invalid", async () => {
    totpValidateMock.mockReturnValue(null);
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue(baseUser) },
    });
    await expect(
      verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], { token: "000000" }),
    ).rejects.toThrow(new TRPCError({ code: "BAD_REQUEST", message: "Invalid TOTP token." }));
  });

  it("throws BAD_REQUEST when no secret has been generated yet", async () => {
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ ...baseUser, totpSecret: null }) },
    });
    await expect(
      verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], { token: "123456" }),
    ).rejects.toThrow(TRPCError);
  });

  it("throws BAD_REQUEST when TOTP is already enabled", async () => {
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ ...baseUser, totpEnabled: true }) },
    });
    await expect(
      verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], { token: "123456" }),
    ).rejects.toThrow(new TRPCError({ code: "BAD_REQUEST", message: "TOTP is already enabled." }));
  });

  it("writes an audit entry on successful enable", async () => {
    totpValidateMock.mockReturnValue(0);
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue(baseUser) },
    });
    await verifyAndEnableTotp(ctx as Parameters<typeof verifyAndEnableTotp>[0], { token: "123456" });
    // Audit entry is fire-and-forget; wait one tick
    await new Promise((r) => setTimeout(r, 0));
    expect(ctx.db.auditLog.create).toHaveBeenCalled();
  });
});

// ─── verifyTotp (login step) ──────────────────────────────────────────────────

describe("verifyTotp", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    totpValidateMock.mockReset();
    totpRateLimiterMock.mockResolvedValue({ allowed: true, remaining: 9, resetAt: new Date(Date.now() + 30_000) });
  });

  const mfaUser = { id: "user_1", totpSecret: "TESTBASE32SECRET", totpEnabled: true };

  it("returns { valid: true } when token is correct", async () => {
    totpValidateMock.mockReturnValue(0);
    const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
    const result = await verifyTotp(ctx as Parameters<typeof verifyTotp>[0], {
      userId: "user_1",
      token: "123456",
    });
    expect(result).toEqual({ valid: true });
  });

  it("throws UNAUTHORIZED when token is invalid", async () => {
    totpValidateMock.mockReturnValue(null);
    const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
    await expect(
      verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "000000" }),
    ).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED", message: "Invalid TOTP token." }));
  });

  it("throws BAD_REQUEST when user does not have MFA enabled", async () => {
    const ctx = makePublicCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ ...mfaUser, totpEnabled: false }) },
    });
    await expect(
      verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
    ).rejects.toThrow(new TRPCError({ code: "BAD_REQUEST", message: "TOTP is not enabled for this user." }));
  });

  it("throws BAD_REQUEST when user has no TOTP secret (inconsistent state)", async () => {
    const ctx = makePublicCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ ...mfaUser, totpSecret: null }) },
    });
    await expect(
      verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
    ).rejects.toThrow(TRPCError);
  });

  it("throws TOO_MANY_REQUESTS when rate limit is exceeded", async () => {
    totpRateLimiterMock.mockResolvedValue({ allowed: false, remaining: 0, resetAt: new Date(Date.now() + 30_000) });
    const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
    await expect(
      verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
    ).rejects.toThrow(new TRPCError({ code: "TOO_MANY_REQUESTS", message: "Too many TOTP attempts. Please wait before trying again." }));
  });

  it("does not check the token when rate limit is exceeded", async () => {
    totpRateLimiterMock.mockResolvedValue({ allowed: false, remaining: 0, resetAt: new Date() });
    const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
    await expect(
      verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" }),
    ).rejects.toThrow(TRPCError);
    // DB user lookup must not happen when rate-limited
    expect(ctx.db.user.findUnique).not.toHaveBeenCalled();
  });

  it("calls the rate limiter with the userId as key", async () => {
    totpValidateMock.mockReturnValue(0);
    const ctx = makePublicCtx({ user: { findUnique: vi.fn().mockResolvedValue(mfaUser) } });
    await verifyTotp(ctx as Parameters<typeof verifyTotp>[0], { userId: "user_1", token: "123456" });
    expect(totpRateLimiterMock).toHaveBeenCalledWith("user_1");
  });
});

// ─── getCurrentMfaStatus ──────────────────────────────────────────────────────

describe("getCurrentMfaStatus", () => {
  beforeEach(() => { vi.clearAllMocks(); });

  it("returns totpEnabled: true when MFA is active", async () => {
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ totpEnabled: true }) },
    });
    const result = await getCurrentMfaStatus(ctx as Parameters<typeof getCurrentMfaStatus>[0]);
    expect(result).toEqual({ totpEnabled: true });
  });

  it("returns totpEnabled: false when MFA is inactive", async () => {
    const ctx = makeSelfServiceCtx({
      user: { findUnique: vi.fn().mockResolvedValue({ totpEnabled: false }) },
    });
    const result = await getCurrentMfaStatus(ctx as Parameters<typeof getCurrentMfaStatus>[0]);
    expect(result).toEqual({ totpEnabled: false });
  });
});
