import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
  collectCurrentSuppressions,
  diffBaseline,
  findBaselineExpansion,
  hasAllRuleDisable,
  hasMaxLinesDisable,
  isGovernedSourcePath,
  main,
  parseBaseline,
} from "../../scripts/check-max-lines-ratchet.mts";

const tempDirs: string[] = [];
const nestedGitEnvKeys = [
  "GIT_ALTERNATE_OBJECT_DIRECTORIES",
  "GIT_COMMON_DIR",
  "GIT_CONFIG",
  "GIT_CONFIG_COUNT",
  "GIT_CONFIG_PARAMETERS",
  "GIT_DIR",
  "GIT_GRAFT_FILE",
  "GIT_IMPLICIT_WORK_TREE",
  "GIT_INDEX_FILE",
  "GIT_NAMESPACE",
  "GIT_NO_REPLACE_OBJECTS",
  "GIT_OBJECT_DIRECTORY",
  "GIT_PREFIX",
  "GIT_QUARANTINE_PATH",
  "GIT_REPLACE_REF_BASE",
  "GIT_SHALLOW_FILE",
  "GIT_WORK_TREE",
] as const;

function git(cwd: string, args: string[]): void {
  const env: NodeJS.ProcessEnv = {
    ...process.env,
    GIT_CONFIG_NOSYSTEM: "1",
    GIT_TERMINAL_PROMPT: "0",
  };
  for (const key of nestedGitEnvKeys) {
    delete env[key];
  }
  execFileSync("git", args, { cwd, env, stdio: "ignore" });
}

afterEach(() => {
  vi.restoreAllMocks();
  for (const dir of tempDirs.splice(0)) {
    fs.rmSync(dir, { force: true, recursive: true });
  }
});

