import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { Script, createContext } from "node:vm";

import { describe, expect, it, vi, afterEach } from "vitest";
import { ModuleKind, ScriptTarget, transpileModule } from "typescript";

interface CliOptions {
  brainPath: string;
  workspaceId: string;
  userId: string;
  apply: boolean;
}

interface MockDirent {
  name: string;
  isFile: () => boolean;
}

interface ScriptInternals {
  parseCliArgs: (args: string[]) => CliOptions;
  mapTaskStatus: (rawStatus: string | null) => { status: string; issue: string | null };
  mapTaskPriority: (rawPriority: string | null) => { priority: string; issue: string | null };
  mapProjectStatus: (rawStatus: string | null) => { status: string; issue: string | null };
  normalizeDomain: (rawDomain: string | null | undefined) => string | null;
  listJsonFiles: (directoryPath: string) => Promise<string[]>;
  parseTaskFile: (
    rawFile: unknown,
    sourceFile: string,
    parseIssues: string[]
  ) => {
    version: string;
    domain: string;
    sourceFile: string;
    tasks: Array<{ id: string; title: string }>;
  } | null;
  parseProjectFile: (
    rawFile: unknown,
    sourceFile: string,
    parseIssues: string[]
  ) => {
    version: string;
    sourceFile: string;
    project: { id: string; name: string };
  } | null;
  loadTaskFiles: (
    taskDirectory: string,
    parseIssues: string[],
    rootPath: string
  ) => Promise<
    Array<{
      version: string;
      domain: string;
      sourceFile: string;
      tasks: Array<{ id: string; title: string }>;
    }>
  >;
  main: () => Promise<void>;
}

interface LoaderOptions {
  argv?: string[];
  homeDirectory?: string;
  readdirImpl?: (directoryPath: string, options: unknown) => Promise<MockDirent[]>;
  readFileImpl?: (filePath: string, encoding: string) => Promise<string>;
}

interface LoaderResult {
  internals: ScriptInternals;
  readdirMock: ReturnType<typeof vi.fn>;
  readFileMock: ReturnType<typeof vi.fn>;
  prismaClientConstructor: ReturnType<typeof vi.fn>;
}

const testDir = path.dirname(fileURLToPath(import.meta.url));
const scriptPath = path.resolve(testDir, "..", "migrate-brain.ts");
const nativeRequire = createRequire(import.meta.url);

const makeDirent = (name: string, isFile: boolean): MockDirent => ({
  name,
  isFile: () => isFile,
});

function buildInstrumentedSource(source: string): string {
  const invocationMarker = "main().catch((error: unknown) => {";
  const markerIndex = source.lastIndexOf(invocationMarker);
  if (markerIndex < 0) {
    throw new Error("Could not find main invocation in migrate-brain.ts");
  }

  const sourceWithoutMain = source.slice(0, markerIndex);
  return `${sourceWithoutMain}
module.exports.__test = {
  parseCliArgs,
  mapTaskStatus,
  mapTaskPriority,
  mapProjectStatus,
  normalizeDomain,
  listJsonFiles,
  parseTaskFile,
  parseProjectFile,
  loadTaskFiles,
  main,
};
`;
}

