import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import {
  mkdirSync,
  mkdtempSync,
  rmSync,
  symlinkSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { computeValidateRows } from "../application/validate-engine";
import type { ValidateRow } from "../application/validate-engine";
import { resolveCodemapConfig } from "../config";
import { closeDb, openDb } from "../db";
import { hashContent } from "../hash";
import { initCodemap } from "../runtime";
import { installCodemapTestTeardown } from "../test-helpers/runtime-reset";
import { canCreateSymlinks } from "../test/symlink-capable";
import { parseValidateRest } from "./cmd-validate";

installCodemapTestTeardown();

let tmpRoot = "";
const symlinkCapable = canCreateSymlinks();

beforeEach(() => {
  tmpRoot = mkdtempSync(join(tmpdir(), "codemap-validate-"));
  mkdirSync(join(tmpRoot, "src"), { recursive: true });
  initCodemap(resolveCodemapConfig(tmpRoot, {}));
  const db = openDb();
  db.run(
    "CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, content_hash TEXT, size INTEGER, line_count INTEGER, language TEXT, last_modified INTEGER, indexed_at INTEGER) STRICT",
  );
  closeDb(db, { readonly: false });
});

afterEach(() => {
  rmSync(tmpRoot, { recursive: true, force: true });
});

function seedIndex(rows: { path: string; content_hash: string }[]) {
  const db = openDb();
  try {
    for (const r of rows) {
      db.run(
        "INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) VALUES (?, ?, 0, 0, 'ts', 0, 0)",
        [r.path, r.content_hash],
      );
    }
  } finally {
    closeDb(db, { readonly: false });
  }
}

function withDb<T>(fn: (db: ReturnType<typeof openDb>) => T): T {
  const db = openDb();
  try {
    return fn(db);
  } finally {
    closeDb(db, { readonly: true });
  }
}

describe("parseValidateRest", () => {
  it("returns help for --help", () => {
    expect(parseValidateRest(["validate", "--help"]).kind).toBe("help");
    expect(parseValidateRest(["validate", "-h"]).kind).toBe("help");
  });

  it("parses no args (check all)", () => {
    expect(parseValidateRest(["validate"])).toEqual({
      kind: "run",
      paths: [],
      json: false,
    });
  });

  it("parses --json with paths", () => {
    expect(parseValidateRest(["validate", "--json", "a.ts", "b.ts"])).toEqual({
      kind: "run",
      paths: ["a.ts", "b.ts"],
      json: true,
    });
  });

  it("rejects unknown option", () => {
    const r = parseValidateRest(["validate", "--nope"]);
    expect(r.kind).toBe("error");
  });
});

describe("computeValidateRows", () => {
  it("returns empty when everything matches", () => {
    const content = "export const a = 1\n";
    writeFileSync(join(tmpRoot, "src/a.ts"), content);
    seedIndex([{ path: "src/a.ts", content_hash: hashContent(content) }]);
    const rows = withDb((db) => computeValidateRows(db, tmpRoot, []));
    expect(rows).toEqual([]);
  });

  it("flags stale entries", () => {
    const old = "export const a = 1\n";
    writeFileSync(join(tmpRoot, "src/a.ts"), "export const a = 2\n");
    seedIndex([{ path: "src/a.ts", content_hash: hashContent(old) }]);
    const rows = withDb((db) => computeValidateRows(db, tmpRoot, []));
    expect(rows).toEqual([{ path: "src/a.ts", status: "stale" }]);
  });

  it("flags missing files", () => {
    seedIndex([{ path: "src/gone.ts", content_hash: "deadbeef" }]);
    const rows = withDb((db) => computeValidateRows(db, tmpRoot, []));
    expect(rows).toEqual([{ path: "src/gone.ts", status: "missing" }]);
  });

  it("flags unindexed files when explicit paths are passed", () => {
    writeFileSync(join(tmpRoot, "src/new.ts"), "export const x = 0\n");
    const rows = withDb((db) =>
      computeValidateRows(db, tmpRoot, ["src/new.ts"]),
    );
    expect(rows).toEqual([{ path: "src/new.ts", status: "unindexed" }]);
  });

  it("rejects explicit paths that escape project root", () => {
    const rows = withDb((db) =>
      computeValidateRows(db, tmpRoot, ["../../../etc/passwd"]),
    );
    expect(rows).toEqual([
      {
        path: "../../../etc/passwd",
        status: "rejected",
        reason: "path escapes project root",
      },
    ]);
  });

  it.skipIf(!symlinkCapable)(
    "rejects explicit paths that escape via symlink",
    () => {
      const base = mkdtempSync(join(tmpdir(), "codemap-validate-symlink-"));
      const outside = join(base, "outside");
      mkdirSync(outside, { recursive: true });
      writeFileSync(join(outside, "secret.ts"), "export const s = 1;\n");
      symlinkSync(join(outside, "secret.ts"), join(tmpRoot, "escape.ts"));

      const rows = withDb((db) =>
        computeValidateRows(db, tmpRoot, ["escape.ts"]),
      );
      expect(rows).toEqual([
        {
          path: "escape.ts",
          status: "rejected",
          reason: "path escapes via symlink",
        },
      ]);
      rmSync(base, { recursive: true, force: true });
    },
  );

  it.skipIf(!symlinkCapable)(
    "rejects indexed escape paths on full-scan validate",
    () => {
      const base = mkdtempSync(join(tmpdir(), "codemap-validate-scan-"));
      const outside = join(base, "outside");
      mkdirSync(outside, { recursive: true });
      writeFileSync(join(outside, "secret.ts"), "export const s = 1;\n");
      symlinkSync(join(outside, "secret.ts"), join(tmpRoot, "escape.ts"));
      seedIndex([
        {
          path: "escape.ts",
          content_hash: hashContent("export const s = 1;\n"),
        },
      ]);

      const rows = withDb((db) => computeValidateRows(db, tmpRoot, []));
      expect(rows).toEqual([
        {
          path: "escape.ts",
          status: "rejected",
          reason: "path escapes via symlink",
        },
      ]);
      rmSync(base, { recursive: true, force: true });
    },
  );

  it.skipIf(!symlinkCapable)(
    "rejects indexed broken symlinks on full-scan validate",
    () => {
      symlinkSync(join(tmpRoot, "missing.ts"), join(tmpRoot, "broken.ts"));
      seedIndex([{ path: "broken.ts", content_hash: hashContent("stale\n") }]);

      const rows = withDb((db) => computeValidateRows(db, tmpRoot, []));
      expect(rows).toEqual([
        {
          path: "broken.ts",
          status: "rejected",
          reason: "path escapes via symlink",
        },
      ]);
    },
  );

  it("normalizes absolute paths inside the project root", () => {
    const old = "export const a = 1\n";
    const abs = join(tmpRoot, "src/a.ts");
    writeFileSync(abs, "export const a = 2\n");
    seedIndex([{ path: "src/a.ts", content_hash: hashContent(old) }]);

    const rows = withDb((db) => computeValidateRows(db, tmpRoot, [abs]));
    expect(rows).toEqual([{ path: "src/a.ts", status: "stale" }]);
  });

  it("rejects absolute paths outside the project root", () => {
    const outside = mkdtempSync(
      join(tmpdir(), "codemap-validate-abs-outside-"),
    );
    try {
      const outsideFile = join(outside, "secret.ts");
      writeFileSync(outsideFile, "export const s = 1;\n");

      const rows = withDb((db) =>
        computeValidateRows(db, tmpRoot, [outsideFile]),
      );
      expect(rows).toHaveLength(1);
      expect(rows[0]?.status).toBe("rejected");
      expect(rows[0]?.reason).toBe("path escapes project root");
    } finally {
      rmSync(outside, { recursive: true, force: true });
    }
  });

  it("dedupes paths and sorts by path", () => {
    writeFileSync(join(tmpRoot, "src/a.ts"), "v2\n");
    writeFileSync(join(tmpRoot, "src/b.ts"), "v2\n");
    seedIndex([
      { path: "src/a.ts", content_hash: hashContent("v1\n") },
      { path: "src/b.ts", content_hash: hashContent("v1\n") },
    ]);
    const rows = withDb((db) =>
      computeValidateRows(db, tmpRoot, ["src/b.ts", "src/a.ts", "src/a.ts"]),
    );
    expect(rows.map((r: ValidateRow) => r.path)).toEqual([
      "src/a.ts",
      "src/b.ts",
    ]);
  });
});
