import { readFile } from "node:fs/promises";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { compareBranches, getDiff } from "./getDiff";
import * as utils from "./utils";

vi.mock("./utils", async (importOriginal) => {
  const actual = await importOriginal<typeof utils>();
  return {
    ...actual,
    executeGitCommand: vi.fn(),
  };
});

vi.mock("node:fs/promises");

describe("getDiff", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("正常系", () => {
    it("2つのブランチ間のdiffを取得できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = `5\t2\tsrc/file1.ts
10\t0\tsrc/file2.ts`;

      const mockDiffOutput = `diff --git a/src/file1.ts b/src/file1.ts
index abc123..def456 100644
--- a/src/file1.ts
+++ b/src/file1.ts
@@ -1,5 +1,8 @@
 function hello() {
-  console.log("old");
+  console.log("new");
+  console.log("added line 1");
+  console.log("added line 2");
 }
diff --git a/src/file2.ts b/src/file2.ts
new file mode 100644
index 0000000..ghi789
--- /dev/null
+++ b/src/file2.ts
@@ -0,0 +1,10 @@
+export const newFile = true;`;

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(2);
        expect(result.data.files[0]?.filePath).toBe("src/file1.ts");
        expect(result.data.files[0]?.status).toBe("modified");
        expect(result.data.files[0]?.additions).toBe(5);
        expect(result.data.files[0]?.deletions).toBe(2);

        expect(result.data.files[1]?.filePath).toBe("src/file2.ts");
        expect(result.data.files[1]?.status).toBe("added");
        expect(result.data.files[1]?.additions).toBe(10);
        expect(result.data.files[1]?.deletions).toBe(0);

        expect(result.data.summary.totalFiles).toBe(2);
        expect(result.data.summary.totalAdditions).toBe(15);
        expect(result.data.summary.totalDeletions).toBe(2);
      }

      expect(utils.executeGitCommand).toHaveBeenCalledWith(
        ["diff", "--numstat", "main", "feature"],
        mockCwd,
      );

      expect(utils.executeGitCommand).toHaveBeenCalledWith(
        ["diff", "--unified=5", "main", "feature"],
        mockCwd,
      );
    });

    it("HEADとworking directoryの比較ができる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:HEAD";
      const toRef = "compare:working";

      const mockNumstatOutput = `3\t1\tsrc/modified.ts`;
      const mockDiffOutput = `diff --git a/src/modified.ts b/src/modified.ts
index abc123..def456 100644
--- a/src/modified.ts
+++ b/src/modified.ts
@@ -1,3 +1,5 @@
 const value = 1;`;

      const mockStatusOutput = `## main
M  src/modified.ts
?? src/untracked.ts`;

      vi.mocked(readFile).mockResolvedValue("line1\nline2\nline3");

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockStatusOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        // modified file + untracked file
        expect(result.data.files.length).toBeGreaterThanOrEqual(1);
        const modifiedFile = result.data.files.find(
          (f) => f.filePath === "src/modified.ts",
        );
        expect(modifiedFile).toBeDefined();
        expect(modifiedFile?.status).toBe("modified");
      }
    });

    it("同一refの場合は空の結果を返す", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:main";

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(0);
        expect(result.data.diffs).toHaveLength(0);
        expect(result.data.summary.totalFiles).toBe(0);
        expect(result.data.summary.totalAdditions).toBe(0);
        expect(result.data.summary.totalDeletions).toBe(0);
      }

      expect(utils.executeGitCommand).not.toHaveBeenCalled();
    });

    it.skip("削除されたファイルを処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = `0\t10\tsrc/deleted.ts`;
      const mockDiffOutput = `diff --git a/src/deleted.ts b/src/deleted.ts
deleted file mode 100644
index abc123..0000000 100644
--- a/src/deleted.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-deleted line 1
-deleted line 2
-deleted line 3
-deleted line 4
-deleted line 5
-deleted line 6
-deleted line 7
-deleted line 8
-deleted line 9
-deleted line 10`;

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(1);
        expect(result.data.files[0]?.filePath).toBe("src/deleted.ts");
        expect(result.data.files[0]?.status).toBe("deleted");
        expect(result.data.files[0]?.additions).toBe(0);
        expect(result.data.files[0]?.deletions).toBe(10);
      }
    });

    it.skip("名前変更されたファイルを処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = `0\t0\tnew-name.ts`;
      const mockDiffOutput = `diff --git a/old-name.ts b/new-name.ts
similarity index 100%
rename from old-name.ts
rename to new-name.ts
index abc123..abc123 100644`;

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(1);
        expect(result.data.files[0]?.status).toBe("renamed");
        expect(result.data.files[0]?.filePath).toBe("new-name.ts");
        expect(result.data.files[0]?.oldPath).toBe("old-name.ts");
      }
    });

    it("空のdiffを処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = "";
      const mockDiffOutput = "";

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(0);
        expect(result.data.diffs).toHaveLength(0);
        expect(result.data.summary.totalFiles).toBe(0);
      }
    });
  });

  describe("エラー系", () => {
    it("ディレクトリが存在しない場合", async () => {
      const mockCwd = "/nonexistent/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      vi.mocked(utils.executeGitCommand).mockResolvedValue({
        success: false,
        error: {
          code: "NOT_A_REPOSITORY",
          message: `Directory does not exist: ${mockCwd}`,
          command: "git diff --numstat main feature",
        },
      });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe("NOT_A_REPOSITORY");
        expect(result.error.message).toContain("Directory does not exist");
      }
    });

    it("Gitリポジトリでない場合", async () => {
      const mockCwd = "/test/not-a-repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      vi.mocked(utils.executeGitCommand).mockResolvedValue({
        success: false,
        error: {
          code: "NOT_A_REPOSITORY",
          message: `Not a git repository: ${mockCwd}`,
          command: "git diff --numstat main feature",
        },
      });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe("NOT_A_REPOSITORY");
        expect(result.error.message).toContain("Not a git repository");
      }
    });

    it("ブランチが見つからない場合", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:nonexistent";
      const toRef = "compare:feature";

      vi.mocked(utils.executeGitCommand).mockResolvedValue({
        success: false,
        error: {
          code: "BRANCH_NOT_FOUND",
          message: "Branch or commit not found",
          command: "git diff --numstat nonexistent feature",
          stderr:
            "fatal: ambiguous argument 'nonexistent': unknown revision or path not in the working tree.",
        },
      });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe("BRANCH_NOT_FOUND");
        expect(result.error.message).toBe("Branch or commit not found");
      }
    });

    it("numstatコマンドが失敗した場合", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      vi.mocked(utils.executeGitCommand).mockResolvedValue({
        success: false,
        error: {
          code: "COMMAND_FAILED",
          message: "Command failed",
          command: "git diff --numstat main feature",
          stderr: "fatal: bad revision",
        },
      });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.code).toBe("COMMAND_FAILED");
      }
    });

    it("無効なfromRefの場合", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "invalidref";
      const toRef = "compare:feature";

      await expect(getDiff(mockCwd, fromRef, toRef)).rejects.toThrow(
        "Invalid ref text",
      );
    });
  });

  describe("エッジケース", () => {
    it("特殊文字を含むファイル名を処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = `5\t2\tsrc/file with spaces.ts
3\t1\tsrc/日本語ファイル.ts`;

      const mockDiffOutput = `diff --git a/src/file with spaces.ts b/src/file with spaces.ts
index abc123..def456 100644
--- a/src/file with spaces.ts
+++ b/src/file with spaces.ts
@@ -1,3 +1,5 @@
 content
diff --git a/src/日本語ファイル.ts b/src/日本語ファイル.ts
index abc123..def456 100644
--- a/src/日本語ファイル.ts
+++ b/src/日本語ファイル.ts
@@ -1,2 +1,3 @@
 content`;

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(2);
        expect(result.data.files[0]?.filePath).toBe("src/file with spaces.ts");
        expect(result.data.files[1]?.filePath).toBe("src/日本語ファイル.ts");
      }
    });

    it("バイナリファイルの変更を処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = `-\t-\timage.png`;
      const mockDiffOutput = `diff --git a/image.png b/image.png
index abc123..def456 100644
Binary files a/image.png and b/image.png differ`;

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(1);
        expect(result.data.files[0]?.filePath).toBe("image.png");
        expect(result.data.files[0]?.additions).toBe(0);
        expect(result.data.files[0]?.deletions).toBe(0);
      }
    });

    it("大量のファイル変更を処理できる", async () => {
      const mockCwd = "/test/repo";
      const fromRef = "base:main";
      const toRef = "compare:feature";

      const mockNumstatOutput = Array.from(
        { length: 100 },
        (_, i) => `1\t1\tfile${i}.ts`,
      ).join("\n");
      const mockDiffOutput = Array.from(
        { length: 100 },
        (_, i) => `diff --git a/file${i}.ts b/file${i}.ts
index abc123..def456 100644
--- a/file${i}.ts
+++ b/file${i}.ts
@@ -1 +1 @@
-old
+new`,
      ).join("\n");

      vi.mocked(utils.executeGitCommand)
        .mockResolvedValueOnce({
          success: true,
          data: mockNumstatOutput,
        })
        .mockResolvedValueOnce({
          success: true,
          data: mockDiffOutput,
        });

      const result = await getDiff(mockCwd, fromRef, toRef);

      expect(result.success).toBe(true);
      if (result.success) {
        expect(result.data.files).toHaveLength(100);
        expect(result.data.summary.totalFiles).toBe(100);
        expect(result.data.summary.totalAdditions).toBe(100);
        expect(result.data.summary.totalDeletions).toBe(100);
      }
    });
  });
});

describe("compareBranches", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("getDiffのショートハンドとして機能する", async () => {
    const mockCwd = "/test/repo";
    const baseBranch = "base:main";
    const targetBranch = "compare:feature";

    const mockNumstatOutput = `5\t2\tfile.ts`;
    const mockDiffOutput = `diff --git a/file.ts b/file.ts
index abc123..def456 100644
--- a/file.ts
+++ b/file.ts
@@ -1,2 +1,5 @@
 content`;

    vi.mocked(utils.executeGitCommand)
      .mockResolvedValueOnce({
        success: true,
        data: mockNumstatOutput,
      })
      .mockResolvedValueOnce({
        success: true,
        data: mockDiffOutput,
      });

    const result = await compareBranches(mockCwd, baseBranch, targetBranch);

    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.files).toHaveLength(1);
      expect(result.data.files[0]?.filePath).toBe("file.ts");
    }
  });
});
