import { after, before, describe, it } from "node:test";
import { strict as assert } from "node:assert";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { FileFinder } from "../dist/src/index.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
const normalizePath = (p) => p.replace(/\\/g, "/");

/** @type {import("../dist/src/finder.js").FileFinder | null} */
let finder = null;

describe("fff-node", { concurrency: 1 }, () => {
  before(async () => {
    const result = FileFinder.create({ basePath: REPO_ROOT });
    assert.ok(result.ok, `create failed: ${!result.ok ? result.error : ""}`);
    finder = result.value;

    const wait = await finder.waitForScan(5_000);
    if (!wait.ok) {
      assert.ok(wait.ok, `waitForScan failed: ${wait.error}`);
    }

    assert.equal(wait.value, true, "scan should finish within 5s");
  });

  after(() => {
    if (finder && !finder.isDestroyed) {
      finder.destroy();
    }
  });

  it("isAvailable returns true when the native library is loadable", () => {
    assert.equal(FileFinder.isAvailable(), true);
  });

  it("getScanProgress reports >100 indexed files", () => {
    const r = finder.getScanProgress();
    assert.ok(r.ok, "getScanProgress failed");
    assert.equal(typeof r.value.scannedFilesCount, "number");
    assert.ok(
      r.value.scannedFilesCount > 100,
      `expected >100, got ${r.value.scannedFilesCount}`,
    );

    assert.equal(r.value.isScanning, false);
  });

  describe("search", { concurrency: 1 }, () => {
    it("finds the root Cargo.toml", () => {
      const r = finder.fileSearch("Cargo.toml", { pageSize: 10 });
      assert.ok(r.ok, `search failed: ${!r.ok ? r.error : ""}`);
      const paths = r.value.items.map((i) => i.relativePath);
      assert.ok(
        paths.includes("Cargo.toml"),
        `expected Cargo.toml in results, got: ${paths}`,
      );
    });

    it("finds crates/fff-c/src/lib.rs", () => {
      const r = finder.fileSearch("lib.rs", { pageSize: 20 });
      assert.ok(r.ok);
      const paths = r.value.items.map((i) => i.relativePath);
      assert.ok(
        paths.some((p) => p.includes("fff-c") && p.endsWith("lib.rs")),
        `expected fff-c/src/lib.rs, got: ${paths}`,
      );
    });

    it("respects pageSize", () => {
      const r = finder.fileSearch("lua", { pageSize: 3 });
      assert.ok(r.ok);
      assert.ok(
        r.value.items.length <= 3,
        `expected <=3, got ${r.value.items.length}`,
      );
    });

    it("items contain all required fields", () => {
      const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
      assert.ok(r.ok);
      const item = r.value.items[0];
      for (const f of ["relativePath", "fileName", "gitStatus"]) {
        assert.equal(typeof item[f], "string", `${f} should be a string`);
      }
      for (const f of ["size", "modified"]) {
        assert.equal(typeof item[f], "number", `${f} should be a number`);
      }
    });

    it("scores contain all required fields", () => {
      const r = finder.fileSearch("Cargo.toml", { pageSize: 1 });
      assert.ok(r.ok);
      assert.ok(r.value.scores.length > 0);
      const s = r.value.scores[0];
      for (const f of ["total", "baseScore"]) {
        assert.equal(typeof s[f], "number", `${f} should be a number`);
      }
      assert.equal(typeof s.matchType, "string");
    });

    it("totalMatched <= totalFiles", () => {
      const r = finder.fileSearch("rs", { pageSize: 1 });
      assert.ok(r.ok);
      assert.ok(r.value.totalMatched > 0);
      assert.ok(r.value.totalFiles > 100);
      assert.ok(r.value.totalFiles >= r.value.totalMatched);
    });

    it("empty query returns files", () => {
      const r = finder.fileSearch("", { pageSize: 5 });
      assert.ok(r.ok, `empty query failed: ${!r.ok ? r.error : ""}`);
      assert.equal(typeof r.value.totalFiles, "number");
    });
  });

  describe("glob", { concurrency: 1 }, () => {
    it("filters by extension via raw glob pattern", () => {
      const r = finder.glob("**/*.rs", { pageSize: 50 });
      assert.ok(r.ok, `glob failed: ${!r.ok ? r.error : ""}`);
      assert.ok(r.value.items.length > 0, "expected at least one .rs file");
      for (const item of r.value.items) {
        assert.ok(
          item.relativePath.endsWith(".rs"),
          `unexpected file: ${item.relativePath}`,
        );
      }
    });

    it("returns empty result for non-matching pattern", () => {
      const r = finder.glob("**/this-extension-does-not-exist-anywhere.zzz");
      assert.ok(r.ok);
      assert.equal(r.value.items.length, 0);
    });

    it("rejects empty pattern", () => {
      const r = finder.glob("");
      assert.equal(r.ok, false);
    });

    it("respects pageSize", () => {
      const r = finder.glob("**/*.rs", { pageSize: 3 });
      assert.ok(r.ok);
      assert.ok(r.value.items.length <= 3);
    });
  });

  describe("grep", { concurrency: 1 }, () => {
    it("finds FffResult in Rust sources", () => {
      // Constrain to .rs files so the assertion doesn't depend on result ordering
      // or content-indexing timing for other file types.
      const rustResults = finder.grep("*.rs FffResult", { mode: "plain" });
      assert.ok(
        rustResults.ok,
        `grep failed: ${!rustResults.ok ? rustResults.error : ""}`,
      );
      assert.ok(
        rustResults.value.items.length > 0,
        "expected at least one .rs match",
      );
      assert.ok(
        rustResults.value.items.some((m) => m.relativePath.endsWith(".rs")),
      );

      const cResults = finder.grep("!**/*.{js,ts,rs} FffResult", {
        mode: "plain",
      });
      assert.ok(
        cResults.ok,
        `grep failed: ${!cResults.ok ? cResults.error : ""}`,
      );
      assert.ok(
        cResults.value.items.length > 0,
        "expected at least one non-js/ts/rs match",
      );
      assert.ok(
        cResults.value.items.some((m) => m.relativePath.endsWith(".h")),
      );
    });

    it("match items contain all required fields", () => {
      const r = finder.grep("FffResult", { mode: "plain" });
      assert.ok(r.ok);
      const m = r.value.items[0];
      assert.equal(typeof m.relativePath, "string");
      assert.equal(typeof m.lineNumber, "number");
      assert.ok(m.lineNumber > 0, "lineNumber is 1-based");
      assert.equal(typeof m.lineContent, "string");
      assert.ok(m.lineContent.includes("FffResult"));
      assert.equal(typeof m.col, "number");
      assert.equal(typeof m.byteOffset, "number");
      assert.ok(Array.isArray(m.matchRanges));
      console.log(m.matchRanges);
    });

    it("pagination returns a second page", () => {
      const r = finder.grep("fn", { mode: "plain" });
      assert.ok(r.ok);
      if (r.value.nextCursor) {
        const r2 = finder.grep("fn", { cursor: r.value.nextCursor });
        assert.ok(r2.ok, `page 2 failed: ${!r2.ok ? r2.error : ""}`);
        assert.equal(typeof r2.value.totalMatched, "number");
      }
    });

    it("respects pageSize", () => {
      // Cap to one match per file so pageSize bounds the total deterministically.
      const unbounded = finder.grep("fn", {
        mode: "plain",
        maxMatchesPerFile: 1,
      });
      assert.ok(
        unbounded.ok,
        `grep failed: ${!unbounded.ok ? unbounded.error : ""}`,
      );
      assert.ok(unbounded.value.items.length > 2);

      const limited = finder.grep("fn", {
        mode: "plain",
        maxMatchesPerFile: 1,
        pageSize: 2,
      });
      assert.ok(limited.ok, `grep failed: ${!limited.ok ? limited.error : ""}`);
      assert.ok(
        limited.value.items.length <= 2,
        `expected <=2 items, got ${limited.value.items.length}`,
      );
      assert.ok(
        limited.value.items.length < unbounded.value.items.length,
        "limited page should yield fewer matches than the default",
      );
      assert.ok(limited.value.nextCursor !== null, "nextCursor should be set");
    });

    it("regex mode matches pub fn declarations", () => {
      const r = finder.grep("pub fn \\w+", { mode: "regex" });
      assert.ok(r.ok, `regex grep failed: ${!r.ok ? r.error : ""}`);
      assert.ok(r.value.items.length > 0);
    });

    it("decodes before/after context lines", () => {
      const r = finder.grep(
        "LOLLOWOIEJIWOIUOIWUIWUIOUWE", // the random text visible here
        {
          mode: "plain",
          beforeContext: 1,
          afterContext: 1,
          maxMatchesPerFile: 5,
        },
      );
      assert.ok(r.ok, `grep with context failed: ${!r.ok ? r.error : ""}`);

      const match = r.value.items.find(
        (m) =>
          normalizePath(m.relativePath) === "packages/fff-node/test/e2e.mjs",
      );
      assert.ok(
        match,
        `expected a single match in the codebase, got: ${r.value.items
          .map((m) => normalizePath(m.relativePath))
          .join(", ")}`,
      );
      assert.deepEqual(match.contextBefore, ["      const r = finder.grep("]);
      assert.deepEqual(match.contextAfter, ["        {"]);
    });
  });

  describe("multiGrep", { concurrency: 1 }, () => {
    it("finds lines matching any of the C FFI function names", () => {
      const r = finder.multiGrep({
        patterns: ["fff_create_instance", "fff_destroy", "fff_search"],
      });
      assert.ok(r.ok, `multiGrep failed: ${!r.ok ? r.error : ""}`);
      assert.ok(r.value.items.length > 0);
    });

    it("rejects empty patterns array", () => {
      const r = finder.multiGrep({ patterns: [] });
      assert.ok(!r.ok);
    });
  });

  it("refreshGitStatus returns a positive count", () => {
    const r = finder.refreshGitStatus();
    assert.ok(r.ok, `refreshGitStatus failed: ${!r.ok ? r.error : ""}`);
    assert.equal(typeof r.value, "number");
    assert.ok(r.value > 0);
  });

  it("isScanning returns a boolean", () => {
    assert.equal(typeof finder.isScanning(), "boolean");
  });

  describe("healthCheck", { concurrency: 1 }, () => {
    it("reports initialized state with instance", () => {
      const r = finder.healthCheck(REPO_ROOT);
      assert.ok(r.ok, `healthCheck failed: ${!r.ok ? r.error : ""}`);
      assert.equal(typeof r.value.version, "string");
      assert.ok(r.value.version.length > 0);
      assert.equal(r.value.git.available, true);
      assert.equal(r.value.git.repositoryFound, true);
      assert.equal(r.value.filePicker.initialized, true);
      assert.ok(r.value.filePicker.indexedFiles > 100);
    });

    it("static check works without instance", () => {
      const r = FileFinder.healthCheckStatic(REPO_ROOT);
      assert.ok(r.ok, `static healthCheck failed: ${!r.ok ? r.error : ""}`);
      assert.equal(typeof r.value.version, "string");
      assert.equal(r.value.filePicker.initialized, false);
      assert.equal(r.value.git.repositoryFound, true);
    });
  });

  it("create rejects a non-existent path", () => {
    const r = FileFinder.create({ basePath: "/nonexistent/fff-test-path" });
    assert.ok(!r.ok);
    assert.ok(
      r.error.toLowerCase().includes("invalid path") ||
        r.error.toLowerCase().includes("not exist"),
      `unexpected error: ${r.error}`,
    );
  });

  // This must be the last describe block since it destroys the shared instance.
  describe("after destroy", { concurrency: 1 }, () => {
    before(() => {
      finder.destroy();
    });

    it("isDestroyed is true", () => {
      assert.equal(finder.isDestroyed, true);
    });

    it("search returns an error", () => {
      const r = finder.fileSearch("test");
      assert.ok(!r.ok);
      assert.ok(r.error.includes("destroyed"));
    });

    it("grep returns an error", () => {
      const r = finder.grep("test");
      assert.ok(!r.ok);
      assert.ok(r.error.includes("destroyed"));
    });

    it("double destroy is a safe no-op", () => {
      finder.destroy();
      assert.equal(finder.isDestroyed, true);
    });
  });
});
