import { afterAll, afterEach, beforeEach, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isRgAvailable, resolveRg } from "../src/core/ripgrep/resolve";
import { type Cleanup, sandboxStashDir, sandboxXdgCacheHome, sandboxXdgConfigHome } from "./_helpers/sandbox";

const createdTmpDirs: string[] = [];

function expectDefined<T>(value: T | null | undefined): T {
  expect(value).toBeDefined();
  if (value === undefined || value === null) {
    throw new Error("Expected value to be defined");
  }
  return value;
}

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

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

// ── Environment isolation ───────────────────────────────────────────────────

let envCleanup: Cleanup = () => {};

beforeEach(() => {
  const cacheResult = sandboxXdgCacheHome();
  const cfgResult = sandboxXdgConfigHome(cacheResult.cleanup);
  const stashResult = sandboxStashDir(cfgResult.cleanup);
  envCleanup = stashResult.cleanup;
});

afterEach(() => {
  envCleanup();
  envCleanup = () => {};
});

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

// ── resolveRg ───────────────────────────────────────────────────────────────

test("resolveRg finds system ripgrep on PATH", () => {
  const originalPath = process.env.PATH;
  const stashDir = tmpDir();
  const binDir = path.join(stashDir, "bin");
  fs.mkdirSync(binDir, { recursive: true });

  // Create a fake rg binary on PATH so the test does not depend on the host environment
  const rgName = process.platform === "win32" ? "rg.cmd" : "rg";
  const fakeRg = path.join(binDir, rgName);
  const scriptContent = process.platform === "win32" ? "@echo off\r\necho fake rg\r\n" : "#!/bin/sh\necho fake rg\n";
  fs.writeFileSync(fakeRg, scriptContent);
  try {
    // Make sure the fake rg is executable where that concept applies
    if (process.platform !== "win32") {
      fs.chmodSync(fakeRg, 0o755);
    }
  } catch {
    // Ignore chmod errors on platforms/filesystems that do not support it
  }

  // Prepend the fake rg directory to PATH for this test only
  process.env.PATH = binDir + path.delimiter + (originalPath ?? "");
  try {
    const rg = resolveRg();
    expect(expectDefined(rg)).toContain("rg");
  } finally {
    process.env.PATH = originalPath;
  }
});

test("resolveRg finds rg in provided bin directory", () => {
  const binDir = tmpDir();

  // Create a fake rg binary
  const fakeRg = path.join(binDir, "rg");
  fs.writeFileSync(fakeRg, "#!/bin/sh\necho fake rg\n");
  fs.chmodSync(fakeRg, 0o755);

  const rg = resolveRg(binDir);
  expect(rg).toBe(fakeRg);
});

test("resolveRg skips non-executable files in bin dir", () => {
  const binDir = tmpDir();

  // Create a non-executable rg file
  const fakeRg = path.join(binDir, "rg");
  fs.writeFileSync(fakeRg, "not executable");
  fs.chmodSync(fakeRg, 0o644);

  const rg = resolveRg(binDir);
  // Should fall through to system PATH
  expect(rg).not.toBe(fakeRg);
});

// ── isRgAvailable ───────────────────────────────────────────────────────────

test("isRgAvailable returns true when rg is on PATH", () => {
  const originalPath = process.env.PATH;
  const stashDir = tmpDir();
  const binDir = path.join(stashDir, "bin");
  fs.mkdirSync(binDir, { recursive: true });

  const rgName = process.platform === "win32" ? "rg.cmd" : "rg";
  const fakeRg = path.join(binDir, rgName);
  const scriptContent = process.platform === "win32" ? "@echo off\r\necho fake rg\r\n" : "#!/bin/sh\necho fake rg\n";
  fs.writeFileSync(fakeRg, scriptContent);
  if (process.platform !== "win32") {
    fs.chmodSync(fakeRg, 0o755);
  }

  process.env.PATH = binDir + path.delimiter + (originalPath ?? "");
  try {
    expect(isRgAvailable()).toBe(true);
  } finally {
    process.env.PATH = originalPath;
  }
});

// ── Integration: indexed search pipeline ────────────────────────────────────

test("search pipeline returns ranked results when index exists", async () => {
  const stashDir = tmpDir();
  for (const sub of ["scripts", "skills", "commands", "agents"]) {
    fs.mkdirSync(path.join(stashDir, sub), { recursive: true });
  }

  // Create scripts with .stash.json metadata
  writeFile(path.join(stashDir, "scripts", "docker", "build.sh"), "#!/bin/bash\necho build\n");
  writeFile(
    path.join(stashDir, "scripts", "docker", ".stash.json"),
    JSON.stringify({
      entries: [
        {
          name: "docker-build",
          type: "script",
          description: "build docker images",
          tags: ["docker", "container"],
          filename: "build.sh",
        },
      ],
    }),
  );
  writeFile(path.join(stashDir, "scripts", "git", "diff.sh"), "#!/bin/bash\necho diff\n");
  writeFile(
    path.join(stashDir, "scripts", "git", ".stash.json"),
    JSON.stringify({
      entries: [
        {
          name: "git-diff",
          type: "script",
          description: "summarize git changes",
          tags: ["git", "diff"],
          filename: "diff.sh",
        },
      ],
    }),
  );

  // Use the sandbox stash dir for the index and search
  process.env.AKM_STASH_DIR = stashDir;

  // Build index
  const { akmIndex } = await import("../src/indexer/indexer");
  await akmIndex({ stashDir });

  // Search — TF-IDF should rank docker-related results first
  const { akmSearch } = await import("../src/commands/read/search");
  const result = await akmSearch({ query: "docker", type: "any" });

  expect(result.hits.length).toBeGreaterThan(0);
  // Docker-related result should be ranked first
  expect(result.hits[0].name).toContain("docker");
});
