import { afterAll, beforeEach, describe, expect, it } from "vitest";
import {
  browseBindableFolder,
  discoverBindableProjects,
  normalizeProjectSearchQuery,
} from "../../src/projectDiscovery.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";

describe("project discovery", () => {
  beforeEach(async () => {
    await resetDb();
    await seedUser("owner", "ou_owner", "OWNER");
    await seedUser("member", "ou_member", "MEMBER");
  });

  afterAll(async () => prisma.$disconnect());

  it("normalizes project codes across punctuation, width and case", () => {
    expect(normalizeProjectSearchQuery(" ＴＨ-141 ")).toBe("th141");
    expect(normalizeProjectSearchQuery("th_141")).toBe("th141");
  });

  it("searches normalized codes, Chinese names and complete folder breadcrumbs", async () => {
    const root = await createFolder("root-legacy", null, "旧教学资产");
    const subject = await createFolder("folder-physics", root.id, "物理竞赛教研");
    const thermal = await createFolder("folder-thermal", subject.id, "TH_热学专题");
    await createProject("project-th141", thermal.id, "TH-141_表面张力的严肃理论");
    await createProject("project-fullwidth", thermal.id, "ＴＨ－１４２_全角编号");
    await createProject("project-explicit-code", thermal.id, "表面课程", "PHY-001");
    await createProject("project-other", thermal.id, "TH-211_理想气体静力学");

    const byCode = await discover("TH141");
    expect(byCode.items[0]).toMatchObject({
      projectId: "project-th141",
      breadcrumb: "旧教学资产 / 物理竞赛教研 / TH_热学专题",
    });
    await expect(discover("表面张力")).resolves.toMatchObject({
      totalItems: 1,
      items: [{ projectId: "project-th141" }],
    });
    await expect(discover("TH142")).resolves.toMatchObject({
      items: [expect.objectContaining({ projectId: "project-fullwidth" })],
    });
    await expect(discover("PHY001")).resolves.toMatchObject({
      items: [expect.objectContaining({ projectId: "project-explicit-code" })],
    });
    const byPath = await discover("物理竞赛教研");
    expect(byPath.items.map((item) => item.projectId)).toEqual(expect.arrayContaining([
      "project-th141",
      "project-other",
    ]));
    await expect(discover("物理竞赛 TH141")).resolves.toMatchObject({
      items: [expect.objectContaining({ projectId: "project-th141" })],
    });
    await expect(discover("%")).resolves.toMatchObject({ totalItems: 0 });
  });

  it("refreshes descendant breadcrumbs after a folder rename", async () => {
    const root = await createFolder("root-before", null, "旧目录");
    const child = await createFolder("child", root.id, "子目录");
    await createProject("project-refresh", child.id, "项目");

    await prisma.folder.update({ where: { id: root.id }, data: { name: "新目录" } });

    await expect(discover("新目录")).resolves.toMatchObject({
      totalItems: 1,
      items: [{ projectId: "project-refresh", breadcrumb: "新目录 / 子目录" }],
    });
    await expect(discover("旧目录")).resolves.toMatchObject({ totalItems: 0 });
  });

  it("filters authorization before counting and paginating", async () => {
    for (let index = 0; index < 12; index += 1) {
      await createProject(`project-${index}`, null, `TH-${index}`);
    }
    await prisma.permissionGrant.create({
      data: {
        resourceType: "PROJECT",
        resourceId: "project-11",
        principalType: "USER",
        principalId: "ou_member",
        role: "MANAGE",
      },
    });

    const result = await discoverBindableProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_member",
      isOrgAdmin: false,
      query: "TH",
      page: 2,
      pageSize: 5,
    });

    expect(result).toMatchObject({ page: 1, totalItems: 1, totalPages: 1 });
    expect(result.items.map((item) => item.projectId)).toEqual(["project-11"]);
  });

  it("browses the preserved folder tree and paginates projects", async () => {
    const root = await createFolder("root", null, "旧教学资产");
    const child = await createFolder("child", root.id, "物理竞赛教研");
    await createProject("project-root", root.id, "根目录项目");

    const result = await browseBindableFolder({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      isOrgAdmin: true,
      folderId: root.id,
    });

    expect(result.breadcrumb).toBe("旧教学资产");
    expect(result.parentFolderId).toBeNull();
    expect(result.childFolders).toEqual([{ folderId: child.id, name: "物理竞赛教研" }]);
    expect(result.projects).toEqual([{
      projectId: "project-root",
      name: "根目录项目",
      breadcrumb: "旧教学资产",
    }]);
  });

  it("hides the system Inbox at root and presents its projects as unclassified", async () => {
    const inbox = await prisma.folder.findFirstOrThrow({
      where: { organizationId: DEFAULT_ORG_ID, kind: "SYSTEM_INBOX" },
    });
    const businessRoot = await createFolder("business-root", null, "业务目录");
    const businessInbox = await createFolder("business-inbox", businessRoot.id, "Inbox");
    await createProject("project-inbox", inbox.id, "新项目");

    const result = await browseBindableFolder({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      isOrgAdmin: true,
      folderId: null,
    });

    expect(result.childFolders.map((folder) => folder.folderId)).not.toContain(inbox.id);
    expect(result.projects).toContainEqual({
      projectId: "project-inbox",
      name: "新项目",
      breadcrumb: "未分类",
    });

    const businessResult = await browseBindableFolder({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      isOrgAdmin: true,
      folderId: businessRoot.id,
    });
    expect(businessResult.childFolders).toContainEqual({ folderId: businessInbox.id, name: "Inbox" });
  });

  it("shares one card page budget across folders and projects", async () => {
    const root = await createFolder("folder-page-root", null, "目录分页");
    for (let index = 0; index < 6; index += 1) {
      await createFolder(`folder-page-${index}`, root.id, `目录-${index}`);
      await createProject(`project-page-${index}`, root.id, `项目-${index}`);
    }

    const first = await browseBindableFolder({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      isOrgAdmin: true,
      folderId: root.id,
      pageSize: 8,
    });
    const second = await browseBindableFolder({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      isOrgAdmin: true,
      folderId: root.id,
      page: 2,
      pageSize: 8,
    });

    expect(first).toMatchObject({ page: 1, totalPages: 2, totalFolders: 6, totalProjects: 6 });
    expect(first.childFolders.length + first.projects.length).toBe(8);
    expect(second.childFolders.length + second.projects.length).toBe(4);
    expect([...first.projects, ...second.projects].map((project) => project.projectId).sort()).toEqual(
      Array.from({ length: 6 }, (_, index) => `project-page-${index}`),
    );
  });
});

async function seedUser(id: string, feishuOpenId: string, role: "OWNER" | "MEMBER"): Promise<void> {
  await prisma.user.create({
    data: {
      id,
      feishuOpenId,
      displayName: id,
      organizationMemberships: { create: { organizationId: DEFAULT_ORG_ID, role } },
    },
  });
}

async function createFolder(id: string, parentId: string | null, name: string) {
  return prisma.folder.create({
    data: { id, organizationId: DEFAULT_ORG_ID, parentId, name },
  });
}

async function createProject(id: string, folderId: string | null, name: string, code?: string) {
  return prisma.project.create({
    data: {
      id,
      organizationId: DEFAULT_ORG_ID,
      folderId,
      ...(code !== undefined ? { code } : {}),
      name,
      workspaceDir: `/tmp/${id}`,
    },
  });
}

function discover(query: string) {
  return discoverBindableProjects({
    prisma,
    organizationId: DEFAULT_ORG_ID,
    actorFeishuOpenId: "ou_owner",
    isOrgAdmin: true,
    query,
  });
}
