import { describe, expect, test } from "bun:test";
import { validateDiff } from "@/pipeline/safety.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Build a diff for a single file with `added` "+" lines and `removed` "-" lines. */
function makeSingleFileDiff(
  path: string,
  added: number,
  removed: number,
  opts?: { deleted?: boolean },
): string {
  const rmLines = Array.from({ length: removed }, (_, i) => `-removed ${i}`).join("\n");
  const addLines = Array.from({ length: added }, (_, i) => `+added ${i}`).join("\n");
  const deletedMarker = opts?.deleted ? "deleted file mode 100644\n" : "";
  const hunkBody = [rmLines, addLines].filter(Boolean).join("\n");
  return [
    `diff --git a/${path} b/${path}`,
    `${deletedMarker}index 1111..2222 100644`,
    `--- a/${path}`,
    `+++ b/${path}`,
    `@@ -1,${removed} +1,${added} @@`,
    hunkBody,
    "",
  ].join("\n");
}

/** Build a diff containing `n` distinct files, each with 1 added line. */
function makeNFiles(n: number): string {
  return Array.from({ length: n }, (_, i) =>
    [
      `diff --git a/f${i}.ts b/f${i}.ts`,
      "index 1111..2222 100644",
      `--- a/f${i}.ts`,
      `+++ b/f${i}.ts`,
      "@@ -1,1 +1,2 @@",
      " context",
      "+added line",
      "",
    ].join("\n"),
  ).join("");
}

