import {
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  symlinkSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
  getPersonalWikiPaths,
  getPersonalWikiRoot,
  getVaultPaths,
  isPersonalVault,
  migrateDoubledPersonalVault,
  resolveVaultPaths,
} from "../extensions/llm-wiki/lib/utils.js";

/**
 * Regression suite for the doubled-dotdir personal-wiki bug.
 *
 * Before the fix, `getPersonalWikiRoot()` returned `~/.llm-wiki` (the
 * dot-dir itself) while `getVaultPaths(root)` appended ANOTHER `.llm-wiki/`,
 * producing paths like `~/.llm-wiki/.llm-wiki/raw`.
 *
 * After the fix:
 *   - `getPersonalWikiRoot()` returns the PARENT of `.llm-wiki/`.
 *   - `WIKI_HOME` overrides that parent.
 *   - `migrateDoubledPersonalVault()` flattens a vault that was already
 *     written in the broken layout, in-place and idempotently.
 */

describe("getPersonalWikiRoot / getPersonalWikiPaths", () => {
  let savedWikiHome: string | undefined;

  beforeEach(() => {
    savedWikiHome = process.env.WIKI_HOME;
    // Use Reflect.deleteProperty rather than `delete` to satisfy biome's
    // performance/noDelete rule. Plain assignment to undefined is wrong here:
    // Node coerces it to the string "undefined" and treats the var as SET,
    // which would defeat the "no env override" precondition this test needs.
    Reflect.deleteProperty(process.env, "WIKI_HOME");
  });

  afterEach(() => {
    if (savedWikiHome === undefined) Reflect.deleteProperty(process.env, "WIKI_HOME");
    else process.env.WIKI_HOME = savedWikiHome;
  });

  it("returns the parent of .llm-wiki, not the dot-dir itself", () => {
    const root = getPersonalWikiRoot();
    // Must not itself end in /.llm-wiki — that would re-create the doubled bug.
    expect(root.endsWith("/.llm-wiki")).toBe(false);
  });

  it("composes vault paths under exactly ONE .llm-wiki segment", () => {
    const paths = getPersonalWikiPaths();
    // dotWiki should be <root>/.llm-wiki, never <root>/.llm-wiki/.llm-wiki.
    expect(paths.dotWiki.endsWith("/.llm-wiki")).toBe(true);
    expect(paths.dotWiki.endsWith("/.llm-wiki/.llm-wiki")).toBe(false);
    expect(paths.raw.endsWith("/.llm-wiki/raw")).toBe(true);
    expect(paths.wiki.endsWith("/.llm-wiki/wiki")).toBe(true);
  });

  it("honours WIKI_HOME as the parent of .llm-wiki", () => {
    process.env.WIKI_HOME = "/tmp/custom-home";
    expect(getPersonalWikiRoot()).toBe("/tmp/custom-home");
    const paths = getPersonalWikiPaths();
    expect(paths.dotWiki).toBe("/tmp/custom-home/.llm-wiki");
    expect(paths.wiki).toBe("/tmp/custom-home/.llm-wiki/wiki");
  });
});

