/**
 * Unit tests for the invite tRPC router.
 *
 * Tests cover:
 *  - createInvite: token created, email sent
 *  - createInvite: duplicate active invite → CONFLICT
 *  - getInvite: valid token → returns email + role
 *  - getInvite: used token → BAD_REQUEST
 *  - getInvite: expired token → BAD_REQUEST
 *  - getInvite: unknown token → NOT_FOUND
 *  - acceptInvite: valid → user created, usedAt set
 *  - acceptInvite: used token → BAD_REQUEST
 *  - acceptInvite: expired token → BAD_REQUEST
 *  - revokeInvite: deletes the token
 */

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

// ── Mocks ────────────────────────────────────────────────────────────────────

vi.mock("../lib/email.js", () => ({ sendEmail: vi.fn().mockResolvedValue(true) }));
vi.mock("@node-rs/argon2", () => ({ hash: vi.fn().mockResolvedValue("$argon2id$hashed") }));

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

const ADMIN_USER = { id: "admin_1" };
const FUTURE = new Date(Date.now() + 72 * 60 * 60 * 1000);
const PAST = new Date(Date.now() - 1000);

function makeInviteDb(overrides: Record<string, unknown> = {}) {
  return {
    findFirst: vi.fn().mockResolvedValue(null),
    create: vi.fn().mockResolvedValue({}),
    findUnique: vi.fn().mockResolvedValue(null),
    findMany: vi.fn().mockResolvedValue([]),
    update: vi.fn().mockResolvedValue({}),
    delete: vi.fn().mockResolvedValue({}),
    ...overrides,
  };
}

function makeCtx(inviteDb: ReturnType<typeof makeInviteDb> = makeInviteDb()) {
  return {
    db: {
      inviteToken: inviteDb,
      user: { findUnique: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({}) },
    } as never,
    dbUser: { id: ADMIN_USER.id, systemRole: "ADMIN", permissionOverrides: null },
    session: {
      user: { id: ADMIN_USER.id, email: "admin@example.com" },
      expires: "2099-01-01T00:00:00.000Z",
    },
  };
}

// ── Import after mocks ───────────────────────────────────────────────────────
const { inviteRouter } = await import("../router/invite.js");

function caller(ctx: ReturnType<typeof makeCtx>) {
  return inviteRouter.createCaller(ctx as never);
}

// ── Tests ────────────────────────────────────────────────────────────────────

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

  it("creates a token and sends an email", async () => {
    const { sendEmail } = await import("../lib/email.js");
    const ctx = makeCtx();
    await caller(ctx).createInvite({ email: "alice@example.com", role: SystemRole.USER });

    expect(ctx.db.inviteToken.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ email: "alice@example.com", role: SystemRole.USER }),
      }),
    );
    expect(sendEmail).toHaveBeenCalledWith(expect.objectContaining({ to: "alice@example.com" }));
  });

  it("throws CONFLICT when an active invite already exists", async () => {
    const inviteDb = makeInviteDb({
      findFirst: vi.fn().mockResolvedValue({ id: "existing" }),
    });
    const ctx = makeCtx(inviteDb);
    await expect(
      caller(ctx).createInvite({ email: "alice@example.com", role: SystemRole.USER }),
    ).rejects.toThrow(TRPCError);
  });
});

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

  it("returns email and role for a valid unused token", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        email: "alice@example.com",
        role: SystemRole.MANAGER,
        expiresAt: FUTURE,
        usedAt: null,
      }),
    });
    const result = await caller(makeCtx(inviteDb)).getInvite({ token: "abc" });
    expect(result).toEqual({ email: "alice@example.com", role: SystemRole.MANAGER });
  });

  it("throws NOT_FOUND for unknown token", async () => {
    const ctx = makeCtx();
    await expect(caller(ctx).getInvite({ token: "unknown" })).rejects.toThrow(TRPCError);
  });

  it("throws BAD_REQUEST for already-used token", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        email: "alice@example.com",
        role: SystemRole.USER,
        expiresAt: FUTURE,
        usedAt: new Date(),
      }),
    });
    await expect(caller(makeCtx(inviteDb)).getInvite({ token: "abc" })).rejects.toThrow(TRPCError);
  });

  it("throws BAD_REQUEST for expired token", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        email: "alice@example.com",
        role: SystemRole.USER,
        expiresAt: PAST,
        usedAt: null,
      }),
    });
    await expect(caller(makeCtx(inviteDb)).getInvite({ token: "abc" })).rejects.toThrow(TRPCError);
  });
});

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

  it("creates the user and marks token as used", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        id: "inv_1",
        email: "alice@example.com",
        role: SystemRole.USER,
        expiresAt: FUTURE,
        usedAt: null,
        token: "validtoken",
      }),
    });
    const ctx = makeCtx(inviteDb);
    const result = await caller(ctx).acceptInvite({ token: "validtoken", password: "SecurePass1!" });

    expect(ctx.db.user.create).toHaveBeenCalled();
    expect(inviteDb.update).toHaveBeenCalledWith(
      expect.objectContaining({ data: { usedAt: expect.any(Date) } }),
    );
    expect(result).toEqual({ success: true });
  });

  it("throws BAD_REQUEST for used token", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        email: "alice@example.com",
        role: SystemRole.USER,
        expiresAt: FUTURE,
        usedAt: new Date(),
        token: "usedtoken",
      }),
    });
    await expect(
      caller(makeCtx(inviteDb)).acceptInvite({ token: "usedtoken", password: "SecurePass1!" }),
    ).rejects.toThrow(TRPCError);
  });

  it("throws BAD_REQUEST for expired token", async () => {
    const inviteDb = makeInviteDb({
      findUnique: vi.fn().mockResolvedValue({
        email: "alice@example.com",
        role: SystemRole.USER,
        expiresAt: PAST,
        usedAt: null,
        token: "expiredtoken",
      }),
    });
    await expect(
      caller(makeCtx(inviteDb)).acceptInvite({ token: "expiredtoken", password: "SecurePass1!" }),
    ).rejects.toThrow(TRPCError);
  });
});

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

  it("deletes the invite token", async () => {
    const inviteDb = makeInviteDb();
    const ctx = makeCtx(inviteDb);
    const result = await caller(ctx).revokeInvite({ id: "inv_1" });
    expect(inviteDb.delete).toHaveBeenCalledWith({ where: { id: "inv_1" } });
    expect(result).toEqual({ success: true });
  });
});
