import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
import { Hono } from "hono";
import { mockLogger } from "../../_helpers/mock-logger.js";
import { RepoManagerImpl } from "@/repo/manager.js";
import * as repoManagerMod from "@/repo/manager.js";
import { GitHubClientImpl } from "@/github/client.js";
import { createDebugRoutes } from "@/server/routes/debug.js";

// ── Use spyOn instead of mock.module — mock.module pollutes the shared module
// cache in Bun and leaks into test/repo/manager.test.ts when run in the same process.
// @/github/client.js and @/scheduler/daily-report.js have dedicated test files —
// use spyOn(GitHubClientImpl.prototype, ...) per-test for GitHub; daily-report
// endpoints are not tested in this file.

beforeEach(() => {
  spyOn(RepoManagerImpl.prototype, "ensureCloned").mockResolvedValue(undefined as never);
  spyOn(RepoManagerImpl.prototype, "checkoutBase" as never).mockResolvedValue(undefined as never);
  spyOn(RepoManagerImpl.prototype, "installDependencies").mockResolvedValue(undefined as never);
  spyOn(RepoManagerImpl.prototype, "validateBuild").mockResolvedValue({
    success: true,
  } as never);
  spyOn(RepoManagerImpl.prototype, "getLocalPath").mockReturnValue("/tmp/workdir/my-api" as never);
  spyOn(RepoManagerImpl.prototype, "cleanStaleBranches").mockResolvedValue(undefined as never);
  spyOn(repoManagerMod as never, "configureGitAuth").mockResolvedValue(undefined);
  spyOn(repoManagerMod as never, "syncAllRepos").mockResolvedValue([]);
});

afterEach(() => mock.restore());

// ── Prototype spy management ──────────────────────────────────────────────────

type GhSpy = ReturnType<typeof spyOn>;

interface GhSpies {
  listOpenPrs: GhSpy;
  getPrDiff: GhSpy;
  listIssueComments: GhSpy;
  listPullReviewComments: GhSpy;
  listPullReviews: GhSpy;
  createPr: GhSpy;
  addLabels: GhSpy;
  listRepoLabels: GhSpy;
  getPullReviewComment: GhSpy;
  createIssueComment: GhSpy;
  createReviewCommentReply: GhSpy;
  getPrForBranch: GhSpy;
}

let ghSpies: GhSpies;

function installGhSpies(): GhSpies {
  return {
    listOpenPrs: spyOn(GitHubClientImpl.prototype, "listOpenPrs").mockResolvedValue([]),
    getPrDiff: spyOn(GitHubClientImpl.prototype, "getPrDiff").mockResolvedValue("diff --git ..."),
    listIssueComments: spyOn(GitHubClientImpl.prototype, "listIssueComments").mockResolvedValue([]),
    listPullReviewComments: spyOn(
      GitHubClientImpl.prototype,
      "listPullReviewComments",
    ).mockResolvedValue([]),
    listPullReviews: spyOn(GitHubClientImpl.prototype, "listPullReviews").mockResolvedValue([]),
    createPr: spyOn(GitHubClientImpl.prototype, "createPr").mockResolvedValue({
      url: "https://github.com/org/my-api/pull/1",
      number: 1,
    }),
    addLabels: spyOn(GitHubClientImpl.prototype, "addLabels").mockResolvedValue(undefined),
    listRepoLabels: spyOn(GitHubClientImpl.prototype, "listRepoLabels").mockResolvedValue([]),
    getPullReviewComment: spyOn(
      GitHubClientImpl.prototype,
      "getPullReviewComment",
    ).mockResolvedValue(null as never),
    createIssueComment: spyOn(GitHubClientImpl.prototype, "createIssueComment").mockResolvedValue(
      undefined,
    ),
    createReviewCommentReply: spyOn(
      GitHubClientImpl.prototype,
      "createReviewCommentReply",
    ).mockResolvedValue(undefined),
    getPrForBranch: spyOn(GitHubClientImpl.prototype, "getPrForBranch").mockResolvedValue(null),
  };
}

