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

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

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);

const mockScheduler = {
  getNextRunTime: vi.fn().mockReturnValue(null),
  scheduleJob: vi.fn(),
  stopJob: vi.fn(),
  executeJobManually: vi.fn(),
  getExecutions: vi.fn().mockReturnValue([]),
};

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

import { registerCronRoutes } from "./cron-routes.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  mockCronStore.listJobs.mockReturnValue([]);
  mockCronStore.getJob.mockReturnValue(null);
  mockCronStore.createJob.mockReset();
  mockCronStore.updateJob.mockReturnValue(null);
  mockCronStore.deleteJob.mockReturnValue(false);
  mockScheduler.getNextRunTime.mockReturnValue(null);
  mockScheduler.scheduleJob.mockReset();
  mockScheduler.stopJob.mockReset();
  mockScheduler.executeJobManually.mockReset();
  mockScheduler.getExecutions.mockReturnValue([]);
  app = new Hono();
  registerCronRoutes(
    app,
    mockScheduler as unknown as import("../cron-scheduler.js").CronScheduler,
  );
});

/* ── 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 makeJob(overrides: Record<string, unknown> = {}) {
  return {
    id: "job-1",
    name: "Test Job",
    prompt: "Do something",
    schedule: "0 9 * * *",
    recurring: true,
    backendType: "claude",
    model: "",
    cwd: "/tmp",
    enabled: true,
    permissionMode: "bypassPermissions",
    createdAt: Date.now(),
    updatedAt: Date.now(),
    ...overrides,
  };
}

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

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

  it("returns enriched jobs with nextRunAt", async () => {
    const job = makeJob();
    mockCronStore.listJobs.mockReturnValue([job]);
    mockScheduler.getNextRunTime.mockReturnValue(new Date("2025-01-01T00:00:00Z"));

    const res = await req("GET", "/cron/jobs");
    const data = await res.json();
    expect(data).toHaveLength(1);
    expect(data[0].nextRunAt).toBe(1735689600000);
  });
});

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

  it("returns job with nextRunAt", async () => {
    const job = makeJob();
    mockCronStore.getJob.mockReturnValue(job);

    const res = await req("GET", "/cron/jobs/job-1");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.id).toBe("job-1");
    expect(body.nextRunAt).toBeNull();
  });
});

describe("POST /cron/jobs", () => {
  it("creates a job and returns 201", async () => {
    const created = makeJob({ id: "new-job" });
    mockCronStore.createJob.mockReturnValue(created);

    const res = await req("POST", "/cron/jobs", {
      name: "New Job",
      prompt: "Do things",
      schedule: "0 * * * *",
    });
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.id).toBe("new-job");
    expect(mockScheduler.scheduleJob).toHaveBeenCalledWith(created);
  });

  it("does not schedule disabled job", async () => {
    const created = makeJob({ enabled: false });
    mockCronStore.createJob.mockReturnValue(created);

    await req("POST", "/cron/jobs", { name: "Disabled", enabled: false });
    expect(mockScheduler.scheduleJob).not.toHaveBeenCalled();
  });

  it("returns 400 on store error", async () => {
    mockCronStore.createJob.mockImplementation(() => {
      throw new Error("Invalid schedule");
    });
    const res = await req("POST", "/cron/jobs", { name: "Bad" });
    expect(res.status).toBe(400);
  });
});

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

  it("updates and reschedules the job", async () => {
    const updated = makeJob({ name: "Updated" });
    mockCronStore.updateJob.mockReturnValue(updated);

    const res = await req("PUT", "/cron/jobs/job-1", { name: "Updated" });
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.name).toBe("Updated");
    expect(mockScheduler.scheduleJob).toHaveBeenCalledWith(updated);
  });

  it("returns 400 on store error", async () => {
    mockCronStore.updateJob.mockImplementation(() => {
      throw new Error("Bad update");
    });
    const res = await req("PUT", "/cron/jobs/job-1", { name: "Nope" });
    expect(res.status).toBe(400);
  });
});

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

  it("deletes the job and stops scheduler", async () => {
    mockCronStore.deleteJob.mockReturnValue(true);
    const res = await req("DELETE", "/cron/jobs/job-1");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockScheduler.stopJob).toHaveBeenCalledWith("job-1");
  });
});

describe("POST /cron/jobs/:id/toggle", () => {
  it("returns 404 for non-existent job", async () => {
    const res = await req("POST", "/cron/jobs/missing/toggle");
    expect(res.status).toBe(404);
  });

  it("toggles enabled and reschedules", async () => {
    const job = makeJob({ enabled: true });
    mockCronStore.getJob.mockReturnValue(job);
    const toggled = makeJob({ enabled: false });
    mockCronStore.updateJob.mockReturnValue(toggled);

    const res = await req("POST", "/cron/jobs/job-1/toggle");
    expect(res.status).toBe(200);
    expect(mockCronStore.updateJob).toHaveBeenCalledWith("job-1", { enabled: false });
    expect(mockScheduler.stopJob).toHaveBeenCalledWith("job-1");
  });

  it("schedules when toggling on", async () => {
    const job = makeJob({ enabled: false });
    mockCronStore.getJob.mockReturnValue(job);
    const toggled = makeJob({ enabled: true });
    mockCronStore.updateJob.mockReturnValue(toggled);

    await req("POST", "/cron/jobs/job-1/toggle");
    expect(mockScheduler.scheduleJob).toHaveBeenCalledWith(toggled);
  });
});

describe("POST /cron/jobs/:id/run", () => {
  it("returns 404 for non-existent job", async () => {
    const res = await req("POST", "/cron/jobs/missing/run");
    expect(res.status).toBe(404);
  });

  it("triggers manual execution", async () => {
    mockCronStore.getJob.mockReturnValue(makeJob());
    const res = await req("POST", "/cron/jobs/job-1/run");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockScheduler.executeJobManually).toHaveBeenCalledWith("job-1");
  });
});

describe("GET /cron/jobs/:id/executions", () => {
  it("returns executions list", async () => {
    const executions = [{ sessionId: "s1", startedAt: Date.now(), status: "completed" }];
    mockScheduler.getExecutions.mockReturnValue(executions);

    const res = await req("GET", "/cron/jobs/job-1/executions");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body).toEqual(executions);
  });

  it("returns empty array when no scheduler", async () => {
    // Test with no scheduler
    const appNoSched = new Hono();
    registerCronRoutes(appNoSched);
    const res = await appNoSched.request("/cron/jobs/job-1/executions");
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
  });
});
