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

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

const mockStore = 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", () => mockStore);

const mockExecutor = {
  executeAgentManually: vi.fn().mockResolvedValue({ sessionId: "sess-1" }),
  getNextRunTime: vi.fn().mockReturnValue(null),
  getExecutions: vi.fn().mockReturnValue([]),
  scheduleAgent: vi.fn(),
  stopAgent: vi.fn(),
};

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

import { registerAgentRoutes } from "./agent-routes.js";
import { errorHandler } from "../errors.js";

let app: Hono;

beforeEach(() => {
  vi.restoreAllMocks();
  // Reset all mocks to their defaults
  mockStore.listAgents.mockReturnValue([]);
  mockStore.getAgent.mockReturnValue(null);
  mockStore.createAgent.mockReset();
  mockStore.updateAgent.mockReturnValue(null);
  mockStore.deleteAgent.mockReturnValue(false);
  mockStore.regenerateWebhookSecret.mockReturnValue(null);
  mockExecutor.executeAgentManually.mockReset();
  mockExecutor.getNextRunTime.mockReturnValue(null);
  mockExecutor.getExecutions.mockReturnValue([]);
  mockExecutor.scheduleAgent.mockReset();
  mockExecutor.stopAgent.mockReset();
  app = new Hono();
  app.onError(errorHandler());
  registerAgentRoutes(
    app,
    mockExecutor as unknown as import("../agent-executor.js").AgentExecutor,
  );
});

