/**
 * Repositories that arrive without a push must still be indexed.
 *
 * indexChangedFiles() is called from exactly ONE place — the git post-receive
 * hook. That is correct for pushes and wrong for every other way a repository
 * comes into existence:
 *
 *   - bulk import CLONES (git clone fires no post-receive hook)
 *   - POST /new + first-repo-scaffold writes via the git plumbing API
 *   - forks copy the bare repo directly
 *
 * So both onboarding paths built in this session produced repositories where
 * gluecron_semantic_search, repo chat and "ask this repo" returned nothing.
 * Silently — an empty index is indistinguishable from "no matches", which is
 * why it survived. It was found while wiring the agent-first path, which
 * depends entirely on that index being populated.
 *
 * Deps are injected rather than mock.module()'d: ../git/repository is imported
 * by dozens of test files and a module mock leaks across the whole run.
 */

import { describe, expect, it } from "bun:test";
import { readFileSync } from "fs";

const SRC = readFileSync("src/lib/index-existing-repo.ts", "utf8");

type Entry = { name: string; type: string; path?: string };

function treeFrom(map: Record<string, Entry[]>) {
  return async (_o: string, _r: string, _ref: string, path?: string) =>
    map[path ?? ""] ?? [];
}

describe("tree walking", () => {
  it("collects blobs across nested directories", async () => {
    const captured: string[][] = [];
    const { indexExistingRepo } = await import("../lib/index-existing-repo");
    const r = await indexExistingRepo(
      { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
      {
        getTree: treeFrom({
          "": [
            { name: "index.ts", type: "blob", path: "index.ts" },
            { name: "src", type: "tree", path: "src" },
          ],
          src: [{ name: "app.ts", type: "blob", path: "src/app.ts" }],
        }),
        indexChangedFiles: async (a) => {
          captured.push(a.changedPaths);
          return { indexed: a.changedPaths.length, skipped: 0, model: "test" };
        },
      }
    );
    expect(r.indexed).toBe(2);
    expect(captured[0].sort()).toEqual(["index.ts", "src/app.ts"]);
  });

  it("skips vendor and build directories", async () => {
    let seen: string[] = [];
    const { indexExistingRepo } = await import("../lib/index-existing-repo");
    await indexExistingRepo(
      { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
      {
        getTree: treeFrom({
          "": [
            { name: "node_modules", type: "tree", path: "node_modules" },
            { name: "dist", type: "tree", path: "dist" },
            { name: "keep.ts", type: "blob", path: "keep.ts" },
          ],
          node_modules: [{ name: "junk.js", type: "blob", path: "node_modules/junk.js" }],
          dist: [{ name: "out.js", type: "blob", path: "dist/out.js" }],
        }),
        indexChangedFiles: async (a) => {
          seen = a.changedPaths;
          return { indexed: a.changedPaths.length, skipped: 0, model: "test" };
        },
      }
    );
    expect(seen).toEqual(["keep.ts"]);
  });

  it("delegates filtering to indexChangedFiles rather than duplicating it", () => {
    // isCodeFile, the per-push cap and the embedding model all live in
    // semantic-index.ts. Re-implementing them here would let scaffolded and
    // pushed repos drift apart in what they index.
    //
    // Comments stripped: the source explains this delegation in prose that
    // names the very symbol being asserted absent.
    const code = SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
    expect(code).toContain("indexChangedFiles");
    expect(code).not.toContain("isCodeFile");
  });
});

describe("fault isolation", () => {
  it("never throws when the tree read fails", async () => {
    const { indexExistingRepo } = await import("../lib/index-existing-repo");
    const r = await indexExistingRepo(
      { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
      {
        getTree: async () => {
          throw new Error("disk on fire");
        },
        indexChangedFiles: async () => ({ indexed: 0, skipped: 0, model: "test" }),
      }
    );
    // A single unreadable directory must not abandon the index, and an
    // unindexed repo must never fail an import or a repo creation.
    expect(r.indexed).toBe(0);
    expect(r.error).toBeUndefined();
  });

  it("reports an embedding failure instead of throwing", async () => {
    const { indexExistingRepo } = await import("../lib/index-existing-repo");
    const r = await indexExistingRepo(
      { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
      {
        getTree: treeFrom({ "": [{ name: "a.ts", type: "blob", path: "a.ts" }] }),
        indexChangedFiles: async () => {
          throw new Error("embedder down");
        },
      }
    );
    expect(r.indexed).toBe(0);
    expect(r.error).toContain("embedder down");
  });

  it("returns zero for an empty repository without calling the embedder", async () => {
    let called = false;
    const { indexExistingRepo } = await import("../lib/index-existing-repo");
    const r = await indexExistingRepo(
      { repositoryId: "r1", owner: "a", repoName: "b", ref: "main", commitSha: "sha" },
      {
        getTree: treeFrom({}),
        indexChangedFiles: async () => {
          called = true;
          return { indexed: 0, skipped: 0, model: "test" };
        },
      }
    );
    expect(r.indexed).toBe(0);
    expect(called).toBe(false);
  });
});

describe("both onboarding paths index", () => {
  it("bulk import indexes each repo", () => {
    const mig = readFileSync("src/lib/migration-onboarding.ts", "utf8");
    expect(mig).toContain("indexExistingRepo");
    expect(mig).toContain("filesIndexed");
  });

  it("new-repo scaffold indexes after committing", () => {
    const scaffold = readFileSync("src/lib/first-repo-scaffold.ts", "utf8");
    expect(scaffold).toContain("indexExistingRepo");
    // Must come after the commits — there is nothing to index before them.
    expect(scaffold.indexOf("createOrUpdateFileOnBranch")).toBeLessThan(
      scaffold.lastIndexOf("indexExistingRepo")
    );
  });
});
