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

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

const mockGetUsageLimits = vi.hoisted(() =>
  vi.fn().mockResolvedValue({
    five_hour: null,
    seven_day: null,
    extra_usage: null,
  }),
);

const mockUpdateChecker = vi.hoisted(() => ({
  getUpdateState: vi.fn().mockReturnValue({
    currentVersion: "1.0.0",
    latestVersion: "1.0.0",
    lastChecked: Date.now(),
    isServiceMode: false,
    updateInProgress: false,
  }),
  checkForUpdate: vi.fn().mockResolvedValue(undefined),
  isUpdateAvailable: vi.fn().mockReturnValue(false),
  setUpdateInProgress: vi.fn(),
}));

const mockRefreshServiceDefinition = vi.hoisted(() => vi.fn());

vi.mock("../usage-limits.js", () => ({
  getUsageLimits: mockGetUsageLimits,
}));

vi.mock("../update-checker.js", () => mockUpdateChecker);

vi.mock("../service.js", () => ({
  refreshServiceDefinition: mockRefreshServiceDefinition,
}));

vi.mock("../logger.js", () => ({
  createLogger: () => ({
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  }),
}));

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

import { registerSystemRoutes } from "./system-routes.js";

const mockLauncher = {
  getSession: vi.fn().mockReturnValue(null),
  isAlive: vi.fn().mockReturnValue(false),
};

const mockWsBridge = {
  getSession: vi.fn().mockReturnValue(null),
  getCodexRateLimits: vi.fn().mockReturnValue(null),
  injectUserMessage: vi.fn(),
};

const mockTerminalManager = {
  getInfo: vi.fn().mockReturnValue(null),
  spawn: vi.fn().mockReturnValue("term-1"),
  kill: vi.fn(),
};

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockGetUsageLimits.mockResolvedValue({
    five_hour: null,
    seven_day: null,
    extra_usage: null,
  });
  mockUpdateChecker.getUpdateState.mockReturnValue({
    currentVersion: "1.0.0",
    latestVersion: "1.0.0",
    lastChecked: Date.now(),
    isServiceMode: false,
    updateInProgress: false,
  });
  mockUpdateChecker.checkForUpdate.mockResolvedValue(undefined);
  mockUpdateChecker.isUpdateAvailable.mockReturnValue(false);
  mockLauncher.getSession.mockReturnValue(null);
  mockLauncher.isAlive.mockReturnValue(false);
  mockWsBridge.getSession.mockReturnValue(null);
  mockWsBridge.getCodexRateLimits.mockReturnValue(null);
  mockWsBridge.injectUserMessage.mockReset();
  mockTerminalManager.getInfo.mockReturnValue(null);
  mockTerminalManager.spawn.mockReturnValue("term-1");
  mockTerminalManager.kill.mockReset();
  app = new Hono();
  registerSystemRoutes(app, {
    launcher: mockLauncher as unknown as import("../cli-launcher.js").CliLauncher,
    wsBridge: mockWsBridge as unknown as import("../ws-bridge.js").WsBridge,
    terminalManager: mockTerminalManager as unknown as import("../terminal-manager.js").TerminalManager,
    updateCheckStaleMs: 60_000,
  });
});