function restoreGhSpies(spies: GhSpies) {
  for (const spy of Object.values(spies)) {
    spy.mockRestore();
  }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function makeJiraClient() {
  return {
    getIssue: mock(async () => ({
      id: "1",
      key: "SQDP-1",
      summary: "Test issue",
      description: null,
      issueType: "Bug",
      status: "To Do",
      created: null,
      acceptanceCriteria: null,
      stacktrace: null,
      flow: null,
      assignee: null,
      parent: null,
      subtasks: [],
      children: [],
      attachments: [],
      comments: [],
      components: [],
    })),
    searchIssues: mock(async () => ({ issues: [], total: 0 })),
    getMyself: mock(async () => ({
      accountId: "bot-id",
      displayName: "hu-agent",
      emailAddress: "bot@example.com",
    })),
    findUserByEmail: mock(async () => ({
      accountId: "user-123",
      displayName: "Dev User",
    })),
    getTransitions: mock(async () => [{ id: "1", name: "In Progress" }]),
    addComment: mock(async () => ({ id: "comment-1" })),
    transitionIssue: mock(async () => {}),
    createSubtask: mock(async () => ({ id: "2", key: "SQDP-2" })),
    listProjects: mock(async () => ({ values: [{ key: "SQDP", name: "SQDP" }], total: 1 })),
    getIssueComponents: mock(async () => []),
    resolveAccountIdForBotEmail: mock(async () => null),
    getLastStatusChangeDates: mock(async () => new Map()),
  };
}

function makeRepoRegistry(repoName = "my-api") {
  const repo = {
    name: repoName,
    url: "https://github.com/org/my-api",
    defaultBranch: "main",
    description: "My API",
    keywords: ["api"],
  };
  return {
    getAll: mock(() => [repo]),
    getByName: mock((_name: string) => (_name === repoName ? repo : undefined)),
  };
}

function makeRepoSelector() {
  return {
    select: mock(() => ({ name: "my-api", url: "https://github.com/org/my-api" })),
  };
}

function makePipeline() {
  return {
    execute: mock(async () => ({ success: true, issueKey: "SQDP-1", outcome: "completed" })),
    triage: mock(async () => ({
      verdict: "PROCEED",
      title: "Fix login",
      summary: "Looks good.",
      questions: null,
      raw: "",
    })),
    evaluateCardReadiness: mock(async () => ({
      verdict: "PROCEED",
      title: null,
      summary: "Card is ready.",
      questions: null,
      raw: "",
    })),
  };
}

function makeQueue() {
  return {
    enqueue: mock(async () => {}),
    start: mock(() => {}),
    stop: mock(() => {}),
    isBusy: mock(() => false),
    getCurrentJobKind: mock(() => null),
    pendingCount: mock(() => 0),
    getCurrentJobKey: mock(() => null),
    getPendingJobKeys: mock(() => []),
  };
}

function makeCursorCloudClient() {
  return {
    listModels: mock(async () => [{ id: "model-1", name: "GPT-4" }]),
    getAgentDetails: mock(async () => ({ id: "agent-1", name: "Test Agent", status: "FINISHED" })),
    getConversation: mock(async () => ({ id: "conv-1", messages: [] })),
  };
}

interface TestDepsOverrides {
  jiraClient?: ReturnType<typeof makeJiraClient>;
  repoRegistry?: ReturnType<typeof makeRepoRegistry>;
  repoSelector?: ReturnType<typeof makeRepoSelector>;
  pipeline?: ReturnType<typeof makePipeline>;
  queue?: ReturnType<typeof makeQueue>;
  cursorCloudClient?: ReturnType<typeof makeCursorCloudClient>;
  datadogLogsClient?: { searchLogs: ReturnType<typeof mock> } | null;
  slackBotToken?: string;
}

function createTestApp(overrides: TestDepsOverrides = {}) {
  const deps = {
    jiraClient: overrides.jiraClient ?? makeJiraClient(),
    cursorCloudClient: overrides.cursorCloudClient ?? makeCursorCloudClient(),
    pipeline: overrides.pipeline ?? makePipeline(),
    repoRegistry: overrides.repoRegistry ?? makeRepoRegistry(),
    repoSelector: overrides.repoSelector ?? makeRepoSelector(),
    getGitHubToken: mock(async () => "ghs_test_token"),
    jiraDomain: "test-company",
    workdirPath: "/tmp/test-workdir",
    githubBotUser: "hu-agent[bot]",
    queue: overrides.queue ?? makeQueue(),
    logger: mockLogger(),
    datadogLogsClient:
      overrides.datadogLogsClient !== undefined ? overrides.datadogLogsClient : null,
    slackNotifier: undefined,
    slackBotToken: overrides.slackBotToken,
    slackDailyReportChannelId: undefined,
    squadScope: [],
    botEmail: "bot@example.com",
  };

  const app = new Hono();
  // Use type cast to avoid TypeScript issues with partial mock types
  app.route(
    "/hu-agent",
    createDebugRoutes(deps as unknown as Parameters<typeof createDebugRoutes>[0]),
  );
  return app;
}

async function get(app: Hono, path: string) {
  return app.request(path);
}

async function post(app: Hono, path: string, body: unknown) {
  return app.request(path, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe("debug routes", () => {
  beforeEach(() => {
    ghSpies = installGhSpies();
  });

  afterEach(() => {
    restoreGhSpies(ghSpies);
  });

  // ── Jira: /api/jira/issue ────────────────────────────────────────────────

  describe("GET /api/jira/issue", () => {
    test("happy path: returns issue data", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/issue?issueKey=SQDP-1");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { key: string };
      expect(body.key).toBe("SQDP-1");
    });

    test("missing issueKey: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/issue");
      expect(res.status).toBe(400);
    });

    test("jiraClient error: returns 502", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.getIssue.mockImplementation(async () => {
        throw new Error("Jira down");
      });
      const app = createTestApp({ jiraClient });
      const res = await get(app, "/hu-agent/api/jira/issue?issueKey=SQDP-1");
      expect(res.status).toBe(502);
    });
  });

  // ── Jira: /api/jira/search ───────────────────────────────────────────────

  describe("GET /api/jira/search", () => {
    test("happy path: returns search results", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/search");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { total: number };
      expect(body.total).toBe(0);
    });

    test("jiraClient error: returns 502", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.searchIssues.mockImplementation(async () => {
        throw new Error("Jira down");
      });
      const app = createTestApp({ jiraClient });
      const res = await get(app, "/hu-agent/api/jira/search");
      expect(res.status).toBe(502);
    });
  });

  // ── Jira: /api/jira/myself ───────────────────────────────────────────────

  describe("GET /api/jira/myself", () => {
    test("happy path: returns current user", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/myself");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { displayName: string };
      expect(body.displayName).toBe("hu-agent");
    });

    test("jiraClient error: returns 502", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.getMyself.mockImplementation(async () => {
        throw new Error("Jira down");
      });
      const app = createTestApp({ jiraClient });
      const res = await get(app, "/hu-agent/api/jira/myself");
      expect(res.status).toBe(502);
    });
  });

  // ── Jira: /api/jira/user ─────────────────────────────────────────────────

  describe("GET /api/jira/user", () => {
    test("happy path: returns user by email", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/user?email=dev@example.com");
      expect(res.status).toBe(200);
    });

    test("missing email: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/user");
      expect(res.status).toBe(400);
    });

    test("user not found: returns 404", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.findUserByEmail.mockImplementation(async () => null as never);
      const app = createTestApp({ jiraClient });
      const res = await get(app, "/hu-agent/api/jira/user?email=notfound@example.com");
      expect(res.status).toBe(404);
    });
  });

  // ── Jira: /api/jira/transitions ──────────────────────────────────────────

  describe("GET /api/jira/transitions", () => {
    test("happy path: returns transitions", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/transitions?issueKey=SQDP-1");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { transitions: unknown[] };
      expect(Array.isArray(body.transitions)).toBe(true);
    });

    test("missing issueKey: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/transitions");
      expect(res.status).toBe(400);
    });
  });

  // ── Jira: POST /api/jira/comment ─────────────────────────────────────────

  describe("POST /api/jira/comment", () => {
    test("happy path: returns success", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/comment", {
        issueKey: "SQDP-1",
        body: "Test comment",
      });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { success: boolean };
      expect(body.success).toBe(true);
    });

    test("missing issueKey: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/comment", { body: "no key" });
      expect(res.status).toBe(400);
    });
  });

  // ── Jira: POST /api/jira/transition ──────────────────────────────────────

  describe("POST /api/jira/transition", () => {
    test("happy path: returns success", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/transition", {
        issueKey: "SQDP-1",
        targetStatus: "In Progress",
      });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { success: boolean };
      expect(body.success).toBe(true);
    });

    test("missing targetStatus: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/transition", { issueKey: "SQDP-1" });
      expect(res.status).toBe(400);
    });
  });

  // ── Jira: POST /api/jira/subtask ─────────────────────────────────────────

  describe("POST /api/jira/subtask", () => {
    test("happy path: returns 201 with new subtask", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/subtask", {
        parentKey: "SQDP-1",
        summary: "Subtask summary",
        assigneeEmail: "dev@example.com",
      });
      expect(res.status).toBe(201);
      const body = (await res.json()) as { key: string };
      expect(body.key).toBe("SQDP-2");
    });

    test("missing summary: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/jira/subtask", {
        parentKey: "SQDP-1",
        assigneeEmail: "dev@example.com",
      });
      expect(res.status).toBe(400);
    });
  });

  // ── Repos: GET /api/repos ────────────────────────────────────────────────

  describe("GET /api/repos", () => {
    test("happy path: returns repos list", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/repos");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { count: number; repos: unknown[] };
      expect(body.count).toBe(1);
      expect(Array.isArray(body.repos)).toBe(true);
    });
  });

  // ── Repos: POST /api/repos/match ─────────────────────────────────────────

  describe("POST /api/repos/match", () => {
    test("happy path: returns selected repo", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/repos/match", {
        summary: "Fix login bug",
        description: "Something is broken",
      });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { selected: { name: string } | null };
      expect(body.selected?.name).toBe("my-api");
    });

    test("no match found: returns null selected", async () => {
      const repoSelector = makeRepoSelector();
      repoSelector.select.mockImplementation(() => null as never);
      const app = createTestApp({ repoSelector });
      const res = await post(app, "/hu-agent/api/repos/match", { summary: "unrelated" });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { selected: null };
      expect(body.selected).toBe(null);
    });
  });

  // ── Repos: POST /api/repos/check ─────────────────────────────────────────

  describe("POST /api/repos/check", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/repos/check", {});
      expect(res.status).toBe(400);
    });

    test("repo not found: returns 404", async () => {
      const repoRegistry = makeRepoRegistry();
      repoRegistry.getByName.mockImplementation(() => undefined);
      const app = createTestApp({ repoRegistry });
      const res = await post(app, "/hu-agent/api/repos/check", { repo: "nonexistent" });
      expect(res.status).toBe(404);
    });
  });

  // ── GitHub: GET /api/github/pulls ────────────────────────────────────────

  describe("GET /api/github/pulls", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls");
      expect(res.status).toBe(400);
    });

    test("repo not found: returns 404", async () => {
      const repoRegistry = makeRepoRegistry();
      repoRegistry.getByName.mockImplementation(() => undefined);
      const app = createTestApp({ repoRegistry });
      const res = await get(app, "/hu-agent/api/github/pulls?repo=nonexistent");
      expect(res.status).toBe(404);
    });

    test("happy path: returns list of open PRs", async () => {
      ghSpies.listOpenPrs.mockResolvedValue([
        {
          number: 1,
          url: "https://github.com/org/my-api/pull/1",
          title: "Fix",
          author: "hu-agent[bot]",
          headRef: "fix/SQDP-1",
        },
      ]);
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls?repo=my-api");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { total: number };
      expect(body.total).toBe(1);
    });

    test("GitHub error: returns 502", async () => {
      ghSpies.listOpenPrs.mockRejectedValue(new Error("GitHub down"));
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls?repo=my-api");
      expect(res.status).toBe(502);
    });
  });

  // ── GitHub: GET /api/github/pulls/diff ───────────────────────────────────

  describe("GET /api/github/pulls/diff", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/diff?pr=1");
      expect(res.status).toBe(400);
    });

    test("missing pr: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/diff?repo=my-api");
      expect(res.status).toBe(400);
    });

    test("happy path: returns diff text", async () => {
      ghSpies.getPrDiff.mockResolvedValue("diff --git a/foo.ts b/foo.ts");
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/diff?repo=my-api&pr=1");
      expect(res.status).toBe(200);
    });
  });

  // ── GitHub: GET /api/github/pulls/comments ────────────────────────────────

  describe("GET /api/github/pulls/comments", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/comments?pr=1");
      expect(res.status).toBe(400);
    });

    test("happy path: returns list of comments", async () => {
      ghSpies.listIssueComments.mockResolvedValue([
        {
          id: 1,
          user: { login: "dev" },
          body: "LGTM",
          created_at: "2026-05-01T10:00:00Z",
        } as never,
      ]);
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/comments?repo=my-api&pr=1");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { total: number };
      expect(body.total).toBe(1);
    });
  });

  // ── GitHub: GET /api/github/pulls/review-comments ─────────────────────────

  describe("GET /api/github/pulls/review-comments", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/review-comments?pr=1");
      expect(res.status).toBe(400);
    });

    test("happy path: returns review comments", async () => {
      ghSpies.listPullReviewComments.mockResolvedValue([]);
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/review-comments?repo=my-api&pr=1");
      expect(res.status).toBe(200);
    });
  });

  // ── GitHub: GET /api/github/pulls/reviews ─────────────────────────────────

  describe("GET /api/github/pulls/reviews", () => {
    test("missing repo: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/reviews?pr=1");
      expect(res.status).toBe(400);
    });

    test("happy path: returns reviews with threads", async () => {
      ghSpies.listPullReviews.mockResolvedValue([
        {
          id: 10,
          user: { login: "reviewer" },
          body: "Good PR",
          state: "APPROVED",
          submitted_at: "2026-05-01T10:00:00Z",
          pull_request_review_id: null,
        } as never,
      ]);
      ghSpies.listPullReviewComments.mockResolvedValue([]);
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/github/pulls/reviews?repo=my-api&pr=1");
      expect(res.status).toBe(200);
    });
  });

  // ── GitHub: POST /api/github/pulls ───────────────────────────────────────

  describe("POST /api/github/pulls", () => {
    test("missing repo in body: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/github/pulls", {
        branch: "fix/SQDP-1",
        title: "Fix bug",
      });
      expect(res.status).toBe(400);
    });

    test("repo not found: returns 404", async () => {
      const repoRegistry = makeRepoRegistry();
      repoRegistry.getByName.mockImplementation(() => undefined);
      const app = createTestApp({ repoRegistry });
      const res = await post(app, "/hu-agent/api/github/pulls", {
        repo: "nonexistent",
        branch: "fix/SQDP-1",
        title: "Fix bug",
      });
      expect(res.status).toBe(404);
    });

    test("happy path: returns 200 with new PR details", async () => {
      ghSpies.createPr.mockResolvedValue({
        url: "https://github.com/org/my-api/pull/2",
        number: 2,
      });
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/github/pulls", {
        repo: "my-api",
        branch: "fix/SQDP-1",
        title: "Fix login bug",
      });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { pr: { number: number } };
      expect(body.pr.number).toBe(2);
    });
  });

  // ── Cursor: GET /api/cursor/models ────────────────────────────────────────

  describe("GET /api/cursor/models", () => {
    test("happy path: returns models list", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/cursor/models");
      expect(res.status).toBe(200);
    });

    test("cursor error: returns 502", async () => {
      const cursorCloudClient = makeCursorCloudClient();
      cursorCloudClient.listModels.mockImplementation(async () => {
        throw new Error("Cursor down");
      });
      const app = createTestApp({ cursorCloudClient });
      const res = await get(app, "/hu-agent/api/cursor/models");
      expect(res.status).toBe(502);
    });
  });

  // ── Cursor: GET /api/cursor/agents ────────────────────────────────────────

  describe("GET /api/cursor/agents", () => {
    test("missing id: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/cursor/agents");
      expect(res.status).toBe(400);
    });

    test("happy path: returns agent details", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/cursor/agents?id=agent-1");
      expect(res.status).toBe(200);
    });

    test("cursor error: returns 502", async () => {
      const cursorCloudClient = makeCursorCloudClient();
      cursorCloudClient.getAgentDetails.mockImplementation(async () => {
        throw new Error("Agent not found");
      });
      const app = createTestApp({ cursorCloudClient });
      const res = await get(app, "/hu-agent/api/cursor/agents?id=missing");
      expect(res.status).toBe(502);
    });
  });

  // ── Cursor: GET /api/cursor/agents/conversation ───────────────────────────

  describe("GET /api/cursor/agents/conversation", () => {
    test("missing id: returns 400", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/cursor/agents/conversation");
      expect(res.status).toBe(400);
    });

    test("happy path: returns conversation", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/cursor/agents/conversation?id=agent-1");
      expect(res.status).toBe(200);
    });
  });

  // ── Datadog: GET /api/datadog/logs ────────────────────────────────────────

  describe("GET /api/datadog/logs", () => {
    test("no Datadog configured: returns 503", async () => {
      const app = createTestApp({ datadogLogsClient: null });
      const res = await get(app, "/hu-agent/api/datadog/logs");
      expect(res.status).toBe(503);
    });

    test("bad window param: returns 400", async () => {
      const datadogLogsClient = {
        searchLogs: mock(async () => ({ logs: [], total: 0, truncated: false })),
      };
      const app = createTestApp({ datadogLogsClient });
      // Both from and to but only one set
      const res = await get(app, "/hu-agent/api/datadog/logs?from=2026-01-01T00:00:00Z");
      expect(res.status).toBe(400);
    });

    test("happy path (15m window): returns logs", async () => {
      const datadogLogsClient = {
        searchLogs: mock(async () => ({
          logs: [{ timestamp: "2026-05-01T10:00:00Z", message: "test log" }],
          total: 1,
          truncated: false,
        })),
      };
      const app = createTestApp({ datadogLogsClient });
      const res = await get(app, "/hu-agent/api/datadog/logs?window=15m");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { total: number };
      expect(body.total).toBe(1);
    });

    test("to must be after from: returns 400 for invalid range", async () => {
      const datadogLogsClient = {
        searchLogs: mock(async () => ({ logs: [], total: 0 })),
      };
      const app = createTestApp({ datadogLogsClient });
      const res = await get(
        app,
        "/hu-agent/api/datadog/logs?from=2026-05-02T00:00:00Z&to=2026-05-01T00:00:00Z",
      );
      expect(res.status).toBe(400);
    });

    test("range exceeds 24h: returns 400", async () => {
      const datadogLogsClient = {
        searchLogs: mock(async () => ({ logs: [], total: 0 })),
      };
      const app = createTestApp({ datadogLogsClient });
      const from = "2026-05-01T00:00:00Z";
      const to = "2026-05-03T00:00:00Z"; // 48h
      const res = await get(app, `/hu-agent/api/datadog/logs?from=${from}&to=${to}`);
      expect(res.status).toBe(400);
    });

    test("datadog API error: returns 502", async () => {
      const { DatadogApiError } = await import("@/datadog/client.js");
      const datadogLogsClient = {
        searchLogs: mock(async () => {
          throw new DatadogApiError("API error", 500, "Internal Server Error");
        }),
      };
      const app = createTestApp({ datadogLogsClient });
      const res = await get(app, "/hu-agent/api/datadog/logs?window=15m");
      expect(res.status).toBe(502);
    });
  });

  // ── Pipeline: POST /api/pipeline/triage ──────────────────────────────────

  describe("POST /api/pipeline/triage", () => {
    test("missing issueKey: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/pipeline/triage", {});
      expect(res.status).toBe(400);
    });

    test("happy path: returns triage result", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/pipeline/triage", { issueKey: "SQDP-1" });
      expect(res.status).toBe(200);
      const body = (await res.json()) as { verdict: string };
      expect(body.verdict).toBe("PROCEED");
    });

    test("pipeline triage error: returns 500", async () => {
      const pipeline = makePipeline();
      pipeline.triage.mockImplementation(async () => {
        throw new Error("Triage failed");
      });
      const app = createTestApp({ pipeline });
      const res = await post(app, "/hu-agent/api/pipeline/triage", { issueKey: "SQDP-1" });
      expect(res.status).toBe(500);
    });
  });

  // ── Pipeline: POST /api/pipeline/fix ─────────────────────────────────────

  describe("POST /api/pipeline/fix", () => {
    test("missing issueKey: returns 400", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/pipeline/fix", {});
      expect(res.status).toBe(400);
    });

    test("happy path: enqueues job and returns 202", async () => {
      const app = createTestApp();
      const res = await post(app, "/hu-agent/api/pipeline/fix", { issueKey: "SQDP-1" });
      expect(res.status).toBe(202);
      const body = (await res.json()) as { accepted: boolean };
      expect(body.accepted).toBe(true);
    });

    test("jiraClient error: returns 500", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.getIssue.mockImplementation(async () => {
        throw new Error("Jira down");
      });
      const app = createTestApp({ jiraClient });
      const res = await post(app, "/hu-agent/api/pipeline/fix", { issueKey: "SQDP-1" });
      expect(res.status).toBe(500);
    });
  });

  // ── Slack: GET /api/slack/usergroups ──────────────────────────────────────

  describe("GET /api/slack/usergroups", () => {
    test("no Slack token configured: returns 503", async () => {
      const app = createTestApp({ slackBotToken: undefined });
      const res = await get(app, "/hu-agent/api/slack/usergroups");
      expect(res.status).toBe(503);
    });

    test("Slack returns ok=true: returns usergroups list", async () => {
      let slackFetchSpy: ReturnType<typeof spyOn>;
      slackFetchSpy = spyOn(globalThis, "fetch").mockResolvedValue(
        new Response(
          JSON.stringify({
            ok: true,
            usergroups: [{ id: "S01", handle: "dev-team", name: "Dev Team" }],
          }),
          { status: 200, headers: { "Content-Type": "application/json" } },
        ),
      );

      const app = createTestApp({ slackBotToken: "xoxb-test" });
      const res = await get(app, "/hu-agent/api/slack/usergroups");
      expect(res.status).toBe(200);
      const body = (await res.json()) as { usergroups: unknown[] };
      expect(body.usergroups.length).toBe(1);

      slackFetchSpy.mockRestore();
    });

    test("Slack returns ok=false with missing_scope: returns 502 with hint", async () => {
      let slackFetchSpy: ReturnType<typeof spyOn>;
      slackFetchSpy = spyOn(globalThis, "fetch").mockResolvedValue(
        new Response(
          JSON.stringify({ ok: false, error: "missing_scope", needed: "usergroups:read" }),
          { status: 200, headers: { "Content-Type": "application/json" } },
        ),
      );

      const app = createTestApp({ slackBotToken: "xoxb-test" });
      const res = await get(app, "/hu-agent/api/slack/usergroups");
      expect(res.status).toBe(502);
      const body = (await res.json()) as { hint: string };
      expect(body.hint).toContain("usergroups:read");

      slackFetchSpy.mockRestore();
    });
  });

  // ── Jira: GET /api/jira/projects ─────────────────────────────────────────

  describe("GET /api/jira/projects", () => {
    test("happy path: returns projects list", async () => {
      const app = createTestApp();
      const res = await get(app, "/hu-agent/api/jira/projects");
      expect(res.status).toBe(200);
    });

    test("jiraClient error: returns 502", async () => {
      const jiraClient = makeJiraClient();
      jiraClient.listProjects.mockImplementation(async () => {
        throw new Error("Jira down");
      });
      const app = createTestApp({ jiraClient });
      const res = await get(app, "/hu-agent/api/jira/projects");
      expect(res.status).toBe(502);
    });
  });
});
