import { afterAll, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
  applyCuratedFrontmatter,
  extractCommentMetadata,
  extractDescriptionFromComments,
  extractPackageMetadata,
  extractTagsFromPath,
  fileNameToDescription,
  generateMetadata,
  isEnrichmentComplete,
  loadStashFile,
  type StashEntry,
  type StashFile,
  validateStashEntry,
  writeStashFile,
} from "../src/indexer/passes/metadata";

// Renderers auto-register via ensureBuiltinsRegistered in file-context.ts

const createdTmpDirs: string[] = [];

function tmpDir(): string {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-meta-"));
  createdTmpDirs.push(dir);
  return dir;
}

afterAll(() => {
  for (const dir of createdTmpDirs) {
    fs.rmSync(dir, { recursive: true, force: true });
  }
});

function writeFile(filePath: string, content = "") {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, content);
}

// ── loadStashFile ───────────────────────────────────────────────────────────

test("loadStashFile reads valid .stash.json", () => {
  const dir = tmpDir();
  const stash: StashFile = {
    entries: [
      {
        name: "docker-build",
        type: "script",
        description: "build docker images",
        tags: ["docker", "build"],
        filename: "docker-build.ts",
      },
    ],
  };
  writeFile(path.join(dir, ".stash.json"), JSON.stringify(stash));

  const result = loadStashFile(dir);
  expect(result).not.toBeNull();
  expect(result?.entries).toHaveLength(1);
  expect(result?.entries[0].name).toBe("docker-build");
  expect(result?.entries[0].description).toBe("build docker images");
  expect(result?.entries[0].tags).toEqual(["docker", "build"]);
});

test("loadStashFile returns null for missing file", () => {
  const dir = tmpDir();
  expect(loadStashFile(dir)).toBeNull();
});

test("loadStashFile returns null for invalid JSON", () => {
  const dir = tmpDir();
  writeFile(path.join(dir, ".stash.json"), "not json");
  expect(loadStashFile(dir)).toBeNull();
});

test("loadStashFile returns null for missing entries array", () => {
  const dir = tmpDir();
  writeFile(path.join(dir, ".stash.json"), '{"foo": "bar"}');
  expect(loadStashFile(dir)).toBeNull();
});

test("loadStashFile parses intent field", () => {
  const dir = tmpDir();
  const stash: StashFile = {
    entries: [
      {
        name: "deploy",
        type: "script",
        intent: { when: "user needs to deploy", input: "service name", output: "deployment status" },
        filename: "deploy.sh",
      },
    ],
  };
  writeFile(path.join(dir, ".stash.json"), JSON.stringify(stash));

  const result = loadStashFile(dir);
  expect(result?.entries[0].intent).toEqual({
    when: "user needs to deploy",
    input: "service name",
    output: "deployment status",
  });
});

// ── writeStashFile ──────────────────────────────────────────────────────────

test("writeStashFile persists .stash.json to disk", () => {
  const dir = tmpDir();
  const stash: StashFile = {
    entries: [{ name: "test", type: "script", quality: "generated" }],
  };
  writeStashFile(dir, stash);

  const raw = fs.readFileSync(path.join(dir, ".stash.json"), "utf8");
  const parsed = JSON.parse(raw);
  expect(parsed.entries[0].name).toBe("test");
  expect(parsed.entries[0].quality).toBe("generated");
});

// ── validateStashEntry ──────────────────────────────────────────────────────

test("validateStashEntry rejects entries without name", () => {
  expect(validateStashEntry({ type: "script" })).toBeNull();
});

test("validateStashEntry rejects entries without valid type", () => {
  expect(validateStashEntry({ name: "x", type: "invalid" })).toBeNull();
});

test("validateStashEntry accepts minimal valid entry", () => {
  const result = validateStashEntry({ name: "x", type: "script" });
  expect(result).not.toBeNull();
  expect(result?.name).toBe("x");
  expect(result?.type).toBe("script");
});

test("validateStashEntry parses quality, confidence, source, and aliases", () => {
  const result = validateStashEntry({
    name: "lint",
    type: "script",
    quality: "curated",
    confidence: 2,
    source: "manual",
    aliases: ["Lint", "linters"],
  });

  expect(result).not.toBeNull();
  expect(result?.quality).toBe("curated");
  expect(result?.confidence).toBe(1);
  expect(result?.source).toBe("manual");
  // R4.6: de-pluralization heuristic removed; FTS5 porter stemmer handles stemming.
  // "linters" is preserved as-is; "linter" is no longer generated.
  expect(result?.aliases).toEqual(["lint", "linters"]);
});

