import express from "express";
import request from "supertest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware/index.js";
import { chatRoutes } from "../routes/chat.js";

const mockChatService = vi.hoisted(() => ({
  createConversation: vi.fn(),
  listConversations: vi.fn(),
  getConversation: vi.fn(),
  updateConversation: vi.fn(),
  softDeleteConversation: vi.fn(),
  listMessages: vi.fn(),
  addMessage: vi.fn(),
}));

vi.mock("../services/chat.js", () => ({
  chatService: () => mockChatService,
}));

function createApp() {
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => {
    (req as any).actor = {
      type: "board",
      userId: "user-1",
      companyIds: ["company-1"],
      source: "session",
      isInstanceAdmin: false,
    };
    next();
  });
  app.use("/api", chatRoutes({} as any));
  app.use(errorHandler);
  return app;
}

describe("chatRoutes", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe("POST /companies/:companyId/conversations", () => {
    it("creates a conversation and returns 201", async () => {
      const conv = { id: "conv-1", companyId: "company-1", title: null };
      mockChatService.createConversation.mockResolvedValue(conv);

      const res = await request(createApp())
        .post("/api/companies/company-1/conversations")
        .send({});

      expect(res.status).toBe(201);
      expect(res.body).toEqual(conv);
      expect(mockChatService.createConversation).toHaveBeenCalledWith("company-1", {});
    });

    it("accepts optional title and agentId", async () => {
      const conv = { id: "conv-1", companyId: "company-1", title: "My Chat" };
      mockChatService.createConversation.mockResolvedValue(conv);

      const res = await request(createApp())
        .post("/api/companies/company-1/conversations")
        .send({ title: "My Chat", agentId: "00000000-0000-0000-0000-000000000001" });

      expect(res.status).toBe(201);
      expect(mockChatService.createConversation).toHaveBeenCalledWith(
        "company-1",
        { title: "My Chat", agentId: "00000000-0000-0000-0000-000000000001" },
      );
    });
  });

  describe("GET /companies/:companyId/conversations", () => {
    it("returns paginated conversation list", async () => {
      const list = { items: [{ id: "conv-1" }], hasMore: false, nextCursor: null };
      mockChatService.listConversations.mockResolvedValue(list);

      const res = await request(createApp())
        .get("/api/companies/company-1/conversations");

      expect(res.status).toBe(200);
      expect(res.body).toEqual(list);
      expect(mockChatService.listConversations).toHaveBeenCalledWith(
        "company-1",
        { cursor: undefined, limit: undefined, includeArchived: false },
      );
    });

    it("supports cursor query param", async () => {
      const list = { items: [], hasMore: false, nextCursor: null };
      mockChatService.listConversations.mockResolvedValue(list);

      const res = await request(createApp())
        .get("/api/companies/company-1/conversations?cursor=2024-01-01T00:00:00.000Z&limit=10");

      expect(res.status).toBe(200);
      expect(mockChatService.listConversations).toHaveBeenCalledWith(
        "company-1",
        expect.objectContaining({ cursor: "2024-01-01T00:00:00.000Z", limit: 10 }),
      );
    });
  });

  describe("GET /conversations/:id", () => {
    it("returns conversation by id", async () => {
      const conv = { id: "conv-1", companyId: "company-1" };
      mockChatService.getConversation.mockResolvedValue(conv);

      const res = await request(createApp()).get("/api/conversations/conv-1");

      expect(res.status).toBe(200);
      expect(res.body).toEqual(conv);
      expect(mockChatService.getConversation).toHaveBeenCalledWith("conv-1");
    });

    it("returns 404 for non-existent conversation", async () => {
      const { HttpError } = await import("../errors.js");
      mockChatService.getConversation.mockRejectedValue(
        new HttpError(404, "Conversation not found"),
      );

      const res = await request(createApp()).get("/api/conversations/no-such-id");

      expect(res.status).toBe(404);
    });
  });

  describe("PATCH /conversations/:id", () => {
    it("updates conversation fields", async () => {
      const updated = { id: "conv-1", title: "Updated" };
      mockChatService.updateConversation.mockResolvedValue(updated);

      const res = await request(createApp())
        .patch("/api/conversations/conv-1")
        .send({ title: "Updated" });

      expect(res.status).toBe(200);
      expect(res.body).toEqual(updated);
      expect(mockChatService.updateConversation).toHaveBeenCalledWith(
        "conv-1",
        { title: "Updated" },
      );
    });
  });

  describe("DELETE /conversations/:id", () => {
    it("soft-deletes and returns 204", async () => {
      mockChatService.softDeleteConversation.mockResolvedValue({ id: "conv-1" });

      const res = await request(createApp()).delete("/api/conversations/conv-1");

      expect(res.status).toBe(204);
      expect(mockChatService.softDeleteConversation).toHaveBeenCalledWith("conv-1");
    });
  });

  describe("POST /conversations/:id/messages", () => {
    it("creates a message and returns 201", async () => {
      const message = { id: "msg-1", conversationId: "conv-1", role: "user", content: "Hello" };
      mockChatService.addMessage.mockResolvedValue(message);

      const res = await request(createApp())
        .post("/api/conversations/conv-1/messages")
        .send({ role: "user", content: "Hello" });

      expect(res.status).toBe(201);
      expect(res.body).toEqual(message);
      expect(mockChatService.addMessage).toHaveBeenCalledWith(
        "conv-1",
        { role: "user", content: "Hello" },
      );
    });

    it("rejects invalid role", async () => {
      const res = await request(createApp())
        .post("/api/conversations/conv-1/messages")
        .send({ role: "invalid-role", content: "Hello" });

      expect(res.status).toBe(400);
    });
  });

  describe("GET /conversations/:id/messages", () => {
    it("returns paginated message list", async () => {
      const list = { items: [{ id: "msg-1" }], hasMore: false, nextCursor: null };
      mockChatService.listMessages.mockResolvedValue(list);

      const res = await request(createApp()).get("/api/conversations/conv-1/messages");

      expect(res.status).toBe(200);
      expect(res.body).toEqual(list);
      expect(mockChatService.listMessages).toHaveBeenCalledWith(
        "conv-1",
        { cursor: undefined, limit: undefined },
      );
    });
  });

  describe("GET /companies/:id/messages/search", () => {
    it.todo("returns 200 with search results");
    it.todo("returns 400 for short query");
  });

  describe("POST /conversations/:id/bookmarks", () => {
    it.todo("toggles bookmark on/off");
  });

  describe("POST /conversations/:id/branch", () => {
    it.todo("returns 201 with branched conversation");
  });

  describe("GET /conversations/:id/export", () => {
    it.todo("returns markdown file download");
    it.todo("returns JSON file download");
  });
});