/* ── 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 makeAgent(overrides: Record<string, unknown> = {}) {
  return {
    id: "test-agent",
    name: "Test Agent",
    prompt: "Do stuff",
    description: "A test agent",
    version: 1,
    backendType: "claude",
    model: "claude-sonnet-4-6",
    cwd: "/tmp",
    enabled: true,
    permissionMode: "bypassPermissions",
    totalRuns: 0,
    consecutiveFailures: 0,
    createdAt: Date.now(),
    updatedAt: Date.now(),
    ...overrides,
  };
}

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

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

  it("returns enriched agents with nextRunAt", async () => {
    const agent = makeAgent();
    mockStore.listAgents.mockReturnValue([agent]);
    mockExecutor.getNextRunTime.mockReturnValue(new Date("2025-01-01T00:00:00Z"));

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

describe("POST /agents", () => {
  it("creates an agent and returns 201", async () => {
    const created = makeAgent({ id: "new-agent", name: "New Agent" });
    mockStore.createAgent.mockReturnValue(created);

    const res = await req("POST", "/agents", {
      name: "New Agent",
      prompt: "Do things",
      backendType: "claude",
    });

    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.id).toBe("new-agent");
    expect(mockStore.createAgent).toHaveBeenCalledTimes(1);
  });

  it("returns 400 on store validation error", async () => {
    mockStore.createAgent.mockImplementation(() => {
      throw new Error("Agent name is required");
    });

    const res = await req("POST", "/agents", { prompt: "hello" });
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Agent name is required");
  });
});

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

  it("returns agent with nextRunAt", async () => {
    const agent = makeAgent();
    mockStore.getAgent.mockReturnValue(agent);
    mockExecutor.getNextRunTime.mockReturnValue(null);

    const res = await req("GET", "/agents/test-agent");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.id).toBe("test-agent");
    expect(body.nextRunAt).toBeNull();
  });
});

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

  it("updates and reschedules the agent", async () => {
    const updated = makeAgent({
      name: "Updated Agent",
      enabled: true,
      triggers: { schedule: { enabled: true, expression: "0 9 * * *", recurring: true } },
    });
    mockStore.updateAgent.mockReturnValue(updated);

    const res = await req("PUT", "/agents/test-agent", { name: "Updated Agent" });
    expect(res.status).toBe(200);

    const body = await res.json();
    expect(body.name).toBe("Updated Agent");
    expect(mockExecutor.scheduleAgent).toHaveBeenCalledWith(updated);
  });

  it("returns 400 on store validation error", async () => {
    mockStore.updateAgent.mockImplementation(() => {
      throw new Error("An agent with a similar name already exists");
    });

    const res = await req("PUT", "/agents/test-agent", { name: "Duplicate" });
    expect(res.status).toBe(400);
  });
});

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

  it("deletes the agent and stops its schedule", async () => {
    mockStore.deleteAgent.mockReturnValue(true);

    const res = await req("DELETE", "/agents/test-agent");
    expect(res.status).toBe(200);
    expect(mockExecutor.stopAgent).toHaveBeenCalledWith("test-agent");
  });
});

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

  it("toggles enabled state and reschedules", async () => {
    const agent = makeAgent({ enabled: true });
    mockStore.getAgent.mockReturnValue(agent);
    const toggled = makeAgent({ enabled: false });
    mockStore.updateAgent.mockReturnValue(toggled);

    const res = await req("POST", "/agents/test-agent/toggle");
    expect(res.status).toBe(200);

    // Should update with inverted enabled state
    expect(mockStore.updateAgent).toHaveBeenCalledWith("test-agent", { enabled: false });
  });
});

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

  it("executes the agent manually", async () => {
    mockStore.getAgent.mockReturnValue(makeAgent());

    const res = await req("POST", "/agents/test-agent/run", { input: "custom input" });
    expect(res.status).toBe(200);

    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockExecutor.executeAgentManually).toHaveBeenCalledWith("test-agent", "custom input");
  });

  it("returns 200 even if executor fires async", async () => {
    mockStore.getAgent.mockReturnValue(makeAgent());

    const res = await req("POST", "/agents/test-agent/run");
    expect(res.status).toBe(200);
  });
});

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

    const res = await req("GET", "/agents/test-agent/executions");
    expect(res.status).toBe(200);

    const body = await res.json();
    expect(body).toEqual(executions);
  });
});

describe("POST /agents/import", () => {
  it("imports an agent config and returns 201", async () => {
    const exported = {
      name: "Imported Agent",
      prompt: "Do imported things",
      version: 1,
      backendType: "claude",
      model: "claude-sonnet-4-6",
      cwd: "/tmp",
      permissionMode: "bypassPermissions",
    };
    const created = makeAgent({ id: "imported-agent", ...exported });
    mockStore.createAgent.mockReturnValue(created);

    const res = await req("POST", "/agents/import", exported);
    expect(res.status).toBe(201);

    // Import should force enabled=false
    const createCall = mockStore.createAgent.mock.calls[0][0];
    expect(createCall.enabled).toBe(false);
  });
});

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

  it("exports agent without tracking fields", async () => {
    const agent = makeAgent({
      totalRuns: 42,
      consecutiveFailures: 3,
      lastRunAt: Date.now(),
    });
    mockStore.getAgent.mockReturnValue(agent);

    const res = await req("GET", "/agents/test-agent/export");
    expect(res.status).toBe(200);

    const body = await res.json();
    expect(body).not.toHaveProperty("id");
    expect(body).not.toHaveProperty("createdAt");
    expect(body).not.toHaveProperty("updatedAt");
    expect(body).not.toHaveProperty("totalRuns");
    expect(body).not.toHaveProperty("consecutiveFailures");
    expect(body).not.toHaveProperty("lastRunAt");
    expect(body).not.toHaveProperty("enabled");
    expect(body.name).toBe("Test Agent");
  });
});

describe("POST /agents/:id/regenerate-secret", () => {
  it("returns 404 for non-existent agent", async () => {
    const res = await req("POST", "/agents/missing/regenerate-secret");
    expect(res.status).toBe(404);
  });

  it("regenerates and returns the updated agent", async () => {
    const updated = makeAgent({
      triggers: { webhook: { enabled: true, secret: "new-hex-secret" } },
    });
    mockStore.regenerateWebhookSecret.mockReturnValue(updated);

    const res = await req("POST", "/agents/test-agent/regenerate-secret");
    expect(res.status).toBe(200);

    const body = await res.json();
    expect(body.triggers.webhook.secret).toBe("new-hex-secret");
  });
});

describe("POST /agents/:id/webhook/:secret", () => {
  it("returns 404 when agent doesn't exist", async () => {
    const res = await req("POST", "/agents/missing/webhook/abc123");
    expect(res.status).toBe(404);
  });

  it("returns 403 when webhook trigger is not enabled", async () => {
    mockStore.getAgent.mockReturnValue(makeAgent());
    const res = await req("POST", "/agents/test-agent/webhook/abc123");
    expect(res.status).toBe(403);
  });

  it("returns 403 when webhook is disabled", async () => {
    mockStore.getAgent.mockReturnValue(
      makeAgent({
        triggers: { webhook: { enabled: false, secret: "abc123" } },
      }),
    );
    const res = await req("POST", "/agents/test-agent/webhook/abc123");
    expect(res.status).toBe(403);
  });

  it("returns 401 for invalid secret", async () => {
    mockStore.getAgent.mockReturnValue(
      makeAgent({
        triggers: { webhook: { enabled: true, secret: "correct-secret" } },
      }),
    );
    const res = await req("POST", "/agents/test-agent/webhook/wrong-secret");
    expect(res.status).toBe(401);
  });

  it("triggers execution on valid webhook", async () => {
    mockStore.getAgent.mockReturnValue(
      makeAgent({
        triggers: { webhook: { enabled: true, secret: "correct-secret" } },
      }),
    );

    const res = await req(
      "POST",
      "/agents/test-agent/webhook/correct-secret",
      { input: "webhook payload" },
    );

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockExecutor.executeAgentManually).toHaveBeenCalledWith(
      "test-agent",
      "webhook payload",
    );
  });
});

describe("POST /agents/:id/webhook/test", () => {
  it("returns 404 for non-existent agent", async () => {
    const res = await req("POST", "/agents/no-agent/webhook/test");
    expect(res.status).toBe(404);
  });

  it("returns 400 when webhook not enabled", async () => {
    mockStore.getAgent.mockReturnValue(makeAgent());
    const res = await req("POST", "/agents/test-agent/webhook/test");
    expect(res.status).toBe(400);
  });

  it("triggers execution when webhook is enabled", async () => {
    mockStore.getAgent.mockReturnValue(
      makeAgent({
        triggers: { webhook: { enabled: true, secret: "abc123" } },
      }),
    );

    const res = await req("POST", "/agents/test-agent/webhook/test");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.ok).toBe(true);
    expect(mockExecutor.executeAgentManually).toHaveBeenCalledWith("test-agent");
  });
});
