/**
 * Integration tests for Zod request validation across all route files.
 * Verifies that invalid input returns 400 with structured error details.
 */
import { Hono } from "hono";
import { describe, it, expect, vi, beforeEach } from "vitest";

/* ── Mock agent-store ─────────────────────────────────────────── */
const mockAgentStore = vi.hoisted(() => ({
  listAgents: vi.fn().mockReturnValue([]),
  getAgent: vi.fn().mockReturnValue(null),
  createAgent: vi.fn(),
  updateAgent: vi.fn().mockReturnValue(null),
  deleteAgent: vi.fn().mockReturnValue(false),
  regenerateWebhookSecret: vi.fn().mockReturnValue(null),
}));
vi.mock("./agent-store.js", () => mockAgentStore);

/* ── Mock cron-store ──────────────────────────────────────────── */
const mockCronStore = vi.hoisted(() => ({
  listJobs: vi.fn().mockReturnValue([]),
  getJob: vi.fn().mockReturnValue(null),
  createJob: vi.fn(),
  updateJob: vi.fn().mockReturnValue(null),
  deleteJob: vi.fn().mockReturnValue(false),
}));
vi.mock("./cron-store.js", () => mockCronStore);

/* ── Mock prompt-manager ──────────────────────────────────────── */
const mockPromptManager = vi.hoisted(() => ({
  listPrompts: vi.fn().mockReturnValue([]),
  getPrompt: vi.fn().mockReturnValue(null),
  createPrompt: vi.fn(),
  updatePrompt: vi.fn().mockReturnValue(null),
  deletePrompt: vi.fn().mockReturnValue(false),
}));
vi.mock("./prompt-manager.js", () => mockPromptManager);

/* ── Mock settings-manager ────────────────────────────────────── */
const mockSettings = vi.hoisted(() => ({
  openrouterApiKey: "",
  openrouterModel: "",
  linearApiKey: "",
  linearAutoTransition: false,
  linearAutoTransitionStateId: "",
  linearAutoTransitionStateName: "",
  editorTabEnabled: true,
}));
vi.mock("./settings-manager.js", () => ({
  DEFAULT_OPENROUTER_MODEL: "openai/gpt-4o",
  getSettings: vi.fn().mockReturnValue(mockSettings),
  updateSettings: vi.fn().mockReturnValue(mockSettings),
}));

vi.mock("./linear-cache.js", () => ({
  linearCache: { clear: vi.fn(), getOrFetch: vi.fn(), invalidate: vi.fn() },
}));

vi.mock("./session-linear-issues.js", () => ({
  getLinearIssue: vi.fn().mockReturnValue(null),
  setLinearIssue: vi.fn(),
  removeLinearIssue: vi.fn(),
}));

vi.mock("./linear-project-manager.js", () => ({
  upsertMapping: vi.fn(),
  removeMapping: vi.fn().mockReturnValue(false),
  getMapping: vi.fn().mockReturnValue(null),
  listMappings: vi.fn().mockReturnValue([]),
}));

/* ── Imports ──────────────────────────────────────────────────── */
import { registerAgentRoutes } from "./routes/agent-routes.js";
import { registerCronRoutes } from "./routes/cron-routes.js";
import { registerPromptRoutes } from "./routes/prompt-routes.js";
import { registerSettingsRoutes } from "./routes/settings-routes.js";
import { registerLinearRoutes } from "./routes/linear-routes.js";

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

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

function expectValidationError(json: { error: string; details: Array<{ path: string; message: string }> }) {
  expect(json.error).toBe("Validation failed");
  expect(json.details).toBeInstanceOf(Array);
  expect(json.details.length).toBeGreaterThan(0);
}

/* ── Agent routes validation ──────────────────────────────────── */

