import { chmod, mkdtemp, readdir, rm, stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization } from "./helpers.js";
import {
  archiveFeishuChatBinding,
  bindFeishuChatToProject,
  createFolder,
  createProjectFromFeishuChat,
  createProjectFromOrgAdmin,
  moveProjectToFolder,
  setMembersCanCreateProjects,
} from "../../src/projectOnboarding.js";

const workspaceRoots: string[] = [];
const itAsNonRoot = typeof process.getuid === "function" && process.getuid() === 0 ? it.skip : it;

describe("ADR-0021 project onboarding", () => {
  beforeEach(async () => {
    await dropPermissionSettingsFailureTrigger();
    await resetDb();
  });

  afterEach(async () => {
    await dropPermissionSettingsFailureTrigger();
    while (workspaceRoots.length > 0) {
      const root = workspaceRoots.pop();
      if (root !== undefined) await rm(root, { recursive: true, force: true });
    }
  });

  afterAll(async () => {
    await dropPermissionSettingsFailureTrigger();
    await prisma.$disconnect();
  });

  it("lets an org admin create an unbound project in a folder", async () => {
    await seedUser("u-admin", "ou_admin", "ADMIN");
    const folder = await createFolder(prisma, {
      organizationId: DEFAULT_ORG_ID,
      name: "Grade 7",
      sortKey: "010000",
    });
    const workspaceRoot = await tempWorkspaceRoot();

    const result = await createProjectFromOrgAdmin(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_admin",
      name: "Newton Lesson",
      folderId: folder.id,
      workspaceRoot,
    });

    expect(result.folderId).toBe(folder.id);
    expect(result.chatId).toBeUndefined();
    expect((await stat(result.workspaceDir)).isDirectory()).toBe(true);
    expect(result.workspaceDir).toMatch(/\/o_[A-Za-z0-9_-]{16}\/p_[A-Za-z0-9_-]{16}$/);
    const grant = await prisma.permissionGrant.findFirst({
      where: {
        resourceType: "PROJECT",
        resourceId: result.projectId,
        principalType: "USER",
        principalId: "ou_admin",
        revokedAt: null,
      },
      select: { role: true },
    });
    expect(grant?.role).toBe("MANAGE");
    const settings = await prisma.permissionSettings.findUnique({
      where: { resourceType_resourceId: { resourceType: "PROJECT", resourceId: result.projectId } },
    });
    expect(settings?.agentTrigger).toBe("ROLE");
  });

  it("lets an org member create and bind a project from an unbound Feishu chat", async () => {
    await seedUser("u-member", "ou_member", "MEMBER");
    const workspaceRoot = await tempWorkspaceRoot();

    const result = await createProjectFromFeishuChat(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_member",
      chatId: "chat-onboard",
      name: "Poetry Workshop",
      workspaceRoot,
    });

    expect(result.chatId).toBe("chat-onboard");
    const binding = await prisma.projectGroupBinding.findFirst({
      where: { projectId: result.projectId, chatId: "chat-onboard", archivedAt: null },
    });
    expect(binding).not.toBeNull();
    const grants = await prisma.permissionGrant.findMany({
      where: { resourceType: "PROJECT", resourceId: result.projectId, revokedAt: null },
      select: { principalType: true, principalId: true, role: true },
      orderBy: { principalType: "asc" },
    });
    expect(grants).toEqual(expect.arrayContaining([
      { principalType: "USER", principalId: "ou_member", role: "MANAGE" },
      { principalType: "FEISHU_CHAT", principalId: "chat-onboard", role: "EDIT" },
    ]));
  });

  it("promotes a legacy root Inbox when Feishu chat creates a project", async () => {
    await seedUser("u-member", "ou_member", "MEMBER");
    // Production drift after bootstrap omitted kind=SYSTEM_INBOX. The protect
    // trigger refuses demotion, so the test plants the legacy shape directly.
    await prisma.$executeRawUnsafe(`ALTER TABLE "Folder" DISABLE TRIGGER cph_protect_system_inbox`);
    try {
      await prisma.folder.updateMany({
        where: { organizationId: DEFAULT_ORG_ID, kind: "SYSTEM_INBOX", archivedAt: null },
        data: { kind: "REGULAR" },
      });
    } finally {
      await prisma.$executeRawUnsafe(`ALTER TABLE "Folder" ENABLE TRIGGER cph_protect_system_inbox`);
    }
    const legacy = await prisma.folder.findFirstOrThrow({
      where: { organizationId: DEFAULT_ORG_ID, parentId: null, name: "Inbox", archivedAt: null },
      select: { id: true, kind: true },
    });
    expect(legacy.kind).toBe("REGULAR");

    const result = await createProjectFromFeishuChat(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_member",
      chatId: "chat-legacy-inbox",
      name: "Recovered Inbox Project",
      workspaceRoot: await tempWorkspaceRoot(),
    });

    expect(result.folderId).toBe(legacy.id);
    await expect(prisma.folder.findUniqueOrThrow({
      where: { id: legacy.id },
      select: { kind: true },
    })).resolves.toEqual({ kind: "SYSTEM_INBOX" });
    expect(await prisma.folder.count({
      where: { organizationId: DEFAULT_ORG_ID, name: "Inbox", archivedAt: null },
    })).toBe(1);
  });

  it("blocks ordinary Feishu project creation when the org setting is off", async () => {
    await seedUser("u-member", "ou_member", "MEMBER");
    await setMembersCanCreateProjects(prisma, {
      organizationId: DEFAULT_ORG_ID,
      enabled: false,
    });

    await expect(createProjectFromFeishuChat(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_member",
      chatId: "chat-denied",
      name: "Denied Project",
      workspaceRoot: await tempWorkspaceRoot(),
    })).rejects.toThrow(/members cannot create projects/);
  });

  it.each(["SUSPENDED", "ARCHIVED"] as const)(
    "blocks project creation and organization mutations when the org is %s",
    async (status) => {
      await seedUser(`u-${status}`, `ou_${status}`, "OWNER");
      const folder = await createFolder(prisma, {
        organizationId: DEFAULT_ORG_ID,
        name: `Before ${status}`,
      });
      const workspaceRoot = await tempWorkspaceRoot();
      const project = await createProjectFromOrgAdmin(prisma, {
        organizationId: DEFAULT_ORG_ID,
        actorFeishuOpenId: `ou_${status}`,
        name: `Existing ${status}`,
        workspaceRoot,
      });
      const organizationWorkspace = dirname(project.workspaceDir);
      const workspacesBefore = (await readdir(organizationWorkspace)).sort();
      await prisma.organization.update({ where: { id: DEFAULT_ORG_ID }, data: { status } });

      await expect(createProjectFromOrgAdmin(prisma, {
        organizationId: DEFAULT_ORG_ID,
        actorFeishuOpenId: `ou_${status}`,
        name: "Blocked",
        workspaceRoot,
      })).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
      await expect(readdir(organizationWorkspace)).resolves.toEqual(workspacesBefore);
      await expect(bindFeishuChatToProject(prisma, {
        projectId: project.projectId,
        actorFeishuOpenId: `ou_${status}`,
        chatId: `chat-${status}`,
      })).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
      await expect(createFolder(prisma, {
        organizationId: DEFAULT_ORG_ID,
        name: "Blocked folder",
      })).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
      await expect(setMembersCanCreateProjects(prisma, {
        organizationId: DEFAULT_ORG_ID,
        enabled: false,
      })).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
      await expect(moveProjectToFolder(prisma, {
        projectId: project.projectId,
        folderId: folder.id,
      })).rejects.toThrow(`organization ${DEFAULT_ORG_ID} is ${status}`);
    },
  );

  it("removes a newly created workspace when the database transaction later fails", async () => {
    await seedUser("u-compensate", "ou_compensate", "ADMIN");
    await installPermissionSettingsFailureTrigger(0);
    const workspaceRoot = await tempWorkspaceRoot();

    await expect(createProjectFromOrgAdmin(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_compensate",
      name: "Must Roll Back",
      workspaceRoot,
    })).rejects.toThrow(/forced permission settings failure/);

    const organizationWorkspaces = await readdir(workspaceRoot);
    expect(organizationWorkspaces).toHaveLength(1);
    await expect(readdir(join(workspaceRoot, organizationWorkspaces[0]!))).resolves.toEqual([]);
    await expect(prisma.project.count()).resolves.toBe(0);
  });

  itAsNonRoot("surfaces both the transaction and workspace cleanup failures", async () => {
    await seedUser("u-cleanup-failure", "ou_cleanup_failure", "ADMIN");
    await installPermissionSettingsFailureTrigger(1);
    const workspaceRoot = await tempWorkspaceRoot();
    let organizationWorkspace: string | undefined;
    const pending = createProjectFromOrgAdmin(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_cleanup_failure",
      name: "Cleanup Must Fail Loudly",
      workspaceRoot,
    });
    const outcome = pending.then(
      () => null,
      (error: unknown) => error,
    );

    await vi.waitFor(async () => {
      const organizationWorkspaces = await readdir(workspaceRoot);
      expect(organizationWorkspaces).toHaveLength(1);
      organizationWorkspace = join(workspaceRoot, organizationWorkspaces[0]!);
      expect(await readdir(organizationWorkspace)).toHaveLength(1);
    }, { timeout: 2_000 });
    if (organizationWorkspace === undefined) throw new Error("organization workspace was not allocated");
    await chmod(organizationWorkspace, 0o500);
    try {
      const error = await outcome;
      expect(error).toBeInstanceOf(AggregateError);
      expect((error as AggregateError).errors).toHaveLength(2);
      await expect(prisma.project.count()).resolves.toBe(0);
    } finally {
      await chmod(organizationWorkspace, 0o700);
    }
  });

  it("binds an existing project once, archives the binding, then allows a new active binding", async () => {
    await seedUser("u-owner", "ou_owner", "OWNER");
    const workspaceRoot = await tempWorkspaceRoot();
    const project = await createProjectFromOrgAdmin(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      name: "Reusable Project",
      workspaceRoot,
    });

    await bindFeishuChatToProject(prisma, {
      projectId: project.projectId,
      actorFeishuOpenId: "ou_owner",
      chatId: "chat-first",
    });
    await expect(bindFeishuChatToProject(prisma, {
      projectId: project.projectId,
      actorFeishuOpenId: "ou_owner",
      chatId: "chat-second",
    })).rejects.toThrow(/already bound/);

    await expect(archiveFeishuChatBinding(prisma, {
      projectId: project.projectId,
      actorFeishuOpenId: "ou_owner",
    })).resolves.toMatchObject({ archived: true, chatId: "chat-first" });
    await bindFeishuChatToProject(prisma, {
      projectId: project.projectId,
      actorFeishuOpenId: "ou_owner",
      chatId: "chat-second",
    });

    const activeBindings = await prisma.projectGroupBinding.findMany({
      where: { projectId: project.projectId, archivedAt: null },
      select: { chatId: true },
    });
    expect(activeBindings).toEqual([{ chatId: "chat-second" }]);
    const oldChatGrant = await prisma.permissionGrant.findFirst({
      where: {
        resourceType: "PROJECT",
        resourceId: project.projectId,
        principalType: "FEISHU_CHAT",
        principalId: "chat-first",
        revokedAt: null,
      },
    });
    expect(oldChatGrant).toBeNull();
  });

  it("rejects selecting an Agent role from another Organization at the database boundary", async () => {
    await seedUser("u-owner", "ou_owner", "OWNER");
    const project = await createProjectFromOrgAdmin(prisma, {
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_owner",
      name: "Tenant-scoped role project",
      workspaceRoot: await tempWorkspaceRoot(),
    });
    await bindFeishuChatToProject(prisma, {
      projectId: project.projectId,
      actorFeishuOpenId: "ou_owner",
      chatId: "chat-tenant-role",
    });
    await seedTestOrganization("org_other", "other");
    const otherRole = await prisma.organizationAgentRole.findUniqueOrThrow({
      where: { organizationId_roleId: { organizationId: "org_other", roleId: "draft" } },
    });

    await expect(prisma.projectGroupBinding.updateMany({
      where: { projectId: project.projectId, chatId: "chat-tenant-role", archivedAt: null },
      data: { selectedAgentRoleId: otherRole.id },
    })).rejects.toThrow();
  });
});

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

