import { SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { commentRouter } from "../router/comment.js";
import { createCallerFactory } from "../trpc.js";

const createCaller = createCallerFactory(commentRouter);

function createContext(
  db: Record<string, unknown>,
  options: {
    role?: SystemRole;
    session?: boolean;
  } = {},
) {
  const { role = SystemRole.USER, session = true } = options;

  return {
    session: session
      ? {
          user: { email: "user@example.com", name: "User", image: null },
          expires: "2099-01-01T00:00:00.000Z",
        }
      : null,
    db: db as never,
    dbUser: session
      ? {
          id: role === SystemRole.ADMIN ? "user_admin" : "user_1",
          systemRole: role,
          permissionOverrides: null,
        }
      : null,
  };
}

describe("comment router authorization", () => {
  it("requires authentication before listing estimate comments", async () => {
    const estimateFindUnique = vi.fn();
    const commentFindMany = vi.fn();
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findMany: commentFindMany,
      },
    }, { session: false }));

    await expect(caller.list({ entityType: "estimate", entityId: "est_1" })).rejects.toMatchObject({
      code: "UNAUTHORIZED",
      message: "Authentication required",
    });

    expect(estimateFindUnique).not.toHaveBeenCalled();
    expect(commentFindMany).not.toHaveBeenCalled();
  });

  it("forbids plain users from reading or creating estimate comments", async () => {
    const estimateFindUnique = vi.fn();
    const commentFindMany = vi.fn();
    const commentCount = vi.fn();
    const commentCreate = vi.fn();
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findMany: commentFindMany,
        count: commentCount,
        create: commentCreate,
      },
    }));

    await expect(caller.list({ entityType: "estimate", entityId: "est_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Controller access required",
    });
    await expect(caller.count({ entityType: "estimate", entityId: "est_1" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Controller access required",
    });
    await expect(caller.create({
      entityType: "estimate",
      entityId: "est_1",
      body: "Please review this estimate.",
    })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Controller access required",
    });

    expect(estimateFindUnique).not.toHaveBeenCalled();
    expect(commentFindMany).not.toHaveBeenCalled();
    expect(commentCount).not.toHaveBeenCalled();
    expect(commentCreate).not.toHaveBeenCalled();
  });

  it("allows controllers to list, count, and create estimate comments", async () => {
    const estimateFindUnique = vi.fn().mockResolvedValue({ id: "est_1" });
    const commentFindMany = vi.fn().mockResolvedValue([]);
    const commentCount = vi.fn().mockResolvedValue(2);
    const commentCreate = vi.fn().mockResolvedValue({
      id: "comment_1",
      body: "Please review this estimate.",
      author: { id: "user_1", name: "Controller User", email: "user@example.com", image: null },
    });
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findMany: commentFindMany,
        count: commentCount,
        create: commentCreate,
      },
      notification: {
        create: vi.fn(),
      },
      auditLog: {
        create: vi.fn(),
      },
    }, { role: SystemRole.CONTROLLER }));

    const listResult = await caller.list({ entityType: "estimate", entityId: "est_1" });
    const countResult = await caller.count({ entityType: "estimate", entityId: "est_1" });
    const createResult = await caller.create({
      entityType: "estimate",
      entityId: "est_1",
      body: "Please review this estimate.",
    });

    expect(listResult).toEqual([]);
    expect(countResult).toBe(2);
    expect(createResult.id).toBe("comment_1");
    expect(estimateFindUnique).toHaveBeenCalledTimes(3);
    expect(commentCreate).toHaveBeenCalledWith({
      data: {
        entityType: "estimate",
        entityId: "est_1",
        authorId: "user_1",
        body: "Please review this estimate.",
        mentions: [],
      },
      include: {
        author: { select: { id: true, name: true, email: true, image: true } },
      },
    });
  });

  it("returns estimate mention candidates only for the controller audience", async () => {
    const estimateFindUnique = vi.fn().mockResolvedValue({ id: "est_1" });
    const userFindMany = vi.fn().mockResolvedValue([
      { id: "user_admin", name: "Admin User", email: "admin@example.com" },
      { id: "user_controller", name: "Controller User", email: "controller@example.com" },
    ]);
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      user: {
        findMany: userFindMany,
      },
    }, { role: SystemRole.CONTROLLER }));

    const result = await caller.listMentionCandidates({
      entityType: "estimate",
      entityId: "est_1",
      query: "con",
    });

    expect(result).toEqual([
      { id: "user_admin", name: "Admin User", email: "admin@example.com" },
      { id: "user_controller", name: "Controller User", email: "controller@example.com" },
    ]);
    expect(estimateFindUnique).toHaveBeenCalledTimes(1);
    expect(userFindMany).toHaveBeenCalledWith({
      where: {
        systemRole: { in: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER] },
        OR: [
          { name: { contains: "con", mode: "insensitive" } },
          { email: { contains: "con", mode: "insensitive" } },
        ],
      },
      select: {
        id: true,
        name: true,
        email: true,
      },
      orderBy: [{ name: "asc" }, { email: "asc" }],
      take: 20,
    });
  });

  it("forbids plain users from reading estimate mention candidates", async () => {
    const estimateFindUnique = vi.fn();
    const userFindMany = vi.fn();
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      user: {
        findMany: userFindMany,
      },
    }));

    await expect(caller.listMentionCandidates({
      entityType: "estimate",
      entityId: "est_1",
      query: "con",
    })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Controller access required",
    });

    expect(estimateFindUnique).not.toHaveBeenCalled();
    expect(userFindMany).not.toHaveBeenCalled();
  });

  it("allows users to list, count, and create comments on their own resource", async () => {
    const resourceFindUnique = vi.fn().mockResolvedValue({ id: "res_1" });
    const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_1" });
    const commentFindMany = vi.fn().mockResolvedValue([]);
    const commentCount = vi.fn().mockResolvedValue(1);
    const commentCreate = vi.fn().mockResolvedValue({
      id: "comment_resource_1",
      body: "Please update my profile summary.",
      author: { id: "user_1", name: "Resource User", email: "user@example.com", image: null },
    });
    const caller = createCaller(createContext({
      resource: {
        findUnique: resourceFindUnique,
        findFirst: resourceFindFirst,
      },
      comment: {
        findMany: commentFindMany,
        count: commentCount,
        create: commentCreate,
      },
      notification: {
        create: vi.fn(),
      },
      auditLog: {
        create: vi.fn(),
      },
    }));

    const listResult = await caller.list({ entityType: "resource", entityId: "res_1" });
    const countResult = await caller.count({ entityType: "resource", entityId: "res_1" });
    const createResult = await caller.create({
      entityType: "resource",
      entityId: "res_1",
      body: "Please update my profile summary.",
    });

    expect(listResult).toEqual([]);
    expect(countResult).toBe(1);
    expect(createResult.id).toBe("comment_resource_1");
    expect(resourceFindUnique).toHaveBeenCalledTimes(3);
    expect(resourceFindFirst).toHaveBeenCalledTimes(3);
    expect(commentCreate).toHaveBeenCalledWith({
      data: {
        entityType: "resource",
        entityId: "res_1",
        authorId: "user_1",
        body: "Please update my profile summary.",
        mentions: [],
      },
      include: {
        author: { select: { id: true, name: true, email: true, image: true } },
      },
    });
  });

  it("returns resource mention candidates for the own-resource audience only", async () => {
    const resourceFindUnique = vi
      .fn()
      .mockResolvedValueOnce({ id: "res_1" })
      .mockResolvedValueOnce({ userId: "user_1" });
    const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_1" });
    const userFindMany = vi.fn().mockResolvedValue([
      {
        id: "user_1",
        name: "Resource User",
        email: "user@example.com",
        systemRole: SystemRole.USER,
        permissionOverrides: null,
      },
      {
        id: "manager_1",
        name: "Manager User",
        email: "manager@example.com",
        systemRole: SystemRole.MANAGER,
        permissionOverrides: null,
      },
      {
        id: "viewer_1",
        name: "Viewer User",
        email: "viewer@example.com",
        systemRole: SystemRole.VIEWER,
        permissionOverrides: null,
      },
      {
        id: "user_2",
        name: "Override Staff",
        email: "override@example.com",
        systemRole: SystemRole.USER,
        permissionOverrides: { granted: ["viewAllResources"] },
      },
    ]);
    const caller = createCaller(createContext({
      resource: {
        findUnique: resourceFindUnique,
        findFirst: resourceFindFirst,
      },
      user: {
        findMany: userFindMany,
      },
    }));

    const result = await caller.listMentionCandidates({
      entityType: "resource",
      entityId: "res_1",
    });

    expect(result).toEqual([
      { id: "user_1", name: "Resource User", email: "user@example.com" },
      { id: "manager_1", name: "Manager User", email: "manager@example.com" },
      { id: "user_2", name: "Override Staff", email: "override@example.com" },
    ]);
    expect(resourceFindUnique).toHaveBeenCalledTimes(2);
    expect(resourceFindFirst).toHaveBeenCalledTimes(1);
    expect(userFindMany).toHaveBeenCalledWith({
      where: undefined,
      select: {
        id: true,
        name: true,
        email: true,
        systemRole: true,
        permissionOverrides: true,
      },
      orderBy: [{ name: "asc" }, { email: "asc" }],
    });
  });

  it("forbids users from reading or creating comments on foreign resources", async () => {
    const resourceFindUnique = vi.fn().mockResolvedValue({ id: "res_2" });
    const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_1" });
    const commentFindMany = vi.fn();
    const commentCount = vi.fn();
    const commentCreate = vi.fn();
    const caller = createCaller(createContext({
      resource: {
        findUnique: resourceFindUnique,
        findFirst: resourceFindFirst,
      },
      comment: {
        findMany: commentFindMany,
        count: commentCount,
        create: commentCreate,
      },
    }));

    await expect(caller.list({ entityType: "resource", entityId: "res_2" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "You can only comment on your own resource unless you have staff access",
    });
    await expect(caller.count({ entityType: "resource", entityId: "res_2" })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "You can only comment on your own resource unless you have staff access",
    });
    await expect(caller.create({
      entityType: "resource",
      entityId: "res_2",
      body: "This should not work.",
    })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "You can only comment on your own resource unless you have staff access",
    });

    expect(commentFindMany).not.toHaveBeenCalled();
    expect(commentCount).not.toHaveBeenCalled();
    expect(commentCreate).not.toHaveBeenCalled();
  });

  it("forbids users from reading mention candidates on foreign resources", async () => {
    const resourceFindUnique = vi.fn().mockResolvedValue({ id: "res_2" });
    const resourceFindFirst = vi.fn().mockResolvedValue({ id: "res_1" });
    const userFindMany = vi.fn();
    const caller = createCaller(createContext({
      resource: {
        findUnique: resourceFindUnique,
        findFirst: resourceFindFirst,
      },
      user: {
        findMany: userFindMany,
      },
    }));

    await expect(caller.listMentionCandidates({
      entityType: "resource",
      entityId: "res_2",
      query: "staff",
    })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "You can only comment on your own resource unless you have staff access",
    });

    expect(userFindMany).not.toHaveBeenCalled();
  });

  it("rejects unsupported comment entity types before touching the database", async () => {
    const estimateFindUnique = vi.fn();
    const commentFindMany = vi.fn();
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findMany: commentFindMany,
      },
    }, { role: SystemRole.CONTROLLER }));

    await expect(caller.list({
      entityType: "scope_item" as never,
      entityId: "scope_1",
    })).rejects.toMatchObject({
      code: "BAD_REQUEST",
    });

    expect(estimateFindUnique).not.toHaveBeenCalled();
    expect(commentFindMany).not.toHaveBeenCalled();
  });

  it("rejects replies whose parent comment belongs to another entity", async () => {
    const estimateFindUnique = vi.fn().mockResolvedValue({ id: "est_1" });
    const parentFindUnique = vi.fn().mockResolvedValue({
      id: "comment_parent",
      entityType: "estimate",
      entityId: "est_2",
    });
    const commentCreate = vi.fn();
    const caller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findUnique: parentFindUnique,
        create: commentCreate,
      },
    }, { role: SystemRole.CONTROLLER }));

    await expect(caller.create({
      entityType: "estimate",
      entityId: "est_1",
      parentId: "comment_parent",
      body: "Replying on the right estimate.",
    })).rejects.toMatchObject({
      code: "BAD_REQUEST",
      message: "Parent comment does not belong to the requested entity",
    });

    expect(commentCreate).not.toHaveBeenCalled();
  });

  it("requires comment authorship or admin rights after entity visibility is granted", async () => {
    const estimateFindUnique = vi.fn().mockResolvedValue({ id: "est_1" });
    const commentFindUnique = vi.fn().mockResolvedValue({
      id: "comment_1",
      authorId: "user_2",
      entityType: "estimate",
      entityId: "est_1",
    });
    const commentUpdate = vi.fn();
    const controllerCaller = createCaller(createContext({
      estimate: {
        findUnique: estimateFindUnique,
      },
      comment: {
        findUnique: commentFindUnique,
        update: commentUpdate,
      },
    }, { role: SystemRole.CONTROLLER }));

    await expect(controllerCaller.resolve({ id: "comment_1", resolved: true })).rejects.toMatchObject({
      code: "FORBIDDEN",
      message: "Only the comment author or an admin can resolve comments",
    });

    const adminUpdate = vi.fn().mockResolvedValue({
      id: "comment_1",
      body: "Needs review",
      resolved: true,
      author: { id: "user_2", name: "Other User", email: "other@example.com", image: null },
    });
    const adminCaller = createCaller(createContext({
      estimate: {
        findUnique: vi.fn().mockResolvedValue({ id: "est_1" }),
      },
      comment: {
        findUnique: vi.fn().mockResolvedValue({
          id: "comment_1",
          authorId: "user_2",
          entityType: "estimate",
          entityId: "est_1",
        }),
        update: adminUpdate,
      },
      auditLog: {
        create: vi.fn(),
      },
    }, { role: SystemRole.ADMIN }));

    const result = await adminCaller.resolve({ id: "comment_1", resolved: true });

    expect(result.resolved).toBe(true);
    expect(adminUpdate).toHaveBeenCalledWith({
      where: { id: "comment_1" },
      data: { resolved: true },
      include: {
        author: { select: { id: true, name: true, email: true, image: true } },
      },
    });
  });
});
