import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  countComments,
  createComment,
  deleteComment,
  listCommentMentionCandidates,
  listComments,
  resolveComment,
} from "../router/comment-procedure-support.js";

const {
  assertCommentEntityAccess,
  createNotification,
  createAuditEntry,
} = vi.hoisted(() => ({
  assertCommentEntityAccess: vi.fn(),
  createNotification: vi.fn(),
  createAuditEntry: vi.fn(),
}));

vi.mock("../lib/comment-entity-registry.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../lib/comment-entity-registry.js")>();
  return {
    ...actual,
    assertCommentEntityAccess,
  };
});

vi.mock("../lib/create-notification.js", () => ({
  createNotification,
}));

vi.mock("../lib/audit.js", () => ({
  createAuditEntry,
}));

function createContext(overrides: Record<string, unknown> = {}) {
  return {
    db: {
      comment: {
        findMany: vi.fn().mockResolvedValue([]),
        count: vi.fn().mockResolvedValue(2),
        findUnique: vi.fn().mockResolvedValue({
          id: "comment_1",
          authorId: "user_1",
          entityType: "estimate",
          entityId: "est_1",
        }),
        create: vi.fn().mockResolvedValue({
          id: "comment_1",
          body: "Hi @[Bob](user_2)",
          author: { id: "user_1", name: "Alice", email: "alice@example.com", image: null },
        }),
        update: vi.fn().mockResolvedValue({
          id: "comment_1",
          resolved: true,
          author: { id: "user_1", name: "Alice", email: "alice@example.com", image: null },
        }),
        deleteMany: vi.fn().mockResolvedValue({ count: 1 }),
        delete: vi.fn().mockResolvedValue(undefined),
      },
    },
    dbUser: {
      id: "user_1",
      systemRole: "CONTROLLER",
      permissionOverrides: null,
    },
    roleDefaults: null,
    ...overrides,
  };
}

describe("comment procedure support", () => {
  beforeEach(() => {
    assertCommentEntityAccess.mockReset();
    createNotification.mockReset();
    createAuditEntry.mockReset();
    assertCommentEntityAccess.mockResolvedValue({
      buildLink: (entityId: string) => `/estimates/${entityId}?tab=comments`,
      listMentionCandidates: vi.fn().mockResolvedValue([
        { id: "user_2", name: "Bob", email: "bob@example.com" },
      ]),
    });
  });

  it("lists and counts comments after access checks", async () => {
    const ctx = createContext();

    const listResult = await listComments(ctx as never, {
      entityType: "estimate",
      entityId: "est_1",
    });
    const countResult = await countComments(ctx as never, {
      entityType: "estimate",
      entityId: "est_1",
    });

    expect(listResult).toEqual([]);
    expect(countResult).toBe(2);
    expect(assertCommentEntityAccess).toHaveBeenNthCalledWith(1, ctx, "estimate", "est_1");
    expect(assertCommentEntityAccess).toHaveBeenNthCalledWith(2, ctx, "estimate", "est_1");
  });

  it("normalizes mention candidate queries via the policy returned from access checks", async () => {
    const listMentionCandidates = vi.fn().mockResolvedValue([
      { id: "user_2", name: "Bob", email: "bob@example.com" },
    ]);
    assertCommentEntityAccess.mockResolvedValue({
      buildLink: (entityId: string) => `/estimates/${entityId}?tab=comments`,
      listMentionCandidates,
    });

    const result = await listCommentMentionCandidates(createContext() as never, {
      entityType: "estimate",
      entityId: "est_1",
      query: "",
    });

    expect(result).toEqual([{ id: "user_2", name: "Bob", email: "bob@example.com" }]);
    expect(listMentionCandidates).toHaveBeenCalledWith(expect.anything(), "est_1", undefined);
  });

  it("creates comments, validates parent ownership, and sends mention notifications", async () => {
    const ctx = createContext({
      db: {
        comment: {
          findMany: vi.fn(),
          count: vi.fn(),
          findUnique: vi.fn().mockResolvedValue({
            id: "comment_parent",
            entityType: "estimate",
            entityId: "est_1",
          }),
          create: vi.fn().mockResolvedValue({
            id: "comment_1",
            body: "Hi @[Bob](user_2) and @[Alice](user_1)",
            author: { id: "user_1", name: "Alice", email: "alice@example.com", image: null },
          }),
          update: vi.fn(),
          deleteMany: vi.fn(),
          delete: vi.fn(),
        },
      },
    });

    const result = await createComment(ctx as never, {
      entityType: "estimate",
      entityId: "est_1",
      parentId: "comment_parent",
      body: "Hi @[Bob](user_2) and @[Alice](user_1)",
    });

    expect(result.id).toBe("comment_1");
    expect(ctx.db.comment.create).toHaveBeenCalledWith({
      data: {
        entityType: "estimate",
        entityId: "est_1",
        parentId: "comment_parent",
        authorId: "user_1",
        body: "Hi @[Bob](user_2) and @[Alice](user_1)",
        mentions: ["user_2", "user_1"],
      },
      include: {
        author: { select: { id: true, name: true, email: true, image: true } },
      },
    });
    expect(createNotification).toHaveBeenCalledTimes(1);
    expect(createNotification).toHaveBeenCalledWith(expect.objectContaining({
      userId: "user_2",
      entityId: "est_1",
      entityType: "estimate",
    }));
  });

  it("rejects mismatched parent entities before creating a reply", async () => {
    const ctx = createContext({
      db: {
        comment: {
          findMany: vi.fn(),
          count: vi.fn(),
          findUnique: vi.fn().mockResolvedValue({
            id: "comment_parent",
            entityType: "estimate",
            entityId: "est_other",
          }),
          create: vi.fn(),
          update: vi.fn(),
          deleteMany: vi.fn(),
          delete: vi.fn(),
        },
      },
    });

    await expect(createComment(ctx as never, {
      entityType: "estimate",
      entityId: "est_1",
      parentId: "comment_parent",
      body: "Reply",
    })).rejects.toThrowError(new TRPCError({
      code: "BAD_REQUEST",
      message: "Parent comment does not belong to the requested entity",
    }));

    expect(ctx.db.comment.create).not.toHaveBeenCalled();
  });

  it("resolves and deletes comments only after management checks", async () => {
    const ctx = createContext({
      dbUser: {
        id: "user_admin",
        systemRole: "ADMIN",
        permissionOverrides: null,
      },
    });

    const resolved = await resolveComment(ctx as never, {
      id: "comment_1",
      resolved: true,
    });
    await deleteComment(ctx as never, { id: "comment_1" });

    expect(resolved.resolved).toBe(true);
    expect(ctx.db.comment.update).toHaveBeenCalledWith({
      where: { id: "comment_1" },
      data: { resolved: true },
      include: {
        author: { select: { id: true, name: true, email: true, image: true } },
      },
    });
    expect(ctx.db.comment.deleteMany).toHaveBeenCalledWith({
      where: { parentId: "comment_1" },
    });
    expect(ctx.db.comment.delete).toHaveBeenCalledWith({
      where: { id: "comment_1" },
    });
  });
});
