/**
 * Unit tests for auth tRPC router — password reset flow.
 *
 * Tests cover:
 *  - requestPasswordReset: known email → token created, email sent
 *  - requestPasswordReset: unknown email → success (no token, no email)
 *  - requestPasswordReset: second request → old token deleted, new one created
 *  - resetPassword: valid token → passwordHash updated, usedAt set
 *  - resetPassword: expired token → BAD_REQUEST
 *  - resetPassword: used token → BAD_REQUEST
 *  - resetPassword: non-existent token → NOT_FOUND
 */

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

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

const FUTURE = new Date(Date.now() + 60 * 60 * 1000);
const PAST = new Date(Date.now() - 1000);

function makeDb(overrides: {
  user?: Partial<Record<string, unknown>>;
  resetToken?: Partial<Record<string, unknown>>;
} = {}) {
  return {
    user: {
      findUnique: vi.fn().mockResolvedValue({ id: "user_1", email: "user@example.com" }),
      update: vi.fn().mockResolvedValue({}),
      ...overrides.user,
    },
    passwordResetToken: {
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
      create: vi.fn().mockResolvedValue({}),
      findUnique: vi.fn().mockResolvedValue(null),
      update: vi.fn().mockResolvedValue({}),
      ...overrides.resetToken,
    },
  } as never;
}

function makeCtx(db = makeDb()) {
  return { db, dbUser: null, session: null };
}

const { authRouter } = await import("../router/auth.js");
const { sendEmail } = await import("../lib/email.js");

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

  it("creates a token and sends an email for a known address", async () => {
    const db = makeDb();
    const ctx = makeCtx(db);

    const result = await authRouter.createCaller(ctx).requestPasswordReset({
      email: "user@example.com",
    });

    expect(result).toEqual({ success: true });
    expect(db.passwordResetToken.deleteMany).toHaveBeenCalledWith({
      where: { email: "user@example.com", usedAt: null },
    });
    expect(db.passwordResetToken.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ email: "user@example.com", token: expect.any(String) }),
      }),
    );
    expect(sendEmail).toHaveBeenCalledWith(
      expect.objectContaining({ to: "user@example.com", subject: expect.stringContaining("reset") }),
    );
  });

  it("returns success silently for an unknown email (no token, no email sent)", async () => {
    const db = makeDb({ user: { findUnique: vi.fn().mockResolvedValue(null) } });
    const ctx = makeCtx(db);

    const result = await authRouter.createCaller(ctx).requestPasswordReset({
      email: "ghost@example.com",
    });

    expect(result).toEqual({ success: true });
    expect(db.passwordResetToken.create).not.toHaveBeenCalled();
    expect(sendEmail).not.toHaveBeenCalled();
  });

  it("deletes existing unused tokens before creating a new one", async () => {
    const db = makeDb();
    const ctx = makeCtx(db);

    await authRouter.createCaller(ctx).requestPasswordReset({ email: "user@example.com" });

    // deleteMany called before create
    const deleteManyOrder = vi.mocked(db.passwordResetToken.deleteMany).mock.invocationCallOrder[0]!;
    const createOrder = vi.mocked(db.passwordResetToken.create).mock.invocationCallOrder[0]!;
    expect(deleteManyOrder).toBeLessThan(createOrder);
  });
});

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

  it("updates passwordHash and sets usedAt for a valid token", async () => {
    const validRecord = {
      token: "valid-token",
      email: "user@example.com",
      expiresAt: FUTURE,
      usedAt: null,
    };
    const db = makeDb({
      resetToken: { findUnique: vi.fn().mockResolvedValue(validRecord) },
    });
    const ctx = makeCtx(db);

    const result = await authRouter.createCaller(ctx).resetPassword({
      token: "valid-token",
      password: "NewPassword1!",
    });

    expect(result).toEqual({ success: true });
    expect(db.user.update).toHaveBeenCalledWith({
      where: { email: "user@example.com" },
      data: { passwordHash: "$argon2id$newhash" },
    });
    expect(db.passwordResetToken.update).toHaveBeenCalledWith({
      where: { token: "valid-token" },
      data: { usedAt: expect.any(Date) },
    });
  });

  it("throws NOT_FOUND for an unknown token", async () => {
    const db = makeDb({ resetToken: { findUnique: vi.fn().mockResolvedValue(null) } });
    const ctx = makeCtx(db);

    await expect(
      authRouter.createCaller(ctx).resetPassword({ token: "bad-token", password: "Password1!" }),
    ).rejects.toThrow(TRPCError);

    const err = await authRouter
      .createCaller(ctx)
      .resetPassword({ token: "bad-token", password: "Password1!" })
      .catch((e: TRPCError) => e);
    expect((err as TRPCError).code).toBe("NOT_FOUND");
  });

  it("throws BAD_REQUEST for an already-used token", async () => {
    const usedRecord = {
      token: "used-token",
      email: "user@example.com",
      expiresAt: FUTURE,
      usedAt: new Date(Date.now() - 5000),
    };
    const db = makeDb({ resetToken: { findUnique: vi.fn().mockResolvedValue(usedRecord) } });
    const ctx = makeCtx(db);

    const err = await authRouter
      .createCaller(ctx)
      .resetPassword({ token: "used-token", password: "Password1!" })
      .catch((e: TRPCError) => e);
    expect((err as TRPCError).code).toBe("BAD_REQUEST");
    expect((err as TRPCError).message).toMatch(/already been used/);
  });

  it("throws BAD_REQUEST for an expired token", async () => {
    const expiredRecord = {
      token: "expired-token",
      email: "user@example.com",
      expiresAt: PAST,
      usedAt: null,
    };
    const db = makeDb({ resetToken: { findUnique: vi.fn().mockResolvedValue(expiredRecord) } });
    const ctx = makeCtx(db);

    const err = await authRouter
      .createCaller(ctx)
      .resetPassword({ token: "expired-token", password: "Password1!" })
      .catch((e: TRPCError) => e);
    expect((err as TRPCError).code).toBe("BAD_REQUEST");
    expect((err as TRPCError).message).toMatch(/expired/);
  });
});
