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

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

const mockSettingsManager = vi.hoisted(() => ({
  getSettings: vi.fn().mockReturnValue({
    linearApiKey: "lin-key-123",
    linearAutoTransition: false,
    linearAutoTransitionStateId: "",
    linearAutoTransitionStateName: "",
  }),
}));

const mockLinearCache = vi.hoisted(() => ({
  linearCache: {
    getOrFetch: vi.fn().mockImplementation(async (_key: string, _ttl: number, fn: () => Promise<unknown>) => fn()),
    clear: vi.fn(),
    invalidate: vi.fn(),
  },
}));

const mockSessionLinearIssues = vi.hoisted(() => ({
  getLinearIssue: vi.fn().mockReturnValue(undefined),
  setLinearIssue: vi.fn(),
  removeLinearIssue: vi.fn(),
}));

const mockLinearProjectManager = vi.hoisted(() => ({
  listMappings: vi.fn().mockReturnValue([]),
  getMapping: vi.fn().mockReturnValue(null),
  upsertMapping: vi.fn().mockReturnValue({ repoRoot: "/repo", projectId: "p1", projectName: "P1" }),
  removeMapping: vi.fn().mockReturnValue(false),
}));

vi.mock("../settings-manager.js", () => mockSettingsManager);
vi.mock("../linear-cache.js", () => mockLinearCache);
vi.mock("../session-linear-issues.js", () => mockSessionLinearIssues);
vi.mock("../linear-project-manager.js", () => mockLinearProjectManager);

/* ── Mock global fetch ────────────────────────────────────────────── */

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

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

import { registerLinearRoutes } from "./linear-routes.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockSettingsManager.getSettings.mockReturnValue({
    linearApiKey: "lin-key-123",
    linearAutoTransition: false,
    linearAutoTransitionStateId: "",
    linearAutoTransitionStateName: "",
  });
  mockLinearCache.linearCache.getOrFetch.mockImplementation(
    async (_key: string, _ttl: number, fn: () => Promise<unknown>) => fn(),
  );
  mockLinearCache.linearCache.clear.mockReset();
  mockLinearCache.linearCache.invalidate.mockReset();
  mockSessionLinearIssues.getLinearIssue.mockReturnValue(undefined);
  mockSessionLinearIssues.setLinearIssue.mockReset();
  mockSessionLinearIssues.removeLinearIssue.mockReset();
  mockLinearProjectManager.listMappings.mockReturnValue([]);
  mockLinearProjectManager.getMapping.mockReturnValue(null);
  mockLinearProjectManager.upsertMapping.mockReturnValue({
    repoRoot: "/repo",
    projectId: "p1",
    projectName: "P1",
  });
  mockLinearProjectManager.removeMapping.mockReturnValue(false);
  mockFetch.mockReset();
  app = new Hono();
  registerLinearRoutes(app);
});

