import { mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
import { importLegacyProjects } from "../../src/deployment/legacyProjectImport.js";
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";

const temporaryRoots: string[] = [];

describe("legacy teaching-material project import", () => {
  beforeEach(async () => {
    await resetDb();
    await prisma.user.create({
      data: {
        id: "legacy-import-owner",
        feishuOpenId: "ou_legacy_owner",
        displayName: "Legacy Import Owner",
        organizationMemberships: { create: { organizationId: DEFAULT_ORG_ID, role: "OWNER" } },
      },
    });
  });

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

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

  it("imports each legacy project as an unbound resumable project under its old folder path", async () => {
    const sourceRoot = await temporaryRoot("cph-legacy-source-");
    const workspaceRoot = await temporaryRoot("cph-legacy-target-");
    const projectSource = join(sourceRoot, "物理", "M-243-牛顿力学");
    await mkdir(join(projectSource, "workspace", "chapters"), { recursive: true });
    await mkdir(join(projectSource, "workspace", ".claude"), { recursive: true });
    await mkdir(join(projectSource, "workspace", "chapters", ".cph"), { recursive: true });
    await mkdir(join(projectSource, "_raw"), { recursive: true });
    await writeFile(join(projectSource, "workspace", "project.toml"), "title = \"牛顿力学\"\n");
    await writeFile(join(projectSource, "workspace", "chapters", "lesson.typ"), "= 牛顿第二定律\n");
    await writeFile(join(projectSource, "workspace", ".claude", "session.json"), "{}\n");
    await writeFile(join(projectSource, "workspace", "chapters", ".cph", "runtime.json"), "{}\n");
    await writeFile(join(projectSource, "project.json"), "{\"id\":\"M-243\"}\n");
    await writeFile(join(projectSource, "_raw", "source.txt"), "legacy source\n");
    const stateFile = join(workspaceRoot, "migration-state", "state.json");
    const manifest = [{
      legacyId: "M-243",
      name: "牛顿力学",
      folderPath: ["物理"],
      sourceRelativePath: "物理/M-243-牛顿力学",
    }];

    const first = await importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot,
      sourceRoot,
      stateFile,
      projects: manifest,
    });
    const imported = first.projects["M-243"];
    expect(imported).toBeDefined();
    if (imported === undefined) throw new Error("missing imported project state");

    const project = await prisma.project.findUniqueOrThrow({
      where: { id: imported.projectId },
      include: { folder: { include: { parent: true } }, groupBindings: true },
    });
    expect(project.name).toBe("牛顿力学");
    expect(project.folder?.name).toBe("物理");
    expect(project.folder?.parent?.name).toBe("旧教学资产");
    expect(project.groupBindings).toEqual([]);
    await expect(readFile(join(imported.workspaceDir, "chapters", "lesson.typ"), "utf8"))
      .resolves.toBe("= 牛顿第二定律\n");
    await expect(readFile(join(imported.workspaceDir, ".claude", "session.json"), "utf8"))
      .rejects.toMatchObject({ code: "ENOENT" });
    await expect(readFile(join(imported.workspaceDir, "chapters", ".cph", "runtime.json"), "utf8"))
      .rejects.toMatchObject({ code: "ENOENT" });
    await expect(readFile(join(imported.workspaceDir, ".legacy-source", "project.json"), "utf8"))
      .resolves.toContain("M-243");
    await expect(readFile(join(imported.workspaceDir, ".legacy-source", "raw", "source.txt"), "utf8"))
      .resolves.toBe("legacy source\n");

    await unlink(stateFile);
    const second = await importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot,
      sourceRoot,
      stateFile,
      projects: manifest,
    });
    expect(second.projects["M-243"]?.projectId).toBe(imported.projectId);
    await expect(prisma.project.count({ where: { organizationId: DEFAULT_ORG_ID } })).resolves.toBe(1);
    expect(JSON.parse(await readFile(stateFile, "utf8"))).toMatchObject({
      version: 1,
      projects: { "M-243": { projectId: imported.projectId } },
    });
    await expect(prisma.auditEntry.count({
      where: { projectId: imported.projectId, action: "legacy_project.imported" },
    })).resolves.toBe(1);

    await writeFile(join(imported.workspaceDir, ".legacy-source", "migration.json"), "not json\n");
    await expect(importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot,
      sourceRoot,
      stateFile,
      projects: manifest,
    })).rejects.toThrow(/invalid legacy completion marker/);
    await expect(prisma.project.count({ where: { id: imported.projectId } })).resolves.toBe(1);
  });

  it("rejects symlinks instead of importing paths outside the staged project", async () => {
    const sourceRoot = await temporaryRoot("cph-legacy-symlink-");
    const workspaceRoot = await temporaryRoot("cph-legacy-target-");
    const projectSource = join(sourceRoot, "legacy");
    await mkdir(join(projectSource, "workspace"), { recursive: true });
    await writeFile(join(projectSource, "project.json"), "{}\n");
    await symlink("/etc/passwd", join(projectSource, "workspace", "outside"));

    await expect(importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot,
      sourceRoot,
      stateFile: join(workspaceRoot, "state.json"),
      projects: [{ legacyId: "symlink", name: "Symlink", folderPath: [], sourceRelativePath: "legacy" }],
    })).rejects.toThrow(/rejects symbolic link/);
    await expect(prisma.project.count()).resolves.toBe(0);
  });

  it("rejects a manifest path that escapes the staged source root", async () => {
    const parent = await temporaryRoot("cph-legacy-escape-");
    const sourceRoot = join(parent, "source");
    const outside = join(parent, "outside");
    await mkdir(sourceRoot);
    await mkdir(outside);

    await expect(importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot: await temporaryRoot("cph-legacy-target-"),
      sourceRoot,
      stateFile: join(parent, "state.json"),
      projects: [{
        legacyId: "escape",
        name: "Escape",
        folderPath: [],
        sourceRelativePath: "../outside",
      }],
    })).rejects.toThrow(/escapes source root/);
  });

  it("rejects malformed resume state before creating a project", async () => {
    const sourceRoot = await temporaryRoot("cph-legacy-state-source-");
    const workspaceRoot = await temporaryRoot("cph-legacy-state-target-");
    const stateFile = join(workspaceRoot, "state.json");
    await writeFile(stateFile, JSON.stringify({ version: 1, projects: { broken: { status: "MAYBE" } } }));

    await expect(importLegacyProjects({
      prisma,
      organizationId: DEFAULT_ORG_ID,
      actorFeishuOpenId: "ou_legacy_owner",
      workspaceRoot,
      sourceRoot,
      stateFile,
      projects: [],
    })).rejects.toThrow(/invalid legacy import state fields/);
    await expect(prisma.project.count()).resolves.toBe(0);
  });
});

async function temporaryRoot(prefix: string): Promise<string> {
  const root = await mkdtemp(join(tmpdir(), prefix));
  temporaryRoots.push(root);
  return root;
}
