import { describe, it, expect } from "vitest";
import { fuzzyFindText, countOccurrences, generateDiff, findClosestSnippet, findOccurrenceLines, stripLeadingBlankLine, stripTrailingBlankLine, stripBlankEdges, applyDotdotdots, applyMissingLeadingWhitespace, } from "./edit-diff.js";
describe("fuzzyFindText", () => {
    it("finds exact match with usedFuzzy=false", () => {
        const content = "hello world\nfoo bar\n";
        const result = fuzzyFindText(content, "foo bar");
        expect(result.found).toBe(true);
        expect(result.usedFuzzy).toBe(false);
        expect(result.index).toBe(content.indexOf("foo bar"));
        expect(result.matchLength).toBe("foo bar".length);
    });
    it("returns not found for missing text", () => {
        const content = "hello world\n";
        const result = fuzzyFindText(content, "does not exist");
        expect(result.found).toBe(false);
        expect(result.index).toBe(-1);
        expect(result.matchLength).toBe(0);
    });
    it("fuzzy matches trailing whitespace differences with usedFuzzy=true", () => {
        const content = "line one   \nline two\n";
        const search = "line one\nline two";
        const result = fuzzyFindText(content, search);
        expect(result.found).toBe(true);
        expect(result.usedFuzzy).toBe(true);
    });
    it("maps fuzzy matches to the correct original byte range after prior trailing spaces", () => {
        const content = "prefix   \ntarget   \nnext\n";
        const result = fuzzyFindText(content, "target\nnext");
        expect(result.found).toBe(true);
        expect(result.index).toBe(content.indexOf("target"));
        expect(content.slice(result.index, result.index + result.matchLength)).toBe("target   \nnext");
    });
    it("fuzzy matches smart quotes to straight quotes", () => {
        const content = 'She said "hello"';
        const search = "She said \u201Chello\u201D";
        const result = fuzzyFindText(content, search);
        expect(result.found).toBe(true);
        expect(result.usedFuzzy).toBe(true);
    });
});
describe("countOccurrences", () => {
    it("counts single occurrence as 1", () => {
        const content = "abc def ghi";
        expect(countOccurrences(content, "def")).toBe(1);
    });
    it("counts multiple occurrences correctly", () => {
        const content = "aaa bbb aaa ccc aaa";
        expect(countOccurrences(content, "aaa")).toBe(3);
    });
    it("returns 0 for no match", () => {
        const content = "hello world";
        expect(countOccurrences(content, "xyz")).toBe(0);
    });
    it("falls back to fuzzy count when exact is 0", () => {
        // Exact won't match because of smart quotes, fuzzy should find 1
        const contentSmartQuote = "say \u201Chi\u201D and \u201Chi\u201D";
        const searchStraight = 'say "hi"';
        // Exact won't match because of smart quotes, fuzzy should find 1
        expect(countOccurrences(contentSmartQuote, searchStraight)).toBe(1);
    });
});
describe("findClosestSnippet", () => {
    const content = [
        "import { useState } from 'react';",
        "",
        "export function Counter() {",
        "  const [count, setCount] = useState(0);",
        "  return <div>{count}</div>;",
        "}",
    ].join("\n");
    it("finds the closest line by token overlap and returns numbered context", () => {
        const result = findClosestSnippet(content, "const [count, setCount] = useState(1);", 1);
        expect(result).not.toBeNull();
        expect(result.snippet).toContain("useState(0)");
        // Numbered (cat -n style)
        expect(result.snippet).toMatch(/^\s+\d+\t/m);
    });
    it("returns the 1-based line of the strongest candidate for read-suggestion use", () => {
        const result = findClosestSnippet(content, "const [count, setCount] = useState(1);", 1);
        expect(result).not.toBeNull();
        // Line 4 of `content` is `  const [count, setCount] = useState(0);`
        expect(result.topLine).toBe(4);
    });
    it("returns null when there are no shared tokens", () => {
        const result = findClosestSnippet(content, "completely unrelated zzzqqq xxx");
        expect(result).toBeNull();
    });
    it("returns null on empty oldText", () => {
        expect(findClosestSnippet(content, "")).toBeNull();
        expect(findClosestSnippet(content, "   \n\n")).toBeNull();
    });
    it("respects contextLines", () => {
        const result = findClosestSnippet(content, "const [count, setCount] = useState(1);", 0);
        expect(result.snippet).toBe("     4\t  const [count, setCount] = useState(0);");
    });
    it("returns multiple matches separated by --- when several regions tie", () => {
        const multi = [
            "function handleClick() {",
            "  setCount(count + 1);",
            "}",
            "",
            "function handleReset() {",
            "  setCount(0);",
            "}",
            "",
            "function handleDouble() {",
            "  setCount(count * 2);",
            "}",
        ].join("\n");
        const result = findClosestSnippet(multi, "setCount(count - 1);", 0, 3);
        expect(result).not.toBeNull();
        const parts = result.snippet.split("\n---\n");
        expect(parts.length).toBeGreaterThanOrEqual(2);
        // Every part should reference setCount
        for (const p of parts)
            expect(p).toContain("setCount");
    });
    it("keeps a single match when one candidate dominates", () => {
        const result = findClosestSnippet(content, "const [count, setCount] = useState(1);", 1);
        // Only line 4 has the full token set; line 1 (just `useState`) is dropped
        // by the bestScore/3 cutoff.
        expect(result).not.toBeNull();
        expect(result.snippet.split("\n---\n")).toHaveLength(1);
    });
});
describe("findOccurrenceLines", () => {
    it("returns 1-indexed line numbers and trimmed previews for every match", () => {
        const css = [
            ".timer { color: white; }",
            ".button { color: black; }",
            ".label { color: white; }",
            ".footer { color: white; }",
        ].join("\n");
        const matches = findOccurrenceLines(css, "color: white;");
        expect(matches).toEqual([
            { line: 1, preview: ".timer { color: white; }" },
            { line: 3, preview: ".label { color: white; }" },
            { line: 4, preview: ".footer { color: white; }" },
        ]);
    });
    it("caps results at `max` so dozens of matches stay compact", () => {
        const lines = Array.from({ length: 20 }, (_, i) => `row ${i} }`);
        const matches = findOccurrenceLines(lines.join("\n"), "}", 4);
        expect(matches).toHaveLength(4);
        expect(matches[0].line).toBe(1);
    });
    it("falls back to fuzzy matching when exact yields zero", () => {
        const content = "say “hi”";
        const matches = findOccurrenceLines(content, 'say "hi"');
        expect(matches).toHaveLength(1);
        expect(matches[0].line).toBe(1);
    });
    it("returns empty array for no matches at all", () => {
        expect(findOccurrenceLines("hello world", "missing")).toEqual([]);
    });
});
describe("applyMissingLeadingWhitespace", () => {
    it("returns null when text matches exactly (caller already handled it)", () => {
        // The whole point of this strategy is to handle MIS-indented text.
        // When indentation matches, this returns null too — but in practice
        // the primary exact matcher catches that case first.
        const file = "  function foo() {\n    return 1;\n  }\n";
        const old = "function foo() {\n  return 1;\n}";
        const next = "function foo() {\n  return 2;\n}";
        const result = applyMissingLeadingWhitespace(file, old, next);
        expect(result).not.toBeNull();
        expect(result).toContain("return 2;");
        // File's 2-space prefix was preserved on the rewritten lines.
        expect(result).toContain("  function foo() {");
        expect(result).toContain("    return 2;");
        expect(result).toContain("  }");
    });
    it("model omits indentation entirely; file has 4 spaces — apply 4-space prefix to new", () => {
        const file = "    const x = 1;\n    const y = 2;\n    const z = 3;\n";
        const old = "const x = 1;\nconst y = 2;\nconst z = 3;";
        const next = "const x = 10;\nconst y = 20;\nconst z = 30;";
        const result = applyMissingLeadingWhitespace(file, old, next);
        expect(result).toBe("    const x = 10;\n    const y = 20;\n    const z = 30;\n");
    });
    it("model used 2-space indent but file uses 4 — outdents both, finds match, re-indents new", () => {
        const file = "    if (x) {\n      return y;\n    }\n";
        const old = "  if (x) {\n    return y;\n  }";
        const next = "  if (x) {\n    return z;\n  }";
        const result = applyMissingLeadingWhitespace(file, old, next);
        expect(result).toContain("    if (x) {");
        expect(result).toContain("      return z;");
        expect(result).toContain("    }");
    });
    it("returns null when non-leading content differs", () => {
        const file = "  function foo() {\n    return 1;\n  }\n";
        const old = "function bar() {\n  return 1;\n}";
        const next = "function bar() {\n  return 2;\n}";
        expect(applyMissingLeadingWhitespace(file, old, next)).toBeNull();
    });
    it("returns null when leading-whitespace delta is non-uniform across lines", () => {
        // File has 4 spaces on line 1, 6 on line 2 (irregular block).
        const file = "    line one\n      line two\n";
        const old = "line one\nline two";
        const next = "LINE ONE\nLINE TWO";
        // The delta is "    " for line 1 but "      " for line 2 — non-uniform.
        expect(applyMissingLeadingWhitespace(file, old, next)).toBeNull();
    });
    it("returns null on multiple matches (ambiguous)", () => {
        const file = "  foo();\n  bar();\n\n  foo();\n  bar();\n";
        const old = "foo();\nbar();";
        const next = "FOO();\nBAR();";
        expect(applyMissingLeadingWhitespace(file, old, next)).toBeNull();
    });
});
describe("applyDotdotdots", () => {
    const file = [
        "function foo() {",
        "  console.log('keep this');",
        "  doStuff();",
        "  return bar;",
        "}",
        "",
        "function unrelated() { return 0; }",
    ].join("\n");
    it("returns null when old_text has no `...` lines", () => {
        expect(applyDotdotdots(file, "function foo() {", "function FOO() {")).toBeNull();
    });
    it("matches bookends and preserves the elided middle", () => {
        const old = "function foo() {\n  ...\n  return bar;\n}";
        const next = "function foo() {\n  ...\n  return baz;\n}";
        const result = applyDotdotdots(file, old, next);
        expect(result).not.toBeNull();
        expect(result).toContain("return baz;");
        expect(result).not.toContain("return bar;");
        // Middle preserved verbatim.
        expect(result).toContain("console.log('keep this');");
        expect(result).toContain("doStuff();");
    });
    it("matches indented elision bookends when the model omits their common prefix", () => {
        const indented = "  function foo() {\n    keep();\n    return bar;\n  }\n";
        const old = "function foo() {\n  ...\n  return bar;\n}";
        const next = "function foo(): Result {\n  ...\n  return baz;\n}";
        const result = applyDotdotdots(indented, old, next);
        expect(result).toBe("  function foo(): Result {\n    keep();\n    return baz;\n  }\n");
    });
    it("returns null when piece counts mismatch (ambiguous elision)", () => {
        const old = "function foo() {\n  ...\n  return bar;\n}";
        const next = "function foo() {\n  ...\n  ...\n  return baz;\n}";
        expect(applyDotdotdots(file, old, next)).toBeNull();
    });
    it("returns null when bookend piece is missing in the file", () => {
        const old = "function MISSING() {\n  ...\n  return bar;\n}";
        const next = "function MISSING() {\n  ...\n  return baz;\n}";
        expect(applyDotdotdots(file, old, next)).toBeNull();
    });
    it("returns null when a piece would be empty (`...` at boundary)", () => {
        const old = "...\nfunction foo() {";
        const next = "...\nfunction FOO() {";
        expect(applyDotdotdots(file, old, next)).toBeNull();
    });
    it("supports multiple `...` blocks", () => {
        const old = "function foo() {\n  ...\n  doStuff();\n  ...\n  return bar;\n}";
        const next = "function foo() {\n  ...\n  doStuff();\n  ...\n  return baz;\n}";
        const result = applyDotdotdots(file, old, next);
        expect(result).not.toBeNull();
        expect(result).toContain("return baz;");
        // First and second middles both preserved.
        expect(result).toContain("console.log('keep this');");
    });
    it("ignores inline `...` (only line-only dots count)", () => {
        // Inline ... like JS spread should NOT trigger dotdotdots.
        expect(applyDotdotdots(file, "doStuff(...args);", "doStuff(...newArgs);")).toBeNull();
    });
});
describe("stripLeadingBlankLine", () => {
    it("returns null when there is no leading blank line", () => {
        expect(stripLeadingBlankLine("foo\nbar")).toBeNull();
    });
    it("strips a single leading newline", () => {
        expect(stripLeadingBlankLine("\nfoo\nbar")).toBe("foo\nbar");
    });
    it("strips multiple consecutive leading blank lines", () => {
        expect(stripLeadingBlankLine("\n\n\nfoo\nbar")).toBe("foo\nbar");
        expect(stripLeadingBlankLine("\n  \n\tfoo")).toBe("\tfoo");
    });
    it("strips a leading whitespace-only line", () => {
        expect(stripLeadingBlankLine("   \nfoo")).toBe("foo");
    });
    it("returns null on empty input", () => {
        expect(stripLeadingBlankLine("")).toBeNull();
    });
    it("returns null when the text is a single line", () => {
        expect(stripLeadingBlankLine("only one line")).toBeNull();
    });
});
describe("stripTrailingBlankLine", () => {
    it("strips a single trailing newline", () => {
        expect(stripTrailingBlankLine("foo\nbar\n")).toBe("foo\nbar");
    });
    it("strips multiple trailing blank/whitespace-only lines", () => {
        expect(stripTrailingBlankLine("foo\nbar\n\n\n")).toBe("foo\nbar");
        expect(stripTrailingBlankLine("foo\n  \n\t\n")).toBe("foo");
    });
    it("returns null when there is no trailing blank line", () => {
        expect(stripTrailingBlankLine("foo\nbar")).toBeNull();
        expect(stripTrailingBlankLine("single")).toBeNull();
        expect(stripTrailingBlankLine("")).toBeNull();
    });
});
describe("stripBlankEdges", () => {
    it("strips spurious blank lines from both edges", () => {
        expect(stripBlankEdges("\n\n  code();\n  more();\n\n")).toBe("  code();\n  more();");
    });
    it("strips only the edge that has blanks", () => {
        expect(stripBlankEdges("\nfoo\nbar")).toBe("foo\nbar");
        expect(stripBlankEdges("foo\nbar\n\n")).toBe("foo\nbar");
    });
    it("returns null when neither edge has a blank line", () => {
        expect(stripBlankEdges("foo\nbar")).toBeNull();
    });
});
describe("generateDiff", () => {
    it("produces diff with --- a/ and +++ b/ header", () => {
        const diff = generateDiff("hello\n", "hello\nworld\n", "test.txt");
        expect(diff).toContain("--- a/test.txt");
        expect(diff).toContain("+++ b/test.txt");
    });
    it("shows removed lines with - prefix and added lines with + prefix", () => {
        const diff = generateDiff("old line\n", "new line\n", "file.ts");
        expect(diff).toContain("-old line");
        expect(diff).toContain("+new line");
    });
});
//# sourceMappingURL=edit-diff.test.js.map