import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { closeDatabase, openDatabase, rebuildFts, searchFts, upsertEntry } from "../src/indexer/db/db";
import type { StashEntry } from "../src/indexer/passes/metadata";
import type { Database } from "../src/storage/database";
import { type Cleanup, sandboxXdgCacheHome, sandboxXdgConfigHome } from "./_helpers/sandbox";

// ── Temp directory management ───────────────────────────────────────────────

const createdTmpDirs: string[] = [];

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

function tmpDbPath(label = "fuzzy"): string {
  const dir = tmpDir(label);
  return path.join(dir, "test.db");
}

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);
  envCleanup = cfgResult.cleanup;
});

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

// ── Helpers ─────────────────────────────────────────────────────────────────

function makeEntry(overrides: Partial<StashEntry> & { name: string; type: StashEntry["type"] }): StashEntry {
  return {
    description: "A test entry",
    ...overrides,
  };
}

function insertTestEntry(
  db: Database,
  key: string,
  opts?: {
    dirPath?: string;
    filePath?: string;
    stashDir?: string;
    description?: string;
    searchText?: string;
    type?: StashEntry["type"];
  },
): number {
  const type = opts?.type ?? "skill";
  const entry = makeEntry({ name: key, type, description: opts?.description ?? `Description for ${key}` });
  return upsertEntry(
    db,
    key,
    opts?.dirPath ?? "/test/dir",
    opts?.filePath ?? `/test/dir/${key}.ts`,
    opts?.stashDir ?? "/test/stash",
    entry,
    opts?.searchText ?? `${key} ${entry.description}`,
  );
}

// ── Fuzzy / prefix fallback tests ───────────────────────────────────────────

describe("Fuzzy prefix fallback in searchFts", () => {
  test("exact match still works normally", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes", {
        searchText: "kubernetes container orchestration platform",
      });
      rebuildFts(db);

      const results = searchFts(db, "kubernetes", 10);
      expect(results.length).toBe(1);
      expect(results[0].entry.name).toBe("kubernetes");
    } finally {
      closeDatabase(db);
    }
  });

  test("truncated prefix — 'kubernet' matches 'kubernetes' via prefix expansion", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes", {
        searchText: "kubernetes container orchestration platform",
      });
      rebuildFts(db);

      // "kubernet" has no exact FTS match; the prefix fallback expands it to "kubernet*".
      const results = searchFts(db, "kubernet", 10);
      expect(results.length).toBe(1);
      expect(results[0].entry.name).toBe("kubernetes");
    } finally {
      closeDatabase(db);
    }
  });

  test("partial prefix match — 'kube' finds 'kubernetes' assets", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes", {
        searchText: "kubernetes container orchestration",
      });
      insertTestEntry(db, "kubelet", {
        searchText: "kubelet node agent kubernetes",
      });
      rebuildFts(db);

      // "kube" should not match exactly (FTS5 uses full token matching).
      // The prefix fallback should append * and find both "kubernetes" and "kubelet".
      const results = searchFts(db, "kube", 10);
      expect(results.length).toBe(2);
      const names = results.map((r) => r.entry.name).sort();
      expect(names).toContain("kubernetes");
      expect(names).toContain("kubelet");
    } finally {
      closeDatabase(db);
    }
  });

  test("multiple token query with prefix fallback", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "deploy-kubernetes", {
        searchText: "deploy kubernetes production cluster",
      });
      insertTestEntry(db, "deploy-docker", {
        searchText: "deploy docker containers locally",
      });
      rebuildFts(db);

      // "deploy kube" — "deploy" matches exactly, "kube" needs prefix fallback.
      // Should find "deploy-kubernetes" because "deploy" AND "kube*" matches.
      const results = searchFts(db, "deploy kube", 10);
      expect(results.length).toBeGreaterThanOrEqual(1);
      const names = results.map((r) => r.entry.name);
      expect(names).toContain("deploy-kubernetes");
      // "deploy-docker" should NOT match since "kube*" doesn't match "docker"
      expect(names).not.toContain("deploy-docker");
    } finally {
      closeDatabase(db);
    }
  });

  test("non-matching query returns empty even with prefix fallback", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes", {
        searchText: "kubernetes container orchestration",
      });
      rebuildFts(db);

      // "xyznonexist" has no prefix match in the index
      const results = searchFts(db, "xyznonexist", 10);
      expect(results).toEqual([]);
    } finally {
      closeDatabase(db);
    }
  });

  test("short tokens (1-2 chars) should NOT get prefix expansion", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes", {
        searchText: "kubernetes container orchestration",
      });
      insertTestEntry(db, "kafka", {
        searchText: "kafka streaming events",
      });
      rebuildFts(db);

      // "k" is a 1-char token — should NOT be prefix-expanded to "k*" which
      // would match everything starting with "k".
      // Since "k" doesn't match any full token, should return empty.
      const results = searchFts(db, "k", 10);
      expect(results).toEqual([]);

      // "ka" is a 2-char token — also should not be prefix-expanded.
      const results2 = searchFts(db, "ka", 10);
      expect(results2).toEqual([]);
    } finally {
      closeDatabase(db);
    }
  });

  test("all short tokens produce no prefix fallback and return empty", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "golang-setup", {
        searchText: "go golang setup configuration",
      });
      insertTestEntry(db, "js-tooling", {
        searchText: "js javascript tooling bundler",
      });
      rebuildFts(db);

      const results = searchFts(db, "go js", 10);
      expect(results).toEqual([]);
    } finally {
      closeDatabase(db);
    }
  });

  test("prefix fallback only triggers when exact match returns zero results", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "deploy", {
        searchText: "deploy application production",
      });
      insertTestEntry(db, "deployment-manager", {
        searchText: "deployment manager orchestration",
      });
      rebuildFts(db);

      // "deploy" matches exactly — should return results from exact match,
      // not the prefix fallback. FTS5 with porter stemmer may match
      // "deployment" as well through stemming, but the key point is that
      // the exact query runs first and returns results.
      const results = searchFts(db, "deploy", 10);
      expect(results.length).toBeGreaterThanOrEqual(1);
      // The first result should be "deploy" (exact match has best BM25)
      expect(results[0].entry.name).toBe("deploy");
    } finally {
      closeDatabase(db);
    }
  });

  test("prefix fallback with entryType filter", () => {
    const db = openDatabase(tmpDbPath());
    try {
      insertTestEntry(db, "kubernetes-skill", {
        type: "skill",
        searchText: "kubernetes container management skill",
      });
      insertTestEntry(db, "kubernetes-script", {
        type: "script",
        searchText: "kubernetes deployment script automation",
      });
      rebuildFts(db);

      // "kube" with type filter "skill" should only return the skill entry
      const results = searchFts(db, "kube", 10, "skill");
      expect(results.length).toBe(1);
      expect(results[0].entry.name).toBe("kubernetes-skill");
      expect(results[0].entry.type).toBe("skill");
    } finally {
      closeDatabase(db);
    }
  });
});
