import { describe, it, expect } from "vitest";
import { parseDiff, detectLanguage } from "./diffParser";

describe("parseDiff", () => {
  describe("empty input", () => {
    it("returns an empty array for an empty string", () => {
      expect(parseDiff("")).toEqual([]);
    });
  });

  describe("file header lines", () => {
    it("classifies diff --git lines as file-header", () => {
      const result = parseDiff("diff --git a/foo.ts b/foo.ts");
      expect(result).toHaveLength(1);
      expect(result[0].type).toBe("file-header");
      expect(result[0].oldLineNumber).toBeNull();
      expect(result[0].newLineNumber).toBeNull();
    });

    it("classifies index lines as file-header", () => {
      const result = parseDiff("index abc123..def456 100644");
      expect(result[0].type).toBe("file-header");
    });

    it("classifies --- lines as file-header", () => {
      const result = parseDiff("--- a/foo.ts");
      expect(result[0].type).toBe("file-header");
    });

    it("classifies +++ lines as file-header", () => {
      const result = parseDiff("+++ b/foo.ts");
      expect(result[0].type).toBe("file-header");
    });

    it("classifies new file mode lines as file-header", () => {
      const result = parseDiff("new file mode 100644");
      expect(result[0].type).toBe("file-header");
    });

    it("classifies deleted file mode lines as file-header", () => {
      const result = parseDiff("deleted file mode 100644");
      expect(result[0].type).toBe("file-header");
    });
  });

  describe("hunk header lines", () => {
    it("classifies @@ lines as hunk-header", () => {
      const result = parseDiff("@@ -1,5 +1,7 @@");
      expect(result).toHaveLength(1);
      expect(result[0].type).toBe("hunk-header");
      expect(result[0].content).toBe("@@ -1,5 +1,7 @@");
      expect(result[0].oldLineNumber).toBeNull();
      expect(result[0].newLineNumber).toBeNull();
    });

    it("sets line counters from the hunk header", () => {
      const diff = "@@ -10,3 +20,3 @@\n-old line\n+new line\n unchanged";
      const result = parseDiff(diff);
      const removed = result.find((l) => l.type === "removed");
      const added = result.find((l) => l.type === "added");
      const context = result.find((l) => l.type === "context");
      expect(removed?.oldLineNumber).toBe(10);
      expect(added?.newLineNumber).toBe(20);
      expect(context?.oldLineNumber).toBe(11);
      expect(context?.newLineNumber).toBe(21);
    });

    it("handles hunk headers with no count (single line, e.g. -1 +1)", () => {
      const diff = "@@ -1 +1 @@\n-old\n+new";
      const result = parseDiff(diff);
      const removed = result.find((l) => l.type === "removed");
      const added = result.find((l) => l.type === "added");
      expect(removed?.oldLineNumber).toBe(1);
      expect(added?.newLineNumber).toBe(1);
    });
  });

  describe("added lines", () => {
    it("classifies + lines as added", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n+new line");
      const added = result.find((l) => l.type === "added");
      expect(added).toBeDefined();
      expect(added?.content).toBe("new line");
    });

    it("strips the leading + from content", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n+  indented code");
      const added = result.find((l) => l.type === "added");
      expect(added?.content).toBe("  indented code");
    });

    it("has null oldLineNumber for added lines", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n+line");
      const added = result.find((l) => l.type === "added");
      expect(added?.oldLineNumber).toBeNull();
    });

    it("increments newLineNumber across multiple added lines", () => {
      const diff = "@@ -1,1 +1,3 @@\n+first\n+second\n+third";
      const result = parseDiff(diff);
      const added = result.filter((l) => l.type === "added");
      expect(added[0].newLineNumber).toBe(1);
      expect(added[1].newLineNumber).toBe(2);
      expect(added[2].newLineNumber).toBe(3);
    });
  });

  describe("removed lines", () => {
    it("classifies - lines as removed", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n-old line");
      const removed = result.find((l) => l.type === "removed");
      expect(removed).toBeDefined();
      expect(removed?.content).toBe("old line");
    });

    it("strips the leading - from content", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n-  indented code");
      const removed = result.find((l) => l.type === "removed");
      expect(removed?.content).toBe("  indented code");
    });

    it("has null newLineNumber for removed lines", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n-line");
      const removed = result.find((l) => l.type === "removed");
      expect(removed?.newLineNumber).toBeNull();
    });

    it("increments oldLineNumber across multiple removed lines", () => {
      const diff = "@@ -5,3 +5,1 @@\n-first\n-second\n-third";
      const result = parseDiff(diff);
      const removed = result.filter((l) => l.type === "removed");
      expect(removed[0].oldLineNumber).toBe(5);
      expect(removed[1].oldLineNumber).toBe(6);
      expect(removed[2].oldLineNumber).toBe(7);
    });
  });

  describe("context lines", () => {
    it("classifies space-prefixed lines as context", () => {
      const result = parseDiff("@@ -1,1 +1,1 @@\n unchanged line");
      const context = result.find((l) => l.type === "context");
      expect(context).toBeDefined();
      expect(context?.content).toBe("unchanged line");
    });

    it("has both line numbers for context lines", () => {
      const diff = "@@ -3,1 +5,1 @@\n context";
      const result = parseDiff(diff);
      const context = result.find((l) => l.type === "context");
      expect(context?.oldLineNumber).toBe(3);
      expect(context?.newLineNumber).toBe(5);
    });

    it("increments both line numbers across context lines", () => {
      const diff = "@@ -1,3 +1,3 @@\n line1\n line2\n line3";
      const result = parseDiff(diff);
      const contexts = result.filter((l) => l.type === "context");
      expect(contexts[0].oldLineNumber).toBe(1);
      expect(contexts[0].newLineNumber).toBe(1);
      expect(contexts[2].oldLineNumber).toBe(3);
      expect(contexts[2].newLineNumber).toBe(3);
    });
  });

  describe("no-newline marker", () => {
    it("classifies the no-newline marker correctly", () => {
      const result = parseDiff("\\ No newline at end of file");
      expect(result[0].type).toBe("no-newline");
      expect(result[0].content).toBe("\\ No newline at end of file");
    });
  });

  describe("line number tracking across mixed lines", () => {
    it("tracks old and new line numbers correctly through a realistic diff", () => {
      const diff = [
        "@@ -10,6 +10,7 @@",
        " context one",
        "-removed line",
        "+added line one",
        "+added line two",
        " context two",
        " context three",
      ].join("\n");

      const result = parseDiff(diff);
      const lines = result.filter((l) => l.type !== "hunk-header");

      expect(lines[0]).toMatchObject({ type: "context", oldLineNumber: 10, newLineNumber: 10 });
      expect(lines[1]).toMatchObject({ type: "removed", oldLineNumber: 11, newLineNumber: null });
      expect(lines[2]).toMatchObject({ type: "added", oldLineNumber: null, newLineNumber: 11 });
      expect(lines[3]).toMatchObject({ type: "added", oldLineNumber: null, newLineNumber: 12 });
      expect(lines[4]).toMatchObject({ type: "context", oldLineNumber: 12, newLineNumber: 13 });
      expect(lines[5]).toMatchObject({ type: "context", oldLineNumber: 13, newLineNumber: 14 });
    });

    it("resets line counters on a second hunk header", () => {
      const diff = [
        "@@ -1,1 +1,1 @@",
        " context",
        "@@ -50,1 +50,1 @@",
        " second hunk context",
      ].join("\n");

      const result = parseDiff(diff);
      const secondContext = result[result.length - 1];
      expect(secondContext.type).toBe("context");
      expect(secondContext.oldLineNumber).toBe(50);
      expect(secondContext.newLineNumber).toBe(50);
    });
  });
});