function loadMigrateBrainModule(options: LoaderOptions = {}): LoaderResult {
  const source = readFileSync(scriptPath, "utf8");
  const instrumentedSource = buildInstrumentedSource(source);

  const transpiled = transpileModule(instrumentedSource, {
    compilerOptions: {
      module: ModuleKind.CommonJS,
      target: ScriptTarget.ES2022,
      esModuleInterop: true,
    },
    fileName: scriptPath,
  });

  const readdirMock = vi.fn(options.readdirImpl ?? (async (): Promise<MockDirent[]> => []));
  const readFileMock = vi.fn(options.readFileImpl ?? (async (): Promise<string> => "{}"));

  const prismaClientConstructor = vi.fn(() => ({
    $disconnect: vi.fn(async () => undefined),
  }));

  const processMock = Object.create(process) as NodeJS.Process;
  processMock.argv = options.argv ?? [
    "node",
    scriptPath,
    "--workspace-id",
    "workspace-1",
    "--user-id",
    "user-1",
  ];

  const prismaClientModule = {
    ActivityAction: { CREATED: "CREATED" },
    EntityType: { DOMAIN: "DOMAIN", PROJECT: "PROJECT", TASK: "TASK" },
    Prisma: {},
    PrismaClient: prismaClientConstructor,
    ProjectStatus: {
      ACTIVE: "ACTIVE",
      PLANNING: "PLANNING",
      PAUSED: "PAUSED",
      ARCHIVED: "ARCHIVED",
    },
    TaskPriority: {
      HIGH: "HIGH",
      MEDIUM: "MEDIUM",
      LOW: "LOW",
    },
    TaskStatus: {
      COMPLETED: "COMPLETED",
      IN_PROGRESS: "IN_PROGRESS",
      NOT_STARTED: "NOT_STARTED",
      PAUSED: "PAUSED",
      ARCHIVED: "ARCHIVED",
    },
  };

  const module = { exports: {} as Record<string, unknown> };

  const requireFromScript = (specifier: string): unknown => {
    if (specifier === "node:fs/promises") {
      return {
        readdir: readdirMock,
        readFile: readFileMock,
      };
    }
    if (specifier === "node:os") {
      return {
        homedir: () => options.homeDirectory ?? "/home/tester",
      };
    }
    if (specifier === "node:process") {
      return processMock;
    }
    if (specifier === "../apps/api/node_modules/@prisma/client") {
      return prismaClientModule;
    }
    return nativeRequire(specifier);
  };

  const context = createContext({
    module,
    exports: module.exports,
    require: requireFromScript,
    __dirname: path.dirname(scriptPath),
    __filename: scriptPath,
    console,
    process: processMock,
    setTimeout,
    clearTimeout,
    setInterval,
    clearInterval,
    Buffer,
  });

  new Script(transpiled.outputText, { filename: scriptPath }).runInContext(context);

  const exported = module.exports as { __test?: ScriptInternals };
  if (!exported.__test) {
    throw new Error("Failed to expose migrate-brain internals for tests");
  }

  return {
    internals: exported.__test,
    readdirMock,
    readFileMock,
    prismaClientConstructor,
  };
}

afterEach(() => {
  vi.restoreAllMocks();
});

describe("migrate-brain mapping helpers", () => {
  it("maps statuses and priorities, including unknown-value fallbacks", () => {
    const { internals } = loadMigrateBrainModule();

    expect(internals.mapTaskStatus("done")).toEqual({
      status: "COMPLETED",
      issue: null,
    });
    expect(internals.mapTaskStatus("mystery")).toEqual({
      status: "NOT_STARTED",
      issue: 'Unknown task status "mystery" mapped to NOT_STARTED',
    });

    expect(internals.mapTaskPriority("critical")).toEqual({
      priority: "HIGH",
      issue: null,
    });
    expect(internals.mapTaskPriority(null)).toEqual({
      priority: "MEDIUM",
      issue: 'Unknown task priority "null" mapped to MEDIUM',
    });

    expect(internals.mapProjectStatus("in-progress")).toEqual({
      status: "ACTIVE",
      issue: null,
    });
    expect(internals.mapProjectStatus("untracked")).toEqual({
      status: "PLANNING",
      issue: 'Unknown project status "untracked" mapped to PLANNING',
    });
  });

  it("normalizes domain strings into lowercase slugs", () => {
    const { internals } = loadMigrateBrainModule();

    expect(internals.normalizeDomain(" Platform Core ")).toBe("platform-core");
    expect(internals.normalizeDomain("###")).toBeNull();
    expect(internals.normalizeDomain(undefined)).toBeNull();
  });
});

describe("migrate-brain CLI parsing", () => {
  it("parses required arguments and default brain path", () => {
    const { internals } = loadMigrateBrainModule({ homeDirectory: "/opt/home" });

    const parsed = internals.parseCliArgs([
      "--workspace-id",
      "workspace-abc",
      "--user-id",
      "user-xyz",
    ]);

    expect(parsed).toEqual({
      brainPath: path.resolve("/opt/home/src/jarvis-brain"),
      workspaceId: "workspace-abc",
      userId: "user-xyz",
      apply: false,
    });
  });

  it("supports inline flags and apply mode", () => {
    const { internals } = loadMigrateBrainModule({ homeDirectory: "/opt/home" });

    const parsed = internals.parseCliArgs([
      "--brain-path=~/custom-brain",
      "--workspace-id=workspace-1",
      "--user-id=user-1",
      "--apply",
    ]);

    expect(parsed).toEqual({
      brainPath: path.resolve("/opt/home/custom-brain"),
      workspaceId: "workspace-1",
      userId: "user-1",
      apply: true,
    });
  });

  it("throws on missing required flags and unknown flags", () => {
    const { internals } = loadMigrateBrainModule();

    expect(() => internals.parseCliArgs(["--workspace-id", "workspace-1"])).toThrowError(
      "Both --workspace-id and --user-id are required"
    );

    expect(() =>
      internals.parseCliArgs(["--workspace-id", "workspace-1", "--user-id", "user-1", "--nope"])
    ).toThrowError("Unknown flag: --nope");
  });
});