describe("check-max-lines-ratchet", () => {
  it("recognizes suppressions without matching reason prose", () => {
    expect(hasMaxLinesDisable("/* oxlint-disable max-lines -- TODO: split. */\n")).toBe(true);
    expect(hasMaxLinesDisable("// eslint-disable-next-line no-console, max-lines\n")).toBe(true);
    expect(hasMaxLinesDisable("/* oxlint-disable */\n")).toBe(false);
    expect(hasMaxLinesDisable("// oxlint-disable-line -- all rules\n")).toBe(false);
    expect(hasMaxLinesDisable("/* oxlint-disable max-lines - TODO: split. */\n")).toBe(true);
    expect(hasMaxLinesDisable("/* oxlint-disable max-lines--temporary */\n")).toBe(true);
    expect(hasMaxLinesDisable("/* oxlint-disable - all rules */\n")).toBe(false);
    expect(hasMaxLinesDisable("/* oxlint-disable eslint/max-lines */\n")).toBe(true);
    expect(hasMaxLinesDisable("/* oxlint-disable\nmax-lines\n-- TODO: split. */\n")).toBe(true);
    expect(
      hasMaxLinesDisable(
        "export const value = 1;\n/* oxlint-disable max-lines -- TODO: split. */\n",
      ),
    ).toBe(true);
    expect(
      hasMaxLinesDisable("if (true) {\n  const value = 1;\n  /* oxlint-disable max-lines */\n}\n"),
    ).toBe(true);
    expect(hasMaxLinesDisable("/* oxlint-disable no-console -- mentions max-lines */\n")).toBe(
      false,
    );
    expect(hasMaxLinesDisable("// Example: oxlint-disable max-lines\n")).toBe(false);
    expect(hasMaxLinesDisable('const example = "/* oxlint-disable max-lines */";\n')).toBe(false);
    expect(hasAllRuleDisable("/* oxlint-disable */\n")).toBe(true);
    expect(hasAllRuleDisable("// oxlint-disable-line -- all rules\n")).toBe(true);
    expect(hasAllRuleDisable("/* oxlint-disable max-lines */\n")).toBe(false);
  });

  it("limits source roots and excludes generated output", () => {
    expect(isGovernedSourcePath("src/runtime.ts")).toBe(true);
    expect(isGovernedSourcePath("extensions/demo/index.mjs")).toBe(true);
    expect(isGovernedSourcePath("scripts/tool.mjs")).toBe(false);
    expect(isGovernedSourcePath("packages/api/protocol-gen/types.ts")).toBe(false);
    expect(isGovernedSourcePath("ui/src/i18n/locales/en.ts")).toBe(false);
    expect(isGovernedSourcePath("src/wizard/i18n/locales/en.ts")).toBe(false);
    expect(isGovernedSourcePath("src/schema.generated.ts")).toBe(false);
  });

  it("reports new suppressions, stale debt, and baseline growth", () => {
    const baseline = parseBaseline("# debt\nsrc/a.ts\nsrc/b.ts\n");
    expect(diffBaseline(["src/b.ts", "src/c.ts"], baseline)).toEqual({
      added: ["src/c.ts"],
      stale: ["src/a.ts"],
    });
    expect(findBaselineExpansion(baseline, new Set(["src/a.ts"]))).toEqual(["src/b.ts"]);
  });

  it("rejects baseline growth even when the new suppression is listed", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(
      path.join(root, "src/a.ts"),
      "/* oxlint-disable max-lines -- TODO: split. */\n",
    );
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }

    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\nsrc/b.ts\n");
    fs.writeFileSync(
      path.join(root, "src/b.ts"),
      "/* oxlint-disable max-lines -- TODO: split. */\n",
    );
    git(root, ["add", "."]);
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root, ["--base", "HEAD"])).toBe(1);
  });

  it("rejects replacing an explicit max-lines suppression with an all-rule disable", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-all-rule-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }

    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable */\n");
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root, ["--base", "HEAD"])).toBe(1);
  });

  it("rejects a new all-rule disable without baseline growth", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-all-rule-new-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "");
    fs.writeFileSync(path.join(root, "src/a.ts"), "export const a = 1;\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }

    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable */\n");
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root, ["--base", "HEAD"])).toBe(1);
  });

  it("transfers grandfathered debt across a verified rename", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-rename-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(
      path.join(root, "src/a.ts"),
      "export const a = 1;\n/* oxlint-disable max-lines -- TODO: split. */\n",
    );
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }
    git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);

    git(root, ["mv", "src/a.ts", "src/b.ts"]);
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/b.ts\n");

    expect(main(root)).toBe(0);
  });

  it("defaults worktree comparisons to origin/main", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-default-base-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }
    git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);

    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\nsrc/b.ts\n");
    fs.writeFileSync(path.join(root, "src/b.ts"), "/* oxlint-disable max-lines */\n");
    git(root, ["add", "."]);
    git(root, ["commit", "-m", "grow baseline"]);
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root)).toBe(1);
  });

  it("compares divergent worktrees against their main merge base", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-diverged-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\nsrc/b.ts\n");
    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n");
    fs.writeFileSync(path.join(root, "src/b.ts"), "/* oxlint-disable max-lines */\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
      ["branch", "release"],
    ]) {
      git(root, args);
    }

    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(path.join(root, "src/b.ts"), "export const b = 1;\n");
    git(root, ["add", "."]);
    git(root, ["commit", "-m", "shrink main debt"]);
    git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    git(root, ["checkout", "release"]);

    expect(main(root)).toBe(0);
  });

  it("falls back to main when no merge base is available", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-disconnected-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable max-lines */\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "release"],
      ["branch", "-m", "release"],
    ]) {
      git(root, args);
    }
    git(root, ["checkout", "--orphan", "main"]);
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "");
    fs.writeFileSync(path.join(root, "src/a.ts"), "export const a = 1;\n");
    git(root, ["add", "."]);
    git(root, ["commit", "-m", "disconnected main"]);
    git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    git(root, ["checkout", "release"]);

    expect(main(root)).toBe(1);
  });

  it("checks staged content instead of unstaged worktree edits", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-staged-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "");
    fs.writeFileSync(path.join(root, "src/a.ts"), "export const a = 1;\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }

    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "src/a.ts\n");
    fs.writeFileSync(path.join(root, "src/a.ts"), "/* oxlint-disable */\n");
    git(root, ["add", "."]);
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "");
    fs.writeFileSync(path.join(root, "src/a.ts"), "export const a = 1;\n");
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root, ["--staged", "--base", "HEAD"])).toBe(1);
  });

  it.skipIf(process.platform === "win32")("keeps staged filenames NUL-framed", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-nul-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    git(root, ["init"]);
    const filePath = "src/newline\nname.ts";
    fs.writeFileSync(path.join(root, filePath), "/* oxlint-disable max-lines */\n");
    git(root, ["add", "."]);

    expect(collectCurrentSuppressions(root, { staged: true })).toEqual([filePath]);
  });

  it("checks untracked sources and tolerates unstaged deletions", () => {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-max-lines-worktree-"));
    tempDirs.push(root);
    fs.mkdirSync(path.join(root, "config"), { recursive: true });
    fs.mkdirSync(path.join(root, "src"), { recursive: true });
    fs.writeFileSync(path.join(root, "config/max-lines-baseline.txt"), "");
    fs.writeFileSync(path.join(root, "src/deleted.ts"), "export const deleted = true;\n");
    for (const args of [
      ["init"],
      ["config", "user.email", "test@example.com"],
      ["config", "user.name", "Test"],
      ["add", "."],
      ["commit", "-m", "base"],
    ]) {
      git(root, args);
    }
    git(root, ["update-ref", "refs/remotes/origin/main", "HEAD"]);
    fs.rmSync(path.join(root, "src/deleted.ts"));
    expect(main(root)).toBe(0);

    fs.writeFileSync(path.join(root, "src/untracked.ts"), "/* oxlint-disable max-lines */\n");
    vi.spyOn(console, "error").mockImplementation(() => {});

    expect(main(root)).toBe(1);
  });
});