describe("migrateDoubledPersonalVault", () => {
  let scratch: string;

  beforeEach(() => {
    scratch = join(tmpdir(), `llm-wiki-mig-${Math.random().toString(36).slice(2)}`);
    mkdirSync(scratch, { recursive: true });
  });

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

  function seedDoubledLayout() {
    // Reproduce the broken layout:
    //   scratch/.llm-wiki/.llm-wiki/config.json
    //   scratch/.llm-wiki/.llm-wiki/wiki/sources/note.md
    //   scratch/.llm-wiki/.llm-wiki/meta/registry.json
    const inner = join(scratch, ".llm-wiki", ".llm-wiki");
    mkdirSync(join(inner, "wiki", "sources"), { recursive: true });
    mkdirSync(join(inner, "meta"), { recursive: true });
    writeFileSync(join(inner, "config.json"), JSON.stringify({ topic: "test" }));
    writeFileSync(join(inner, "wiki", "sources", "note.md"), "# hello");
    writeFileSync(join(inner, "meta", "registry.json"), "{}");
  }

  it("is a no-op when the layout is already correct", () => {
    // Correct layout: scratch/.llm-wiki/config.json (single dot-dir level).
    mkdirSync(join(scratch, ".llm-wiki"), { recursive: true });
    writeFileSync(join(scratch, ".llm-wiki", "config.json"), "{}");

    const result = migrateDoubledPersonalVault(scratch);
    expect(result).toBeNull();
    // Nothing was moved or destroyed.
    expect(existsSync(join(scratch, ".llm-wiki", "config.json"))).toBe(true);
  });

  it("is a no-op when no vault exists at all", () => {
    const result = migrateDoubledPersonalVault(scratch);
    expect(result).toBeNull();
  });

  it("flattens a doubled layout in place", () => {
    seedDoubledLayout();

    const result = migrateDoubledPersonalVault(scratch);
    expect(result).not.toBeNull();
    expect(result?.moved.sort()).toEqual(["config.json", "meta", "wiki"]);
    expect(result?.skipped).toEqual([]);

    // After: only ONE level of .llm-wiki, with all original payload.
    const outer = join(scratch, ".llm-wiki");
    expect(existsSync(join(outer, "config.json"))).toBe(true);
    expect(existsSync(join(outer, "wiki", "sources", "note.md"))).toBe(true);
    expect(existsSync(join(outer, "meta", "registry.json"))).toBe(true);
    // Inner dir is gone.
    expect(existsSync(join(outer, ".llm-wiki"))).toBe(false);
    // Payload is intact byte-for-byte.
    expect(readFileSync(join(outer, "wiki", "sources", "note.md"), "utf-8")).toBe("# hello");
  });

  it("is idempotent — second call is a no-op", () => {
    seedDoubledLayout();
    const first = migrateDoubledPersonalVault(scratch);
    expect(first?.moved.length).toBeGreaterThan(0);
    const second = migrateDoubledPersonalVault(scratch);
    expect(second).toBeNull();
  });

  it("preserves outer entries on collision and leaves inner copy behind", () => {
    seedDoubledLayout();
    // Pre-existing entry at the outer level that would collide.
    writeFileSync(join(scratch, ".llm-wiki", "config.json"), '{"outer":true}');

    const result = migrateDoubledPersonalVault(scratch);
    expect(result?.skipped).toContain("config.json");
    // Outer config preserved untouched.
    expect(readFileSync(join(scratch, ".llm-wiki", "config.json"), "utf-8")).toBe('{"outer":true}');
    // Inner dir kept because not fully drained.
    expect(existsSync(join(scratch, ".llm-wiki", ".llm-wiki"))).toBe(true);
    // Collided inner copy is preserved for the user to resolve.
    expect(existsSync(join(scratch, ".llm-wiki", ".llm-wiki", "config.json"))).toBe(true);
    // Non-colliding entries still moved up.
    expect(existsSync(join(scratch, ".llm-wiki", "wiki", "sources", "note.md"))).toBe(true);
    expect(existsSync(join(scratch, ".llm-wiki", ".llm-wiki", "wiki"))).toBe(false);
  });

  it("composes a path that getVaultPaths can consume without double-prefixing", () => {
    // The bug surfaced because getVaultPaths appended .llm-wiki/ to a root
    // that already ended in .llm-wiki/. After migration, the parent root
    // composes cleanly: <root>/.llm-wiki/wiki, never <root>/.llm-wiki/.llm-wiki/wiki.
    seedDoubledLayout();
    migrateDoubledPersonalVault(scratch);

    const paths = getVaultPaths(scratch);
    expect(paths.dotWiki).toBe(join(scratch, ".llm-wiki"));
    expect(paths.wiki).toBe(join(scratch, ".llm-wiki", "wiki"));
    // Nothing should resolve under a doubled segment.
    expect(paths.wiki.includes("/.llm-wiki/.llm-wiki/")).toBe(false);
    // And the migrated content is reachable through the standard paths.
    const sources = readdirSync(join(paths.wiki, "sources"));
    expect(sources).toContain("note.md");
  });
});