describe("migrate-brain file discovery", () => {
  it("returns only .json files in sorted order", async () => {
    const readdirImpl = async (): Promise<MockDirent[]> => [
      makeDirent("z.json", true),
      makeDirent("notes.md", true),
      makeDirent("nested", false),
      makeDirent("a.json", true),
    ];

    const { internals, readdirMock } = loadMigrateBrainModule({ readdirImpl });

    const files = await internals.listJsonFiles("/tmp/brain/data/tasks");

    expect(files).toEqual([
      path.join("/tmp/brain/data/tasks", "a.json"),
      path.join("/tmp/brain/data/tasks", "z.json"),
    ]);
    expect(readdirMock).toHaveBeenCalledWith("/tmp/brain/data/tasks", {
      withFileTypes: true,
    });
  });
});

describe("migrate-brain parsing and validation", () => {
  it("loads task files and tracks validation issues for invalid task records", async () => {
    const taskDirectory = "/tmp/brain/data/tasks";
    const taskFilePath = path.join(taskDirectory, "core.json");

    const readdirImpl = async (): Promise<MockDirent[]> => [makeDirent("core.json", true)];
    const readFileImpl = async (filePath: string): Promise<string> => {
      if (filePath === taskFilePath) {
        return JSON.stringify({
          version: "1",
          domain: "core",
          tasks: [{ id: "TASK-1", title: "Valid" }, { id: "TASK-2" }],
        });
      }

      throw new Error(`Unexpected file read: ${filePath}`);
    };

    const { internals, readFileMock } = loadMigrateBrainModule({
      readdirImpl,
      readFileImpl,
    });

    const parseIssues: string[] = [];
    const loaded = await internals.loadTaskFiles(taskDirectory, parseIssues, "/tmp/brain");

    expect(loaded).toHaveLength(1);
    expect(loaded[0]).toMatchObject({
      version: "1",
      domain: "core",
      sourceFile: path.join("data", "tasks", "core.json"),
    });
    expect(loaded[0].tasks).toHaveLength(1);
    expect(parseIssues).toContain(
      'data/tasks/core.json task[1]: required field "title" missing or invalid'
    );
    expect(readFileMock).toHaveBeenCalledWith(taskFilePath, "utf8");
  });

  it("rejects invalid project payloads", () => {
    const { internals } = loadMigrateBrainModule();

    const parseIssues: string[] = [];
    const parsed = internals.parseProjectFile(
      {
        version: "1",
        project: {
          name: "Project without ID",
        },
      },
      "data/projects/project.json",
      parseIssues
    );

    expect(parsed).toBeNull();
    expect(parseIssues).toContain(
      'data/projects/project.json project: required field "id" missing or invalid'
    );
  });
});

describe("migrate-brain dry-run behavior", () => {
  it("does not instantiate Prisma client when --apply is omitted", async () => {
    const brainPath = "/tmp/brain";
    const readdirImpl = async (): Promise<MockDirent[]> => [];

    const { internals, prismaClientConstructor, readdirMock } = loadMigrateBrainModule({
      argv: [
        "node",
        scriptPath,
        "--brain-path",
        brainPath,
        "--workspace-id",
        "workspace-1",
        "--user-id",
        "user-1",
      ],
      readdirImpl,
    });

    const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);

    await internals.main();

    expect(prismaClientConstructor).not.toHaveBeenCalled();
    expect(readdirMock).toHaveBeenCalledWith(path.join(brainPath, "data", "tasks"), {
      withFileTypes: true,
    });
    expect(readdirMock).toHaveBeenCalledWith(path.join(brainPath, "data", "projects"), {
      withFileTypes: true,
    });
    expect(logSpy).toHaveBeenCalledWith("Dry-run complete. Re-run with --apply to write records.");
  });
});