// ── extractDescriptionFromComments ──────────────────────────────────────────

test("extractDescriptionFromComments parses JSDoc block comment", () => {
  const dir = tmpDir();
  const file = path.join(dir, "tool.ts");
  writeFile(file, `/**\n * Generate docker compose stacks\n */\nconsole.log("hi")\n`);

  const desc = extractDescriptionFromComments(file);
  expect(desc).toBe("Generate docker compose stacks");
});

test("extractDescriptionFromComments parses hash comments after shebang", () => {
  const dir = tmpDir();
  const file = path.join(dir, "tool.sh");
  writeFile(file, `#!/usr/bin/env bash\n# Deploy to production\n# Handles rollback\necho deploy\n`);

  const desc = extractDescriptionFromComments(file);
  expect(desc).toBe("Deploy to production Handles rollback");
});

test("extractDescriptionFromComments returns null for no comments", () => {
  const dir = tmpDir();
  const file = path.join(dir, "tool.ts");
  writeFile(file, `console.log("no comments")\n`);

  expect(extractDescriptionFromComments(file)).toBeNull();
});

// ── extractPackageMetadata ──────────────────────────────────────────────────

test("extractPackageMetadata reads package.json fields", () => {
  const dir = tmpDir();
  writeFile(
    path.join(dir, "package.json"),
    JSON.stringify({ name: "my-tool", description: "A useful tool", keywords: ["deploy", "ci"] }),
  );

  const meta = extractPackageMetadata(dir);
  expect(meta).not.toBeNull();
  expect(meta?.name).toBe("my-tool");
  expect(meta?.description).toBe("A useful tool");
  expect(meta?.keywords).toEqual(["deploy", "ci"]);
});

test("extractPackageMetadata returns null when no package.json", () => {
  const dir = tmpDir();
  expect(extractPackageMetadata(dir)).toBeNull();
});

// ── fileNameToDescription ───────────────────────────────────────────────────

test("fileNameToDescription converts dashes and underscores to spaces", () => {
  expect(fileNameToDescription("docker-compose-generator")).toBe("docker compose generator");
  expect(fileNameToDescription("my_script_tool")).toBe("my script tool");
});

test("fileNameToDescription handles camelCase", () => {
  expect(fileNameToDescription("dockerBuild")).toBe("docker build");
});

// ── extractTagsFromPath ─────────────────────────────────────────────────────

test("extractTagsFromPath extracts tokens from path segments", () => {
  const root = "/stash/scripts";
  const file = path.join(root, "docker", "compose-generator.ts");
  const tags = extractTagsFromPath(file, root);
  expect(tags).toContain("docker");
  expect(tags).toContain("compose");
  expect(tags).toContain("generator");
});

// ── generateMetadata ────────────────────────────────────────────────────────

test("generateMetadata creates entries from script files with filename heuristics", async () => {
  const dir = tmpDir();
  const tool1 = path.join(dir, "summarize-diff.ts");
  writeFile(tool1, `console.log("summarize")\n`);

  const stash = await generateMetadata(dir, "script", [tool1]);
  expect(stash.entries).toHaveLength(1);
  expect(stash.entries[0].name).toBe("summarize-diff.ts");
  expect(stash.entries[0].type).toBe("script");
  expect(stash.entries[0].description).toBe("summarize diff");
  expect(stash.entries[0].quality).toBe("generated");
  expect(stash.entries[0].source).toBe("filename");
  expect(stash.entries[0].confidence).toBe(0.55);
  expect(stash.entries[0].aliases).toContain("summarize diff");
  expect(stash.entries[0].filename).toBe("summarize-diff.ts");
});

test("generateMetadata extracts description from code comments", async () => {
  const dir = tmpDir();
  const tool1 = path.join(dir, "deploy.sh");
  writeFile(tool1, `#!/usr/bin/env bash\n# Deploy services to production\necho deploy\n`);

  const stash = await generateMetadata(dir, "script", [tool1]);
  expect(stash.entries[0].description).toBe("Deploy services to production");
  expect(stash.entries[0].source).toBe("comments");
});

