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

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

const mockTemplateManager = vi.hoisted(() => ({
  listTemplates: vi.fn().mockReturnValue([]),
  getTemplate: vi.fn().mockReturnValue(null),
  createTemplate: vi.fn(),
  updateTemplate: vi.fn().mockReturnValue(null),
  deleteTemplate: vi.fn().mockReturnValue(false),
}));

vi.mock("../template-manager.js", () => mockTemplateManager);

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

import { registerTemplateRoutes } from "./template-routes.js";
import { errorHandler } from "../errors.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockTemplateManager.listTemplates.mockReturnValue([]);
  mockTemplateManager.getTemplate.mockReturnValue(null);
  mockTemplateManager.createTemplate.mockReset();
  mockTemplateManager.updateTemplate.mockReturnValue(null);
  mockTemplateManager.deleteTemplate.mockReturnValue(false);
  app = new Hono();
  app.onError(errorHandler());
  registerTemplateRoutes(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 makeTemplate(overrides: Record<string, unknown> = {}) {
  return {
    id: "tpl-1",
    name: "Test Template",
    description: "A test template",
    config: { backend: "claude" },
    createdAt: Date.now(),
    updatedAt: Date.now(),
    ...overrides,
  };
}

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

describe("GET /templates", () => {
  it("returns empty list", async () => {
    const res = await req("GET", "/templates");
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
  });

  it("returns templates list", async () => {
    const template = makeTemplate();
    mockTemplateManager.listTemplates.mockReturnValue([template]);
    const res = await req("GET", "/templates");
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toHaveLength(1);
    expect(data[0].id).toBe("tpl-1");
  });

  it("returns 500 on internal error", async () => {
    mockTemplateManager.listTemplates.mockImplementation(() => {
      throw new Error("boom");
    });
    const res = await req("GET", "/templates");
    expect(res.status).toBe(500);
    const body = await res.json();
    expect(body.error).toBe("boom");
  });
});

describe("GET /templates/:id", () => {
  it("returns 404 for non-existent template", async () => {
    const res = await req("GET", "/templates/nonexistent");
    expect(res.status).toBe(404);
  });

  it("returns template by id", async () => {
    const template = makeTemplate();
    mockTemplateManager.getTemplate.mockReturnValue(template);
    const res = await req("GET", "/templates/tpl-1");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.id).toBe("tpl-1");
  });
});

describe("POST /templates", () => {
  it("creates a template and returns 201", async () => {
    const created = makeTemplate({ id: "new-tpl" });
    mockTemplateManager.createTemplate.mockReturnValue(created);
    const res = await req("POST", "/templates", {
      name: "New Template",
      description: "A new one",
      config: { backend: "claude", model: "sonnet" },
    });
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.id).toBe("new-tpl");
    expect(mockTemplateManager.createTemplate).toHaveBeenCalledWith(
      "New Template",
      "A new one",
      { backend: "claude", model: "sonnet" },
    );
  });

  it("uses default backend when not specified", async () => {
    mockTemplateManager.createTemplate.mockReturnValue(makeTemplate());
    await req("POST", "/templates", {
      name: "Minimal",
      config: {},
    });
    expect(mockTemplateManager.createTemplate).toHaveBeenCalledWith(
      "Minimal",
      "",
      { backend: "claude" },
    );
  });

  it("returns 400 on store error", async () => {
    mockTemplateManager.createTemplate.mockImplementation(() => {
      throw new Error("Name is required");
    });
    const res = await req("POST", "/templates", { name: "", config: { backend: "claude" } });
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Name is required");
  });

  it("returns 400 on validation error (missing config)", async () => {
    const res = await req("POST", "/templates", { name: "No config" });
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toBe("Validation failed");
  });

  it("returns 400 on validation error (name too long)", async () => {
    const res = await req("POST", "/templates", {
      name: "x".repeat(201),
      config: { backend: "claude" },
    });
    expect(res.status).toBe(400);
  });
});

describe("PUT /templates/:id", () => {
  it("returns 404 for non-existent template", async () => {
    const res = await req("PUT", "/templates/missing", { name: "Updated" });
    expect(res.status).toBe(404);
  });

  it("updates and returns template", async () => {
    const updated = makeTemplate({ name: "Updated" });
    mockTemplateManager.updateTemplate.mockReturnValue(updated);
    const res = await req("PUT", "/templates/tpl-1", { name: "Updated" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.name).toBe("Updated");
  });

  it("supports partial update (name only)", async () => {
    const updated = makeTemplate({ name: "New Name" });
    mockTemplateManager.updateTemplate.mockReturnValue(updated);
    const res = await req("PUT", "/templates/tpl-1", { name: "New Name" });
    expect(res.status).toBe(200);
    expect(mockTemplateManager.updateTemplate).toHaveBeenCalledWith("tpl-1", {
      name: "New Name",
    });
  });

  it("supports partial update (config only)", async () => {
    const updated = makeTemplate();
    mockTemplateManager.updateTemplate.mockReturnValue(updated);
    const res = await req("PUT", "/templates/tpl-1", {
      config: { backend: "codex", model: "o3" },
    });
    expect(res.status).toBe(200);
    expect(mockTemplateManager.updateTemplate).toHaveBeenCalledWith("tpl-1", {
      config: { backend: "codex", model: "o3" },
    });
  });

  it("returns 400 on store error", async () => {
    mockTemplateManager.updateTemplate.mockImplementation(() => {
      throw new Error("Invalid update");
    });
    const res = await req("PUT", "/templates/tpl-1", { name: "Bad" });
    expect(res.status).toBe(400);
  });
});

describe("DELETE /templates/:id", () => {
  it("returns 404 for non-existent template", async () => {
    const res = await req("DELETE", "/templates/missing");
    expect(res.status).toBe(404);
  });

  it("deletes template and returns ok", async () => {
    mockTemplateManager.deleteTemplate.mockReturnValue(true);
    const res = await req("DELETE", "/templates/tpl-1");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
  });
});
