import { Hono } from "hono";
import { describe, it, expect, vi, beforeEach } from "vitest";

/* ── Mocks ────────────────────────────────────────────────────────── */

const mockGitUtils = vi.hoisted(() => ({
  getRepoInfo: vi.fn().mockResolvedValue(null),
  listBranches: vi.fn().mockResolvedValue([]),
  gitFetch: vi.fn().mockResolvedValue({ success: true, output: "" }),
  listWorktrees: vi.fn().mockResolvedValue([]),
  ensureWorktree: vi.fn().mockResolvedValue({ path: "/tmp/wt", created: true }),
  removeWorktree: vi.fn().mockResolvedValue({ success: true }),
  gitPull: vi.fn().mockResolvedValue({ success: true, output: "" }),
}));

vi.mock("../git-utils.js", () => mockGitUtils);

// Mock child_process.execSync for the git pull rev-list call
const mockExecSync = vi.hoisted(() => vi.fn().mockReturnValue("0\t0"));
vi.mock("node:child_process", () => ({
  execSync: mockExecSync,
}));

// Mock github-pr dynamic import used in pr-status route
vi.mock("../github-pr.js", () => ({
  initGhAvailability: vi.fn().mockResolvedValue(false),
  fetchPRInfoAsync: vi.fn().mockResolvedValue(null),
}));

/* ── App setup ────────────────────────────────────────────────────── */

import { registerGitRoutes } from "./git-routes.js";

let app: Hono;

const mockPrPoller = {
  getCached: vi.fn().mockReturnValue(null),
};

beforeEach(() => {
  vi.restoreAllMocks();
  mockGitUtils.getRepoInfo.mockResolvedValue(null);
  mockGitUtils.listBranches.mockResolvedValue([]);
  mockGitUtils.gitFetch.mockResolvedValue({ success: true, output: "" });
  mockGitUtils.listWorktrees.mockResolvedValue([]);
  mockGitUtils.ensureWorktree.mockResolvedValue({ path: "/tmp/wt", created: true });
  mockGitUtils.removeWorktree.mockResolvedValue({ success: true });
  mockGitUtils.gitPull.mockResolvedValue({ success: true, output: "" });
  mockExecSync.mockReturnValue("0\t0");
  mockPrPoller.getCached.mockReturnValue(null);
  app = new Hono();
  registerGitRoutes(
    app,
    mockPrPoller as unknown as import("../pr-poller.js").PRPoller,
  );
});

/* ── Helpers ──────────────────────────────────────────────────────── */