/** Build a diff with `n` deleted files. */
function makeNDeletedFiles(n: number): string {
  return Array.from({ length: n }, (_, i) =>
    [
      `diff --git a/del${i}.ts b/del${i}.ts`,
      "deleted file mode 100644",
      "index 1111..0000",
      `--- a/del${i}.ts`,
      "+++ /dev/null",
      "@@ -1,2 +0,0 @@",
      "-old line 1",
      "-old line 2",
      "",
    ].join("\n"),
  ).join("");
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("validateDiff", () => {
  // 1. Empty diff
  test("returns a clean report for an empty diff", () => {
    const report = validateDiff("");
    expect(report).toEqual({
      passed: true,
      totalLinesChanged: 0,
      filesChanged: 0,
      filesDeleted: 0,
      violations: [],
    });
  });

  // 2. Single-file small diff
  test("counts lines correctly for a single small file", () => {
    const diff = makeSingleFileDiff("src/foo.ts", 10, 5);
    const report = validateDiff(diff);
    expect(report.totalLinesChanged).toBe(15);
    expect(report.filesChanged).toBe(1);
    expect(report.filesDeleted).toBe(0);
    expect(report.passed).toBe(true);
    expect(report.violations).toEqual([]);
  });

  // 3. Exactly at MAX_DIFF_LINES (500) — boundary passes
  test("passes when total lines changed equals exactly MAX_DIFF_LINES (500)", () => {
    const diff = makeSingleFileDiff("src/big.ts", 250, 250);
    const report = validateDiff(diff);
    expect(report.totalLinesChanged).toBe(500);
    expect(report.passed).toBe(true);
    expect(report.violations).toEqual([]);
  });

  // 4. Over MAX_DIFF_LINES
  test("fails when total lines changed exceeds MAX_DIFF_LINES", () => {
    const diff = makeSingleFileDiff("src/big.ts", 251, 250);
    const report = validateDiff(diff);
    expect(report.totalLinesChanged).toBe(501);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("Diff exceeds 500 lines (501 changed)");
  });

  // 5. Exactly at MAX_FILES_CHANGED (10) — boundary passes
  test("passes when files changed equals exactly MAX_FILES_CHANGED (10)", () => {
    const diff = makeNFiles(10);
    const report = validateDiff(diff);
    expect(report.filesChanged).toBe(10);
    expect(report.passed).toBe(true);
    expect(report.violations).toEqual([]);
  });

  // 6. Over MAX_FILES_CHANGED
  test("fails when files changed exceeds MAX_FILES_CHANGED", () => {
    const diff = makeNFiles(11);
    const report = validateDiff(diff);
    expect(report.filesChanged).toBe(11);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("More than 10 files changed (11)");
  });

  // 7. Over MAX_FILES_DELETED (3)
  test("fails when deleted files exceed MAX_FILES_DELETED", () => {
    const diff = makeNDeletedFiles(4);
    const report = validateDiff(diff);
    expect(report.filesDeleted).toBe(4);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("More than 3 files deleted (4)");
  });

  // 8. Forbidden path — .github/workflows/ci.yml
  test("flags modification of .github/workflows/ci.yml as forbidden path", () => {
    const diff = makeSingleFileDiff(".github/workflows/ci.yml", 1, 0);
    const report = validateDiff(diff);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("Modifies forbidden path: .github/workflows/ci.yml");
  });

  // 9. Forbidden path — .gitlab-ci.yml
  test("flags modification of .gitlab-ci.yml as forbidden path", () => {
    const diff = makeSingleFileDiff(".gitlab-ci.yml", 1, 0);
    const report = validateDiff(diff);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("Modifies forbidden path: .gitlab-ci.yml");
  });

  // 10. Lock file — package-lock.json
  test("flags modification of package-lock.json as lock file", () => {
    const diff = makeSingleFileDiff("package-lock.json", 2, 1);
    const report = validateDiff(diff);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("Modifies lock file: package-lock.json");
  });

  // 11. Lock file — bun.lock
  test("flags modification of bun.lock as lock file", () => {
    const diff = makeSingleFileDiff("bun.lock", 3, 2);
    const report = validateDiff(diff);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual("Modifies lock file: bun.lock");
  });

  // 12. Combined: forbidden path + lock file in same diff
  test("reports both forbidden path and lock file violations in same diff", () => {
    const forbiddenDiff = makeSingleFileDiff(".github/workflows/deploy.yml", 1, 0);
    const lockDiff = makeSingleFileDiff("yarn.lock", 5, 3);
    const combined = forbiddenDiff + lockDiff;
    const report = validateDiff(combined);
    expect(report.passed).toBe(false);
    expect(report.violations).toContainEqual(
      "Modifies forbidden path: .github/workflows/deploy.yml",
    );
    expect(report.violations).toContainEqual("Modifies lock file: yarn.lock");
    expect(report.violations.length).toBeGreaterThanOrEqual(2);
  });

  // 13. Multi-hunk file — two hunks counted as 1 file, lines aggregated
  test("aggregates lines across multiple hunks in a single file", () => {
    const diff = `diff --git a/src/multi.ts b/src/multi.ts
index 1111..2222 100644
--- a/src/multi.ts
+++ b/src/multi.ts
@@ -1,3 +1,5 @@
 context
-removed A1
+added A1
+added A2
+added A3
 context
@@ -20,3 +22,4 @@
 context
-removed B1
-removed B2
+added B1
 context
`;
    const report = validateDiff(diff);
    expect(report.filesChanged).toBe(1);
    // Hunk 1: 3 added + 1 removed = 4. Hunk 2: 1 added + 2 removed = 3. Total = 7.
    expect(report.totalLinesChanged).toBe(7);
    expect(report.passed).toBe(true);
  });

  // 14. +++ /dev/null and --- /dev/null markers are NOT counted as changed lines
  test("does not count +++ and --- header lines as changed lines", () => {
    // New file diff: --- /dev/null appears before the hunk
    const newFileDiff = `diff --git a/src/brand-new.ts b/src/brand-new.ts
new file mode 100644
index 0000..1111
--- /dev/null
+++ b/src/brand-new.ts
@@ -0,0 +1,3 @@
+line one
+line two
+line three
`;
    const report = validateDiff(newFileDiff);
    expect(report.filesChanged).toBe(1);
    // Only the 3 "+" lines inside the hunk, not the "--- /dev/null" or "+++ b/..." headers
    expect(report.totalLinesChanged).toBe(3);
    expect(report.filesDeleted).toBe(0);
    expect(report.passed).toBe(true);
  });

  // 15. Realistic multi-file diff (mixed add/delete/modify) under all limits
  test("passes a realistic multi-file diff under all limits with correct counts", () => {
    const modifyA = `diff --git a/src/app.ts b/src/app.ts
index 1111..2222 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -10,4 +10,6 @@
 import { foo } from "./foo";
-const old = true;
+const updated = false;
+const extra = 42;
 export default app;
`;
    const deleteB = `diff --git a/src/deprecated.ts b/src/deprecated.ts
deleted file mode 100644
index 3333..0000
--- a/src/deprecated.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { bar } from "./bar";
-export function deprecated() {}
-deprecated();
-// end
`;
    const addC = `diff --git a/src/new-util.ts b/src/new-util.ts
new file mode 100644
index 0000..4444
--- /dev/null
+++ b/src/new-util.ts
@@ -0,0 +1,5 @@
+export function helper() {
+  return 1;
+}
+
+export const VERSION = "1.0";
`;
    const modifyD = `diff --git a/src/config.ts b/src/config.ts
index 5555..6666 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,2 +1,3 @@
 const PORT = 3000;
+const HOST = "localhost";
 export { PORT };
`;

    const combined = modifyA + deleteB + addC + modifyD;
    const report = validateDiff(combined);

    expect(report.filesChanged).toBe(4);
    expect(report.filesDeleted).toBe(1);
    // modifyA: 2 added + 1 removed = 3
    // deleteB: 0 added + 4 removed = 4
    // addC: 5 added + 0 removed = 5
    // modifyD: 1 added + 0 removed = 1
    // Total = 3 + 4 + 5 + 1 = 13
    expect(report.totalLinesChanged).toBe(13);
    expect(report.passed).toBe(true);
    expect(report.violations).toEqual([]);
  });
});