/**
 * Regression suite for issue #145: `isPersonalVault()` compared root strings,
 * so on image-based ("atomic") Linux distributions — where `/home` is a
 * symlink to `var/home` — the personal vault was classified as a project
 * vault.
 *
 * `homedir()` returns the `$HOME` string (`/home/u`), but `process.cwd()`
 * resolves symlinks, so `resolveVaultRoot()`'s parent walk returns
 * `/var/home/u`. Both name the same directory; only the strings differ.
 * The consequences were a doubled layered-recall scan of one vault (every hit
 * mislabelled "📓 personal") and a doubled `vaultPageCount()`, which tripped
 * links-first rendering at half the configured threshold.
 */
describe("isPersonalVault on a symlinked home (atomic OS layout)", () => {
  let scratch: string;
  let physicalHome: string;
  let symlinkedHome: string;
  let savedWikiHome: string | undefined;
  let savedHome: string | undefined;

  beforeEach(() => {
    savedWikiHome = process.env.WIKI_HOME;
    savedHome = process.env.HOME;
    scratch = join(tmpdir(), `llm-wiki-symlink-${Math.random().toString(36).slice(2)}`);

    // Physical layout: <scratch>/var/home/u, reached via <scratch>/home -> var/home.
    physicalHome = join(scratch, "var", "home", "u");
    symlinkedHome = join(scratch, "home", "u");
    mkdirSync(join(physicalHome, ".llm-wiki"), { recursive: true });
    writeFileSync(join(physicalHome, ".llm-wiki", "config.json"), "{}");
    symlinkSync("var/home", join(scratch, "home"));
  });

  afterEach(() => {
    if (savedWikiHome === undefined) Reflect.deleteProperty(process.env, "WIKI_HOME");
    else process.env.WIKI_HOME = savedWikiHome;
    if (savedHome === undefined) Reflect.deleteProperty(process.env, "HOME");
    else process.env.HOME = savedHome;
    rmSync(scratch, { recursive: true, force: true });
  });

  it("recognises the personal vault reached through a symlinked root", () => {
    // WIKI_HOME points at the symlinked path; the vault root is the physical
    // one. Before the fix this string compare returned false.
    process.env.WIKI_HOME = symlinkedHome;
    expect(isPersonalVault(getVaultPaths(physicalHome))).toBe(true);
  });

  it("recognises it from the symlinked side too (comparison is symmetric)", () => {
    process.env.WIKI_HOME = physicalHome;
    expect(isPersonalVault(getVaultPaths(symlinkedHome))).toBe(true);
  });

  it("classifies the vault resolved from a physical cwd as personal", () => {
    // The real atomic-OS path: $HOME uses the symlink, but process.cwd() (and
    // so resolveVaultRoot's parent walk) yields the physical path. WIKI_HOME
    // must be unset or it short-circuits the walk.
    Reflect.deleteProperty(process.env, "WIKI_HOME");
    process.env.HOME = symlinkedHome;
    const cwd = join(physicalHome, "proj", "src");
    mkdirSync(cwd, { recursive: true });

    const paths = resolveVaultPaths(cwd);
    expect(paths.root).toBe(physicalHome); // walked up to the physical root
    expect(isPersonalVault(paths)).toBe(true);
  });

  it("still treats a vault nested under the home directory as a project vault", () => {
    // Guards against fixing this with containment instead of equality.
    process.env.WIKI_HOME = symlinkedHome;
    const nested = join(physicalHome, "projects", "foo");
    mkdirSync(join(nested, ".llm-wiki"), { recursive: true });
    expect(isPersonalVault(getVaultPaths(nested))).toBe(false);
  });

  it("still treats an unrelated vault as a project vault", () => {
    process.env.WIKI_HOME = symlinkedHome;
    const unrelated = join(scratch, "var", "srv", "other");
    mkdirSync(join(unrelated, ".llm-wiki"), { recursive: true });
    expect(isPersonalVault(getVaultPaths(unrelated))).toBe(false);
  });

  it("does not throw when the personal root does not exist on disk", () => {
    process.env.WIKI_HOME = join(scratch, "nonexistent", "home");
    expect(() => isPersonalVault(getVaultPaths(physicalHome))).not.toThrow();
    expect(isPersonalVault(getVaultPaths(physicalHome))).toBe(false);
  });
});