function req(method: string, path: string, body?: unknown) {
  const init: RequestInit = { method, headers: {} };
  if (body) {
    (init.headers as Record<string, string>)["content-type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  return app.request(path, init);
}

/* ── Tests ────────────────────────────────────────────────────────── */

describe("GET /git/repo-info", () => {
  it("returns 400 when path is missing", async () => {
    const res = await req("GET", "/git/repo-info");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toBe("path required");
  });

  it("returns 400 when not a git repo", async () => {
    const res = await req("GET", "/git/repo-info?path=/tmp");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toBe("Not a git repository");
  });

  it("returns repo info", async () => {
    const info = { repoRoot: "/repo", branch: "main", remote: "origin" };
    mockGitUtils.getRepoInfo.mockResolvedValue(info);
    const res = await req("GET", "/git/repo-info?path=/repo");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.repoRoot).toBe("/repo");
  });
});

describe("GET /git/branches", () => {
  it("returns 400 when repoRoot is missing", async () => {
    const res = await req("GET", "/git/branches");
    expect(res.status).toBe(400);
  });

  it("returns branches", async () => {
    mockGitUtils.listBranches.mockResolvedValue([
      { name: "main", current: true },
    ]);
    const res = await req("GET", "/git/branches?repoRoot=/repo");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
    expect(body[0].name).toBe("main");
  });

  it("returns 500 on error", async () => {
    mockGitUtils.listBranches.mockRejectedValue(new Error("git error"));
    const res = await req("GET", "/git/branches?repoRoot=/repo");
    expect(res.status).toBe(500);
    const body = await res.json();
    expect(body.error).toBe("git error");
  });
});

describe("POST /git/fetch", () => {
  it("returns fetch result", async () => {
    mockGitUtils.gitFetch.mockResolvedValue({ success: true, output: "ok" });
    const res = await req("POST", "/git/fetch", { repoRoot: "/repo" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.success).toBe(true);
  });

  it("returns 400 on validation error (missing repoRoot)", async () => {
    const res = await req("POST", "/git/fetch", {});
    expect(res.status).toBe(400);
  });
});

describe("GET /git/worktrees", () => {
  it("returns 400 when repoRoot is missing", async () => {
    const res = await req("GET", "/git/worktrees");
    expect(res.status).toBe(400);
  });

  it("returns worktrees", async () => {
    mockGitUtils.listWorktrees.mockResolvedValue([
      { path: "/repo", branch: "main", bare: false },
    ]);
    const res = await req("GET", "/git/worktrees?repoRoot=/repo");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveLength(1);
  });
});

describe("POST /git/worktree", () => {
  it("creates a worktree", async () => {
    const res = await req("POST", "/git/worktree", {
      repoRoot: "/repo",
      branch: "feature/test",
    });
    expect(res.status).toBe(200);
    expect(mockGitUtils.ensureWorktree).toHaveBeenCalledWith(
      "/repo",
      "feature/test",
      { baseBranch: undefined, createBranch: undefined },
    );
  });

  it("returns 400 on validation error", async () => {
    const res = await req("POST", "/git/worktree", { repoRoot: "/repo" });
    expect(res.status).toBe(400);
  });
});

describe("DELETE /git/worktree", () => {
  it("removes a worktree", async () => {
    const res = await req("DELETE", "/git/worktree", {
      repoRoot: "/repo",
      worktreePath: "/repo-wt",
    });
    expect(res.status).toBe(200);
    expect(mockGitUtils.removeWorktree).toHaveBeenCalledWith(
      "/repo",
      "/repo-wt",
      { force: undefined },
    );
  });
});

describe("POST /git/pull", () => {
  it("returns pull result with ahead/behind counts", async () => {
    mockGitUtils.gitPull.mockResolvedValue({ success: true, output: "ok" });
    mockExecSync.mockReturnValue("2\t3");
    const res = await req("POST", "/git/pull", { cwd: "/repo" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.success).toBe(true);
    expect(body.git_behind).toBe(2);
    expect(body.git_ahead).toBe(3);
  });

  it("returns 400 on validation error", async () => {
    const res = await req("POST", "/git/pull", {});
    expect(res.status).toBe(400);
  });

  it("handles missing upstream gracefully", async () => {
    mockGitUtils.gitPull.mockResolvedValue({ success: true, output: "" });
    mockExecSync.mockImplementation(() => {
      throw new Error("no upstream");
    });
    const res = await req("POST", "/git/pull", { cwd: "/repo" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.git_ahead).toBe(0);
    expect(body.git_behind).toBe(0);
  });
});

describe("GET /git/pr-status", () => {
  it("returns 400 when cwd or branch is missing", async () => {
    const res = await req("GET", "/git/pr-status?cwd=/repo");
    expect(res.status).toBe(400);
  });

  it("returns cached PR status from poller", async () => {
    const cached = { available: true, pr: { number: 1, title: "Test" } };
    mockPrPoller.getCached.mockReturnValue(cached);
    const res = await req("GET", "/git/pr-status?cwd=/repo&branch=main");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.pr.number).toBe(1);
  });

  it("returns unavailable when gh is not available", async () => {
    const res = await req("GET", "/git/pr-status?cwd=/repo&branch=main");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.available).toBe(false);
  });
});