describe("detectLanguage", () => {
  it("detects TypeScript from .ts extension", () => {
    expect(detectLanguage("src/foo.ts")).toBe("typescript");
  });

  it("detects TypeScript from .tsx extension", () => {
    expect(detectLanguage("src/foo.tsx")).toBe("typescript");
  });

  it("detects JavaScript from .js extension", () => {
    expect(detectLanguage("src/foo.js")).toBe("javascript");
  });

  it("detects Rust from .rs extension", () => {
    expect(detectLanguage("src-tauri/src/lib.rs")).toBe("rust");
  });

  it("detects Python from .py extension", () => {
    expect(detectLanguage("script.py")).toBe("python");
  });

  it("detects CSS from .css extension", () => {
    expect(detectLanguage("style.css")).toBe("css");
  });

  it("detects JSON from .json extension", () => {
    expect(detectLanguage("package.json")).toBe("json");
  });

  it("detects YAML from .yaml extension", () => {
    expect(detectLanguage("config.yaml")).toBe("yaml");
  });

  it("detects YAML from .yml extension", () => {
    expect(detectLanguage(".github/workflows/ci.yml")).toBe("yaml");
  });

  it("detects bash from .sh extension", () => {
    expect(detectLanguage("check-all.sh")).toBe("bash");
  });

  it("uses plaintext for unknown extensions", () => {
    expect(detectLanguage("file.xyz")).toBe("plaintext");
  });

  it("uses plaintext when there is no extension", () => {
    expect(detectLanguage("Makefile")).toBe("plaintext");
  });

  it("handles paths with multiple dots correctly (uses last segment)", () => {
    expect(detectLanguage("my.config.ts")).toBe("typescript");
  });

  it("is case-insensitive for extension detection", () => {
    expect(detectLanguage("README.MD")).toBe("markdown");
  });
});