/* ── 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 /usage-limits", () => {
  it("returns usage limits", async () => {
    const res = await req("GET", "/usage-limits");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveProperty("five_hour");
    expect(body).toHaveProperty("seven_day");
  });
});

describe("GET /sessions/:id/usage-limits", () => {
  it("returns usage limits for non-codex session", async () => {
    const res = await req("GET", "/sessions/sess-1/usage-limits");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toHaveProperty("five_hour");
  });

  it("returns codex rate limits for codex session", async () => {
    mockWsBridge.getSession.mockReturnValue({ backendType: "codex" });
    mockWsBridge.getCodexRateLimits.mockReturnValue({
      primary: { usedPercent: 50, windowDurationMins: 300, resetsAt: 1735689600 },
      secondary: null,
    });
    const res = await req("GET", "/sessions/sess-1/usage-limits");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.five_hour).not.toBeNull();
    expect(body.five_hour.utilization).toBe(50);
  });

  it("returns empty limits for codex session with no rate limit data", async () => {
    mockWsBridge.getSession.mockReturnValue({ backendType: "codex" });
    const res = await req("GET", "/sessions/sess-1/usage-limits");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.five_hour).toBeNull();
  });
});

describe("GET /update-check", () => {
  it("returns update state", async () => {
    const res = await req("GET", "/update-check");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.currentVersion).toBe("1.0.0");
    expect(body.updateAvailable).toBe(false);
  });

  it("triggers refresh when stale", async () => {
    mockUpdateChecker.getUpdateState.mockReturnValue({
      currentVersion: "1.0.0",
      latestVersion: "1.0.0",
      lastChecked: 0,
      isServiceMode: false,
      updateInProgress: false,
    });
    await req("GET", "/update-check");
    expect(mockUpdateChecker.checkForUpdate).toHaveBeenCalled();
  });
});

describe("POST /update-check", () => {
  it("forces update check", async () => {
    const res = await req("POST", "/update-check");
    expect(res.status).toBe(200);
    expect(mockUpdateChecker.checkForUpdate).toHaveBeenCalled();
  });
});

describe("POST /update", () => {
  it("returns 400 when not in service mode", async () => {
    const res = await req("POST", "/update");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("service mode");
  });

  it("returns 400 when no update available", async () => {
    mockUpdateChecker.getUpdateState.mockReturnValue({
      currentVersion: "1.0.0",
      latestVersion: "1.0.0",
      lastChecked: Date.now(),
      isServiceMode: true,
      updateInProgress: false,
    });
    const res = await req("POST", "/update");
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("No update available");
  });

  it("returns 409 when update already in progress", async () => {
    mockUpdateChecker.getUpdateState.mockReturnValue({
      currentVersion: "1.0.0",
      latestVersion: "2.0.0",
      lastChecked: Date.now(),
      isServiceMode: true,
      updateInProgress: true,
    });
    mockUpdateChecker.isUpdateAvailable.mockReturnValue(true);
    const res = await req("POST", "/update");
    expect(res.status).toBe(409);
  });
});

describe("GET /terminal", () => {
  it("returns inactive when no terminal", async () => {
    const res = await req("GET", "/terminal");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.active).toBe(false);
  });

  it("returns terminal info when active", async () => {
    mockTerminalManager.getInfo.mockReturnValue({ id: "term-1", cwd: "/tmp" });
    const res = await req("GET", "/terminal");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.active).toBe(true);
    expect(body.terminalId).toBe("term-1");
  });
});

describe("POST /terminal/spawn", () => {
  it("spawns a terminal", async () => {
    const res = await req("POST", "/terminal/spawn", { cwd: "/tmp" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.terminalId).toBe("term-1");
  });

  it("returns 400 on missing cwd", async () => {
    const res = await req("POST", "/terminal/spawn", {});
    expect(res.status).toBe(400);
  });

  it("returns 500 on spawn failure", async () => {
    mockTerminalManager.spawn.mockImplementation(() => {
      throw new Error("Spawn failed");
    });
    const res = await req("POST", "/terminal/spawn", { cwd: "/tmp" });
    expect(res.status).toBe(500);
  });
});

describe("POST /terminal/kill", () => {
  it("kills a terminal", async () => {
    const res = await req("POST", "/terminal/kill", { terminalId: "term-1" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });

  it("returns 400 on missing terminalId", async () => {
    const res = await req("POST", "/terminal/kill", {});
    expect(res.status).toBe(400);
  });
});

describe("POST /sessions/:id/message", () => {
  it("returns 404 for non-existent session", async () => {
    const res = await req("POST", "/sessions/missing/message", {
      content: "hello",
    });
    expect(res.status).toBe(404);
  });

  it("returns 400 when session is not running", async () => {
    mockLauncher.getSession.mockReturnValue({ id: "sess-1" });
    mockLauncher.isAlive.mockReturnValue(false);
    const res = await req("POST", "/sessions/sess-1/message", {
      content: "hello",
    });
    expect(res.status).toBe(400);
  });

  it("injects message into running session", async () => {
    mockLauncher.getSession.mockReturnValue({ id: "sess-1" });
    mockLauncher.isAlive.mockReturnValue(true);
    const res = await req("POST", "/sessions/sess-1/message", {
      content: "hello",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockWsBridge.injectUserMessage).toHaveBeenCalledWith("sess-1", "hello");
  });
});