test("generateMetadata extracts metadata from package.json", async () => {
  const dir = tmpDir();
  const tool1 = path.join(dir, "run.ts");
  writeFile(tool1, `console.log("run")\n`);
  writeFile(
    path.join(dir, "package.json"),
    JSON.stringify({ description: "Git diff summarizer", keywords: ["git", "diff"] }),
  );

  const stash = await generateMetadata(dir, "script", [tool1]);
  expect(stash.entries[0].description).toBe("Git diff summarizer");
  expect(stash.entries[0].source).toBe("package");
  expect(stash.entries[0].confidence).toBe(0.8);
  expect(stash.entries[0].tags).toEqual(["git", "diff"]);
});

test("generateMetadata skips non-script extensions for script type", async () => {
  const dir = tmpDir();
  const mdFile = path.join(dir, "README.md");
  writeFile(mdFile, "# Readme\n");

  const stash = await generateMetadata(dir, "script", [mdFile]);
  expect(stash.entries).toHaveLength(0);
});

test("generateMetadata handles multi-script directories", async () => {
  const dir = tmpDir();
  const tool1 = path.join(dir, "docker-build.ts");
  const tool2 = path.join(dir, "docker-compose.ts");
  writeFile(tool1, `/**\n * Build docker images\n */\n`);
  writeFile(tool2, `/**\n * Generate docker compose stacks\n */\n`);

  const stash = await generateMetadata(dir, "script", [tool1, tool2]);
  expect(stash.entries).toHaveLength(2);
  expect(stash.entries[0].name).toBe("docker-build.ts");
  expect(stash.entries[0].description).toBe("Build docker images");
  expect(stash.entries[1].name).toBe("docker-compose.ts");
  expect(stash.entries[1].description).toBe("Generate docker compose stacks");
});

// ── validateStashEntry with searchHints ─────────────────────────────────────────

test("validateStashEntry accepts entries with searchHints array", () => {
  const result = validateStashEntry({
    name: "test",
    type: "script",
    searchHints: ["summarize commits", "explain changes"],
  });
  expect(result).not.toBeNull();
  expect(result?.searchHints).toEqual(["summarize commits", "explain changes"]);
});

test("validateStashEntry filters non-string elements from searchHints", () => {
  const result = validateStashEntry({
    name: "test",
    type: "script",
    searchHints: ["valid", 42, "", "also valid", null],
  });
  expect(result).not.toBeNull();
  expect(result?.searchHints).toEqual(["valid", "also valid"]);
});

test("validateStashEntry omits searchHints if all filtered out", () => {
  const result = validateStashEntry({
    name: "test",
    type: "script",
    searchHints: ["", "  "],
  });
  expect(result).not.toBeNull();
  expect(result?.searchHints).toBeUndefined();
});

test("validateStashEntry accepts usage as string", () => {
  const result = validateStashEntry({
    name: "test",
    type: "script",
    usage: "Run after checking branch state",
  });
  expect(result).not.toBeNull();
  expect(result?.usage).toEqual(["Run after checking branch state"]);
});

test("validateStashEntry normalizes usage array", () => {
  const result = validateStashEntry({
    name: "test",
    type: "script",
    usage: ["  First step  ", "", "Second step", 2, null],
  });
  expect(result).not.toBeNull();
  expect(result?.usage).toEqual(["First step", "Second step"]);
});

test("loadStashFile parses usage field", () => {
  const dir = tmpDir();
  const stash: StashFile = {
    entries: [
      {
        name: "git-diff",
        type: "script",
        usage: ["Run after fetching main", "Use --stat for quick output"],
        filename: "run.ts",
      },
    ],
  };
  writeFile(path.join(dir, ".stash.json"), JSON.stringify(stash));

  const result = loadStashFile(dir);
  expect(result?.entries[0].usage).toEqual(["Run after fetching main", "Use --stat for quick output"]);
});

test("loadStashFile parses searchHints field", () => {
  const dir = tmpDir();
  const stash: StashFile = {
    entries: [
      {
        name: "git-diff",
        type: "script",
        searchHints: ["summarize git commits", "explain what changed"],
        filename: "run.ts",
      },
    ],
  };
  writeFile(path.join(dir, ".stash.json"), JSON.stringify(stash));

  const result = loadStashFile(dir);
  expect(result?.entries[0].searchHints).toEqual(["summarize git commits", "explain what changed"]);
});

// ── generateMetadata populates searchHints ──────────────────────────────────────