/* ── 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);
}

function mockLinearResponse(data: unknown, ok = true) {
  mockFetch.mockResolvedValue({
    ok,
    statusText: ok ? "OK" : "Bad Request",
    json: async () => ({ data }),
  });
}

function mockLinearErrorResponse(errors: Array<{ message: string }>) {
  mockFetch.mockResolvedValue({
    ok: true,
    statusText: "OK",
    json: async () => ({ errors }),
  });
}

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

describe("GET /linear/issues", () => {
  it("returns empty when no query", async () => {
    const res = await req("GET", "/linear/issues");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.issues).toEqual([]);
  });

  it("returns 400 when API key is not configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "",
      linearAutoTransition: false,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("GET", "/linear/issues?query=test");
    expect(res.status).toBe(400);
  });

  it("returns issues from search", async () => {
    mockLinearResponse({
      searchIssues: {
        nodes: [
          {
            id: "issue-1",
            identifier: "ENG-1",
            title: "Test Issue",
            url: "https://linear.app/issue/1",
            state: { name: "In Progress", type: "started" },
            team: { id: "t1", key: "ENG", name: "Engineering" },
          },
        ],
      },
    });
    const res = await req("GET", "/linear/issues?query=test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.issues).toHaveLength(1);
    expect(body.issues[0].identifier).toBe("ENG-1");
  });

  it("returns 502 on Linear API error", async () => {
    mockLinearErrorResponse([{ message: "Auth failed" }]);
    const res = await req("GET", "/linear/issues?query=test");
    expect(res.status).toBe(502);
  });
});

describe("GET /linear/connection", () => {
  it("returns 400 when API key is not configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "",
      linearAutoTransition: false,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("GET", "/linear/connection");
    expect(res.status).toBe(400);
  });

  it("returns connection info", async () => {
    mockLinearResponse({
      viewer: { id: "v1", name: "Test User", email: "test@ex.com" },
      teams: { nodes: [{ id: "t1", key: "ENG", name: "Engineering" }] },
    });
    const res = await req("GET", "/linear/connection");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.connected).toBe(true);
    expect(body.viewerName).toBe("Test User");
  });
});

describe("PUT /sessions/:id/linear-issue", () => {
  it("associates issue with session", async () => {
    const res = await req("PUT", "/sessions/sess-1/linear-issue", {
      id: "issue-1",
      identifier: "ENG-1",
      title: "Test",
      url: "https://linear.app/1",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockSessionLinearIssues.setLinearIssue).toHaveBeenCalled();
  });

  it("returns 400 on missing required fields", async () => {
    const res = await req("PUT", "/sessions/sess-1/linear-issue", {
      id: "issue-1",
    });
    expect(res.status).toBe(400);
  });
});

describe("GET /sessions/:id/linear-issue", () => {
  it("returns null when no issue associated", async () => {
    const res = await req("GET", "/sessions/sess-1/linear-issue");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.issue).toBeNull();
  });

  it("returns stored issue without refresh", async () => {
    const stored = {
      id: "issue-1",
      identifier: "ENG-1",
      title: "Test",
      url: "https://linear.app/1",
    };
    mockSessionLinearIssues.getLinearIssue.mockReturnValue(stored);
    const res = await req("GET", "/sessions/sess-1/linear-issue");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.issue.identifier).toBe("ENG-1");
  });
});

describe("DELETE /sessions/:id/linear-issue", () => {
  it("removes issue association", async () => {
    const res = await req("DELETE", "/sessions/sess-1/linear-issue");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockSessionLinearIssues.removeLinearIssue).toHaveBeenCalledWith("sess-1");
  });
});

describe("POST /linear/issues/:issueId/comments", () => {
  it("returns 400 when API key is not configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "",
      linearAutoTransition: false,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("POST", "/linear/issues/issue-1/comments", {
      body: "Hello",
    });
    expect(res.status).toBe(400);
  });

  it("creates a comment", async () => {
    mockFetch.mockResolvedValue({
      ok: true,
      statusText: "OK",
      json: async () => ({
        data: {
          commentCreate: {
            success: true,
            comment: {
              id: "c1",
              body: "Hello",
              createdAt: "2025-01-01T00:00:00Z",
              user: { name: "User", displayName: "User D" },
            },
          },
        },
      }),
    });
    const res = await req("POST", "/linear/issues/issue-1/comments", {
      body: "Hello",
    });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.ok).toBe(true);
    expect(data.comment.body).toBe("Hello");
  });

  it("returns 502 on API error", async () => {
    mockFetch.mockResolvedValue({
      ok: false,
      statusText: "Unauthorized",
      json: async () => ({}),
    });
    const res = await req("POST", "/linear/issues/issue-1/comments", {
      body: "Hello",
    });
    expect(res.status).toBe(502);
  });
});

describe("GET /linear/states", () => {
  it("returns 400 when API key is not configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "",
      linearAutoTransition: false,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("GET", "/linear/states");
    expect(res.status).toBe(400);
  });

  it("returns workflow states", async () => {
    mockLinearResponse({
      teams: {
        nodes: [
          {
            id: "t1",
            key: "ENG",
            name: "Engineering",
            states: {
              nodes: [
                { id: "s1", name: "Todo", type: "unstarted" },
                { id: "s2", name: "In Progress", type: "started" },
              ],
            },
          },
        ],
      },
    });
    const res = await req("GET", "/linear/states");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.teams).toHaveLength(1);
    expect(body.teams[0].states).toHaveLength(2);
  });
});

describe("GET /linear/projects", () => {
  it("returns projects", async () => {
    mockLinearResponse({
      projects: {
        nodes: [{ id: "p1", name: "Project 1", state: "started" }],
      },
    });
    const res = await req("GET", "/linear/projects");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.projects).toHaveLength(1);
  });
});

describe("GET /linear/project-issues", () => {
  it("returns 400 when projectId is missing", async () => {
    const res = await req("GET", "/linear/project-issues");
    expect(res.status).toBe(400);
  });

  it("returns project issues", async () => {
    mockLinearResponse({
      issues: {
        nodes: [
          {
            id: "i1",
            identifier: "ENG-1",
            title: "Issue 1",
            url: "https://linear.app/1",
            state: { name: "Todo", type: "unstarted" },
            team: { key: "ENG", name: "Eng" },
          },
        ],
      },
    });
    const res = await req("GET", "/linear/project-issues?projectId=p1");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.issues).toHaveLength(1);
  });
});

describe("GET /linear/project-mappings", () => {
  it("returns all mappings", async () => {
    const res = await req("GET", "/linear/project-mappings");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.mappings).toEqual([]);
  });

  it("returns mapping for specific repoRoot", async () => {
    const mapping = { repoRoot: "/repo", projectId: "p1", projectName: "P1" };
    mockLinearProjectManager.getMapping.mockReturnValue(mapping);
    const res = await req("GET", "/linear/project-mappings?repoRoot=/repo");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.mapping).toEqual(mapping);
  });
});

describe("PUT /linear/project-mappings", () => {
  it("upserts a mapping", async () => {
    const res = await req("PUT", "/linear/project-mappings", {
      repoRoot: "/repo",
      projectId: "p1",
      projectName: "Project 1",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.mapping).toBeDefined();
  });

  it("returns 400 on validation error", async () => {
    const res = await req("PUT", "/linear/project-mappings", {});
    expect(res.status).toBe(400);
  });
});

describe("DELETE /linear/project-mappings", () => {
  it("returns 404 when mapping not found", async () => {
    const res = await req("DELETE", "/linear/project-mappings", {
      repoRoot: "/missing",
    });
    expect(res.status).toBe(404);
  });

  it("removes a mapping", async () => {
    mockLinearProjectManager.removeMapping.mockReturnValue(true);
    const res = await req("DELETE", "/linear/project-mappings", {
      repoRoot: "/repo",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });
});

describe("POST /linear/issues/:id/transition", () => {
  it("returns 400 when API key is not configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "",
      linearAutoTransition: false,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("POST", "/linear/issues/issue-1/transition");
    expect(res.status).toBe(400);
  });

  it("skips when auto transition is disabled", async () => {
    const res = await req("POST", "/linear/issues/issue-1/transition");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.skipped).toBe(true);
    expect(body.reason).toBe("auto_transition_disabled");
  });

  it("skips when no target state configured", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "key",
      linearAutoTransition: true,
      linearAutoTransitionStateId: "",
      linearAutoTransitionStateName: "",
    });
    const res = await req("POST", "/linear/issues/issue-1/transition");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.skipped).toBe(true);
    expect(body.reason).toBe("no_target_state_configured");
  });

  it("transitions issue successfully", async () => {
    mockSettingsManager.getSettings.mockReturnValue({
      linearApiKey: "key",
      linearAutoTransition: true,
      linearAutoTransitionStateId: "state-1",
      linearAutoTransitionStateName: "In Progress",
    });
    mockFetch.mockResolvedValue({
      ok: true,
      statusText: "OK",
      json: async () => ({
        data: {
          issueUpdate: {
            success: true,
            issue: {
              id: "issue-1",
              identifier: "ENG-1",
              state: { name: "In Progress", type: "started" },
            },
          },
        },
      }),
    });
    const res = await req("POST", "/linear/issues/issue-1/transition");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(body.skipped).toBe(false);
  });
});