async function tempWorkspaceRoot(): Promise<string> {
  const root = await mkdtemp(join(tmpdir(), "cph-onboarding-"));
  workspaceRoots.push(root);
  return root;
}

async function installPermissionSettingsFailureTrigger(delaySeconds: number): Promise<void> {
  await dropPermissionSettingsFailureTrigger();
  await prisma.$executeRawUnsafe(`
    CREATE FUNCTION cph_test_fail_permission_settings() RETURNS trigger
    LANGUAGE plpgsql AS $$
    BEGIN
      PERFORM pg_sleep(${delaySeconds});
      RAISE EXCEPTION 'forced permission settings failure';
    END;
    $$
  `);
  await prisma.$executeRawUnsafe(`
    CREATE TRIGGER cph_test_fail_permission_settings
    BEFORE INSERT ON "PermissionSettings"
    FOR EACH ROW EXECUTE FUNCTION cph_test_fail_permission_settings()
  `);
}

async function dropPermissionSettingsFailureTrigger(): Promise<void> {
  await prisma.$executeRawUnsafe(
    'DROP TRIGGER IF EXISTS cph_test_fail_permission_settings ON "PermissionSettings"',
  );
  await prisma.$executeRawUnsafe('DROP FUNCTION IF EXISTS cph_test_fail_permission_settings()');
}