test("generateMetadata does not generate heuristic searchHints (LLM-only)", async () => {
  const dir = tmpDir();
  const tool = path.join(dir, "summarize-diff.ts");
  writeFile(tool, `/**\n * Summarize git diff changes\n */\n`);

  const stash = await generateMetadata(dir, "script", [tool]);
  // Search hints are only generated when LLM is configured, not heuristically
  expect(stash.entries[0].searchHints).toBeUndefined();
});

test("extractCommentMetadata parses curated header tags from scripts", () => {
  const dir = tmpDir();
  const file = path.join(dir, "deploy.sh");
  writeFile(
    file,
    [
      "#!/usr/bin/env bash",
      "# @description Deploy service to production",
      "# @tags deploy, production, ops",
      "# @aliases release-service, push-live",
      "# @searchHints deploy service, release rollout",
      "# @usage Run after validating the release branch",
      "# @usage Use with a service slug",
      "# @intent.when user needs to roll out a service",
      "# @intent.input service slug",
      "# @intent.output deployment status",
      "# @run bash deploy.sh $1",
      "# @setup bun install",
      "# @cwd scripts/deploy",
      "# @scope agent=opencode, run=release",
      "echo deploy",
    ].join("\n"),
  );

  const metadata = extractCommentMetadata(file);
  expect(metadata).toEqual({
    description: "Deploy service to production",
    tags: ["deploy", "production", "ops"],
    aliases: ["release-service", "push-live"],
    searchHints: ["deploy service", "release rollout"],
    usage: ["Run after validating the release branch", "Use with a service slug"],
    intent: {
      when: "user needs to roll out a service",
      input: "service slug",
      output: "deployment status",
    },
    run: "bash deploy.sh $1",
    setup: "bun install",
    cwd: "scripts/deploy",
    scope: { agent: "opencode", run: "release" },
  });
});

test("generateMetadata applies curated frontmatter fields for markdown assets", async () => {
  const dir = tmpDir();
  const file = path.join(dir, "deploy.md");
  writeFile(
    file,
    [
      "---",
      "description: Deploy a service safely",
      "tags:",
      "  - deploy",
      "  - production",
      "aliases:",
      "  - release service",
      "searchHints:",
      "  - deploy rollout",
      "  - ship service",
      "usage:",
      "  - Use after approvals complete",
      "examples:",
      "  - Deploy api to prod",
      "run: akm run deploy",
      "setup: bun install",
      "cwd: tools/release",
      "intent:",
      "  when: user needs to deploy",
      "  input: service name",
      "  output: deployment status",
      "scope:",
      "  user: alice",
      "  agent: opencode",
      "---",
      "Deploy $1",
    ].join("\n"),
  );

  const stash = await generateMetadata(dir, "command", [file]);
  expect(stash.entries).toHaveLength(1);
  expect(stash.entries[0]).toMatchObject({
    description: "Deploy a service safely",
    tags: ["deploy", "production"],
    searchHints: ["deploy rollout", "ship service"],
    usage: ["Use after approvals complete"],
    examples: ["Deploy api to prod"],
    run: "akm run deploy",
    setup: "bun install",
    cwd: "tools/release",
    intent: {
      when: "user needs to deploy",
      input: "service name",
      output: "deployment status",
    },
    scope: { user: "alice", agent: "opencode" },
    source: "frontmatter",
  });
  expect(stash.entries[0].aliases).toEqual(expect.arrayContaining(["release service", "deploy production"]));
});

test("generateMetadata preserves curated aliases from comment metadata", async () => {
  const dir = tmpDir();
  const file = path.join(dir, "deploy-service.sh");
  writeFile(file, ["#!/usr/bin/env bash", "# @aliases release workflow, ship service", "echo deploy"].join("\n"));

  const stash = await generateMetadata(dir, "script", [file]);
  expect(stash.entries[0].aliases).toEqual(
    expect.arrayContaining(["release workflow", "ship service", "deploy service"]),
  );
});

// ── isEnrichmentComplete ────────────────────────────────────────────────────

test("isEnrichmentComplete returns true when description, tags, and searchHints are all populated", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "Deploy services to production",
    tags: ["deploy", "production"],
    searchHints: ["deploy a service to production", "roll out new code"],
  };
  expect(isEnrichmentComplete(entry)).toBe(true);
});