describe("Agent route validation", () => {
  let app: Hono;

  beforeEach(() => {
    vi.restoreAllMocks();
    mockAgentStore.createAgent.mockReset();
    mockAgentStore.updateAgent.mockReturnValue(null);
    app = new Hono();
    registerAgentRoutes(app);
  });

  it("POST /agents rejects invalid backendType", async () => {
    const res = await req(app, "POST", "/agents", { name: "test", backendType: "invalid" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /agents rejects version as string", async () => {
    const res = await req(app, "POST", "/agents", { name: "test", version: "abc" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /agents rejects name too long", async () => {
    const res = await req(app, "POST", "/agents", { name: "x".repeat(201) });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /agents/:id rejects invalid backendType", async () => {
    const res = await req(app, "PUT", "/agents/test-id", { backendType: 123 });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /agents accepts valid minimal body", async () => {
    mockAgentStore.createAgent.mockReturnValue({
      id: "new", name: "Test", prompt: "", version: 1,
      backendType: "claude", model: "", cwd: "", enabled: true,
      permissionMode: "bypassPermissions", totalRuns: 0,
      consecutiveFailures: 0, createdAt: Date.now(), updatedAt: Date.now(),
    });
    const res = await req(app, "POST", "/agents", { name: "Test", prompt: "hello" });
    expect(res.status).toBe(201);
  });

  it("POST /agents rejects broken JSON", async () => {
    const res = await app.request("/agents", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: "not-json",
    });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });
});

/* ── Cron routes validation ───────────────────────────────────── */

describe("Cron route validation", () => {
  let app: Hono;

  beforeEach(() => {
    vi.restoreAllMocks();
    mockCronStore.createJob.mockReset();
    mockCronStore.updateJob.mockReturnValue(null);
    app = new Hono();
    registerCronRoutes(app);
  });

  it("POST /cron/jobs rejects invalid backendType", async () => {
    const res = await req(app, "POST", "/cron/jobs", { backendType: "invalid" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /cron/jobs/:id rejects non-boolean recurring", async () => {
    const res = await req(app, "PUT", "/cron/jobs/test", { recurring: "yes" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /cron/jobs accepts valid body", async () => {
    mockCronStore.createJob.mockReturnValue({
      id: "j1", name: "Test", prompt: "do stuff", schedule: "0 9 * * *",
      recurring: true, backendType: "claude", model: "", cwd: "/tmp",
      enabled: true, permissionMode: "bypassPermissions",
      createdAt: Date.now(), updatedAt: Date.now(),
    });
    const res = await req(app, "POST", "/cron/jobs", {
      name: "Test", prompt: "do stuff", schedule: "0 9 * * *", cwd: "/tmp",
    });
    expect(res.status).toBe(201);
  });
});

/* ── Prompt routes validation ─────────────────────────────────── */

describe("Prompt route validation", () => {
  let app: Hono;

  beforeEach(() => {
    vi.restoreAllMocks();
    mockPromptManager.createPrompt.mockReset();
    app = new Hono();
    registerPromptRoutes(app);
  });

  it("POST /prompts rejects title too long", async () => {
    const res = await req(app, "POST", "/prompts", { title: "x".repeat(501) });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /prompts accepts valid body", async () => {
    mockPromptManager.createPrompt.mockReturnValue({
      id: "p1", name: "Test", content: "hello", scope: "global",
    });
    const res = await req(app, "POST", "/prompts", { title: "Test", content: "hello" });
    expect(res.status).toBe(201);
  });

  it("POST /prompts rejects invalid scope", async () => {
    const res = await req(app, "POST", "/prompts", {
      title: "Test", content: "hello", scope: "invalid",
    });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });
});

/* ── Settings routes validation ───────────────────────────────── */

describe("Settings route validation", () => {
  let app: Hono;

  beforeEach(() => {
    vi.restoreAllMocks();
    app = new Hono();
    registerSettingsRoutes(app);
  });

  it("PUT /settings rejects empty body", async () => {
    const res = await req(app, "PUT", "/settings", {});
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /settings rejects non-boolean linearAutoTransition", async () => {
    const res = await req(app, "PUT", "/settings", { linearAutoTransition: "yes" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /settings rejects non-string openrouterApiKey", async () => {
    const res = await req(app, "PUT", "/settings", { openrouterApiKey: 123 });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /settings accepts valid single field", async () => {
    const res = await req(app, "PUT", "/settings", { editorTabEnabled: false });
    expect(res.status).toBe(200);
  });
});

/* ── Linear routes validation ─────────────────────────────────── */

describe("Linear route validation", () => {
  let app: Hono;

  beforeEach(() => {
    vi.restoreAllMocks();
    app = new Hono();
    registerLinearRoutes(app);
  });

  it("PUT /sessions/:id/linear-issue rejects missing required fields", async () => {
    const res = await req(app, "PUT", "/sessions/s1/linear-issue", { id: "abc" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /sessions/:id/linear-issue rejects empty id", async () => {
    const res = await req(app, "PUT", "/sessions/s1/linear-issue", {
      id: "", identifier: "PROJ-1", title: "Test", url: "https://linear.app/...",
    });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("PUT /sessions/:id/linear-issue accepts valid body", async () => {
    const res = await req(app, "PUT", "/sessions/s1/linear-issue", {
      id: "abc", identifier: "PROJ-1", title: "Test", url: "https://linear.app/x",
    });
    expect(res.status).toBe(200);
  });

  it("PUT /linear/project-mappings rejects missing projectId", async () => {
    const res = await req(app, "PUT", "/linear/project-mappings", {
      repoRoot: "/tmp", projectName: "proj",
    });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /linear/issues/:id/comments rejects empty body text", async () => {
    const res = await req(app, "POST", "/linear/issues/abc/comments", { body: "" });
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });

  it("POST /linear/issues/:id/comments rejects missing body", async () => {
    const res = await req(app, "POST", "/linear/issues/abc/comments", {});
    expect(res.status).toBe(400);
    const json = await res.json();
    expectValidationError(json);
  });
});
