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

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

const mockExistsSync = vi.hoisted(() => vi.fn().mockReturnValue(false));
const mockReaddir = vi.hoisted(() => vi.fn().mockResolvedValue([]));
const mockReadFile = vi.hoisted(() => vi.fn().mockResolvedValue(""));
const mockWriteFile = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockRm = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const mockMkdir = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));

vi.mock("node:fs", () => ({ existsSync: mockExistsSync }));
vi.mock("node:fs/promises", () => ({
  readdir: mockReaddir,
  readFile: mockReadFile,
  writeFile: mockWriteFile,
  rm: mockRm,
  mkdir: mockMkdir,
}));

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

import { registerSkillRoutes } from "./skills-routes.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockExistsSync.mockReturnValue(false);
  mockReaddir.mockResolvedValue([]);
  mockReadFile.mockResolvedValue("");
  mockWriteFile.mockResolvedValue(undefined);
  mockRm.mockResolvedValue(undefined);
  mockMkdir.mockResolvedValue(undefined);
  app = new Hono();
  registerSkillRoutes(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);
}

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

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

  it("returns skills list", async () => {
    mockExistsSync.mockImplementation((p: string) => {
      if (typeof p === "string" && p.endsWith("skills")) return true;
      if (typeof p === "string" && p.endsWith("SKILL.md")) return true;
      return false;
    });
    mockReaddir.mockResolvedValue([
      { name: "my-skill", isDirectory: () => true },
    ]);
    mockReadFile.mockResolvedValue(
      "---\nname: My Skill\ndescription: A test skill\n---\n\n# My Skill\n",
    );
    const res = await req("GET", "/skills");
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toHaveLength(1);
    expect(data[0].slug).toBe("my-skill");
    expect(data[0].name).toBe("My Skill");
  });

  it("returns 500 on file read error", async () => {
    mockExistsSync.mockReturnValue(true);
    mockReaddir.mockRejectedValue(new Error("Permission denied"));
    const res = await req("GET", "/skills");
    expect(res.status).toBe(500);
  });
});

describe("GET /skills/:slug", () => {
  it("returns 404 for non-existent skill", async () => {
    const res = await req("GET", "/skills/missing");
    expect(res.status).toBe(404);
  });

  it("returns 400 for invalid slug with path traversal", async () => {
    const res = await req("GET", "/skills/..%2F..%2Fetc");
    expect(res.status).toBe(400);
  });

  it("returns skill content", async () => {
    mockExistsSync.mockReturnValue(true);
    mockReadFile.mockResolvedValue("# My Skill\nSome content");
    const res = await req("GET", "/skills/my-skill");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.slug).toBe("my-skill");
    expect(body.content).toBe("# My Skill\nSome content");
  });
});

describe("POST /skills", () => {
  it("creates a skill", async () => {
    mockExistsSync.mockReturnValue(false);
    const res = await req("POST", "/skills", {
      name: "New Skill",
      description: "A new skill",
      content: "# New Skill",
    });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.slug).toBe("new-skill");
    expect(mockMkdir).toHaveBeenCalled();
    expect(mockWriteFile).toHaveBeenCalled();
  });

  it("returns 409 when skill already exists", async () => {
    mockExistsSync.mockReturnValue(true);
    const res = await req("POST", "/skills", {
      name: "Existing",
    });
    expect(res.status).toBe(409);
  });

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

  it("returns 400 on invalid name that produces empty slug", async () => {
    const res = await req("POST", "/skills", { name: "---" });
    expect(res.status).toBe(400);
  });
});

describe("PUT /skills/:slug", () => {
  it("returns 404 for non-existent skill", async () => {
    const res = await req("PUT", "/skills/missing", { content: "updated" });
    expect(res.status).toBe(404);
  });

  it("returns 400 for invalid slug", async () => {
    const res = await req("PUT", "/skills/..%2Fhack", { content: "x" });
    expect(res.status).toBe(400);
  });

  it("updates skill content", async () => {
    mockExistsSync.mockReturnValue(true);
    const res = await req("PUT", "/skills/my-skill", { content: "updated content" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockWriteFile).toHaveBeenCalled();
  });

  it("returns 400 when content is missing", async () => {
    mockExistsSync.mockReturnValue(true);
    const res = await req("PUT", "/skills/my-skill", {});
    expect(res.status).toBe(400);
  });
});

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

  it("returns 400 for invalid slug", async () => {
    const res = await req("DELETE", "/skills/..%2Fhack");
    expect(res.status).toBe(400);
  });

  it("deletes skill directory", async () => {
    mockExistsSync.mockReturnValue(true);
    const res = await req("DELETE", "/skills/my-skill");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockRm).toHaveBeenCalled();
  });
});