test("isEnrichmentComplete returns false when description is missing", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    tags: ["deploy", "production"],
    searchHints: ["deploy a service to production"],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

test("isEnrichmentComplete returns false when description is an empty string", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "   ",
    tags: ["deploy"],
    searchHints: ["deploy a service"],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

test("isEnrichmentComplete returns false when tags array is empty", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "Deploy services to production",
    tags: [],
    searchHints: ["deploy a service to production"],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

test("isEnrichmentComplete returns false when tags is missing", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "Deploy services to production",
    searchHints: ["deploy a service to production"],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

test("isEnrichmentComplete returns false when searchHints is missing", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "Deploy services to production",
    tags: ["deploy", "production"],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

test("isEnrichmentComplete returns false when searchHints array is empty", () => {
  const entry: StashEntry = {
    name: "deploy",
    type: "script",
    description: "Deploy services to production",
    tags: ["deploy", "production"],
    searchHints: [],
  };
  expect(isEnrichmentComplete(entry)).toBe(false);
});

// ── Wave 1: captureMode / whenToUse / lessonStrength / evidenceSources ──────

test("applyCuratedFrontmatter extracts captureMode='hot' and 'background'", () => {
  const hotEntry: StashEntry = { name: "m", type: "memory" };
  applyCuratedFrontmatter(hotEntry, { captureMode: "hot" });
  expect(hotEntry.captureMode).toBe("hot");

  const bgEntry: StashEntry = { name: "m", type: "memory" };
  applyCuratedFrontmatter(bgEntry, { captureMode: "background" });
  expect(bgEntry.captureMode).toBe("background");
});

test("applyCuratedFrontmatter ignores unknown captureMode values", () => {
  const entry: StashEntry = { name: "m", type: "memory" };
  applyCuratedFrontmatter(entry, { captureMode: "freeform-bogus" });
  expect(entry.captureMode).toBeUndefined();
});

test("applyCuratedFrontmatter maps when_to_use frontmatter to whenToUse field", () => {
  const entry: StashEntry = { name: "skill", type: "skill" };
  applyCuratedFrontmatter(entry, { when_to_use: "When provisioning a new tenant cluster" });
  expect(entry.whenToUse).toBe("When provisioning a new tenant cluster");
});

test("applyCuratedFrontmatter ignores blank when_to_use values", () => {
  const entry: StashEntry = { name: "skill", type: "skill" };
  applyCuratedFrontmatter(entry, { when_to_use: "   " });
  expect(entry.whenToUse).toBeUndefined();
});

test("applyCuratedFrontmatter sets lessonStrength from an array's length", () => {
  const entry: StashEntry = { name: "lesson", type: "lesson" };
  applyCuratedFrontmatter(entry, { lessonStrength: ["memory:a", "memory:b", "memory:c"] });
  expect(entry.lessonStrength).toBe(3);
});

test("applyCuratedFrontmatter sets lessonStrength from a numeric value", () => {
  const entry: StashEntry = { name: "lesson", type: "lesson" };
  applyCuratedFrontmatter(entry, { lessonStrength: 7 });
  expect(entry.lessonStrength).toBe(7);
});

test("applyCuratedFrontmatter clamps negative lessonStrength to zero", () => {
  const entry: StashEntry = { name: "lesson", type: "lesson" };
  applyCuratedFrontmatter(entry, { lessonStrength: -3 });
  expect(entry.lessonStrength).toBe(0);
});

test("applyCuratedFrontmatter omits lessonStrength when absent", () => {
  const entry: StashEntry = { name: "lesson", type: "lesson" };
  applyCuratedFrontmatter(entry, {});
  expect(entry.lessonStrength).toBeUndefined();
});

test("applyCuratedFrontmatter extracts evidenceSources as a string list", () => {
  const entry: StashEntry = { name: "lesson", type: "lesson" };
  applyCuratedFrontmatter(entry, { evidenceSources: ["memory:a", "memory:b"] });
  expect(entry.evidenceSources).toEqual(["memory:a", "memory:b"]);
});

test("validateStashEntry preserves captureMode, whenToUse, lessonStrength, evidenceSources", () => {
  const result = validateStashEntry({
    name: "m",
    type: "memory",
    captureMode: "hot",
    whenToUse: "for triage",
    lessonStrength: 4,
    evidenceSources: ["memory:x"],
  });
  expect(result).not.toBeNull();
  expect(result?.captureMode).toBe("hot");
  expect(result?.whenToUse).toBe("for triage");
  expect(result?.lessonStrength).toBe(4);
  expect(result?.evidenceSources).toEqual(["memory:x"]);
});
