import { and, desc, eq, isNull, lt } from "drizzle-orm";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { chatConversations, chatMessages } from "@paperclipai/db";
import { chatService } from "../services/chat.js";

/**
 * Build a query chain mock.
 * - `selectResult`: what `db.select()...limit()` resolves to (array)
 * - `returningResult`: what `db.insert/update()...returning()` resolves to (array)
 */
function makeSelectChain(rows: unknown[], opts: { terminalAtWhere?: boolean } = {}) {
  const chain: any = {};
  const methods = ["select", "from", "orderBy"];
  for (const m of methods) {
    chain[m] = vi.fn(() => chain);
  }
  chain.limit = vi.fn(() => Promise.resolve(rows));
  if (opts.terminalAtWhere) {
    // getConversation: select().from().where() — no orderBy/limit
    chain.where = vi.fn(() => Promise.resolve(rows));
  } else {
    // listConversations / listMessages: select().from().where().orderBy().limit()
    chain.where = vi.fn(() => chain);
  }
  return chain;
}

function makeUpdateChain(rows: unknown[]) {
  const chain: any = {};
  chain.set = vi.fn(() => chain);
  chain.where = vi.fn(() => chain);
  chain.returning = vi.fn(() => Promise.resolve(rows));
  return chain;
}

function makeInsertChain(rows: unknown[]) {
  const chain: any = {};
  chain.values = vi.fn(() => chain);
  chain.returning = vi.fn(() => Promise.resolve(rows));
  return chain;
}

function makeDb() {
  return {
    select: vi.fn(),
    insert: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  };
}

describe("chatService", () => {
  let db: ReturnType<typeof makeDb>;

  beforeEach(() => {
    vi.clearAllMocks();
    db = makeDb();
  });

  describe("createConversation", () => {
    it("inserts into chatConversations and returns the created row", async () => {
      const inserted = {
        id: "conv-1",
        companyId: "company-1",
        title: "Hello",
        agentId: null,
        pinnedAt: null,
        archivedAt: null,
        deletedAt: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const chain = makeInsertChain([inserted]);
      db.insert.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.createConversation("company-1", { title: "Hello" });

      expect(db.insert).toHaveBeenCalledWith(chatConversations);
      expect(chain.values).toHaveBeenCalledWith(
        expect.objectContaining({ companyId: "company-1", title: "Hello" }),
      );
      expect(result).toEqual(inserted);
    });

    it("returns the created conversation with id and timestamps", async () => {
      const now = new Date();
      const inserted = {
        id: "conv-1",
        companyId: "company-1",
        title: null,
        agentId: null,
        pinnedAt: null,
        archivedAt: null,
        deletedAt: null,
        createdAt: now,
        updatedAt: now,
      };
      const chain = makeInsertChain([inserted]);
      db.insert.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.createConversation("company-1", {});

      expect(result.id).toBe("conv-1");
      expect(result.createdAt).toBe(now);
    });
  });

  describe("listConversations", () => {
    it("returns conversations sorted by updatedAt DESC", async () => {
      const rows = [
        { id: "conv-2", updatedAt: new Date("2024-01-02") },
        { id: "conv-1", updatedAt: new Date("2024-01-01") },
      ];

      const chain = makeSelectChain(rows);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.listConversations("company-1", {});

      expect(db.select).toHaveBeenCalled();
      expect(chain.orderBy).toHaveBeenCalledWith(desc(chatConversations.updatedAt));
    });

    it("excludes soft-deleted conversations", async () => {
      const chain = makeSelectChain([]);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.listConversations("company-1", {});

      // where should be called with a combined condition that includes isNull(deletedAt)
      expect(chain.where).toHaveBeenCalled();
    });

    it("supports cursor-based pagination with hasMore", async () => {
      // Return limit+1 rows to trigger hasMore=true
      const rows = Array.from({ length: 31 }, (_, i) => ({
        id: `conv-${i}`,
        updatedAt: new Date(2024, 0, 31 - i),
      }));

      const chain = makeSelectChain(rows);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.listConversations("company-1", { limit: 30 });

      expect(result.hasMore).toBe(true);
      expect(result.items.length).toBe(30);
    });

    it("limits results to max 100", async () => {
      const chain = makeSelectChain([]);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.listConversations("company-1", { limit: 9999 });

      // limit should be called with 101 (100 + 1 for hasMore detection)
      expect(chain.limit).toHaveBeenCalledWith(101);
    });
  });

  describe("getConversation", () => {
    it("returns conversation by id", async () => {
      const conv = { id: "conv-1", companyId: "company-1", deletedAt: null };
      const chain = makeSelectChain([conv], { terminalAtWhere: true });
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.getConversation("conv-1");

      expect(result).toEqual(conv);
    });

    it("throws notFound for non-existent conversation", async () => {
      const chain = makeSelectChain([], { terminalAtWhere: true });
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await expect(svc.getConversation("no-such-id")).rejects.toMatchObject({
        status: 404,
      });
    });

    it("throws notFound for soft-deleted conversation", async () => {
      // When deletedAt is set, the WHERE clause won't match — returns empty
      const chain = makeSelectChain([], { terminalAtWhere: true });
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await expect(svc.getConversation("deleted-id")).rejects.toMatchObject({
        status: 404,
      });
    });
  });

  describe("updateConversation", () => {
    it("updates title and bumps updatedAt", async () => {
      const updated = { id: "conv-1", title: "New Title", updatedAt: new Date() };
      const chain = makeUpdateChain([updated]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.updateConversation("conv-1", { title: "New Title" });

      expect(db.update).toHaveBeenCalledWith(chatConversations);
      expect(chain.set).toHaveBeenCalledWith(
        expect.objectContaining({ title: "New Title", updatedAt: expect.any(Date) }),
      );
      expect(result).toEqual(updated);
    });

    it("sets pinnedAt timestamp when provided as string", async () => {
      const pinnedAt = "2024-06-01T00:00:00.000Z";
      const updated = { id: "conv-1", pinnedAt: new Date(pinnedAt) };
      const chain = makeUpdateChain([updated]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.updateConversation("conv-1", { pinnedAt });

      expect(chain.set).toHaveBeenCalledWith(
        expect.objectContaining({ pinnedAt: new Date(pinnedAt) }),
      );
    });

    it("clears pinnedAt when set to null", async () => {
      const updated = { id: "conv-1", pinnedAt: null };
      const chain = makeUpdateChain([updated]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.updateConversation("conv-1", { pinnedAt: null });

      expect(chain.set).toHaveBeenCalledWith(
        expect.objectContaining({ pinnedAt: null }),
      );
    });

    it("bumps updatedAt on every update", async () => {
      const before = new Date("2024-01-01");
      const updated = { id: "conv-1", updatedAt: new Date() };
      const chain = makeUpdateChain([updated]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.updateConversation("conv-1", { title: "x" });

      const setCall = chain.set.mock.calls[0][0];
      expect(setCall.updatedAt).toBeInstanceOf(Date);
      expect(setCall.updatedAt.getTime()).toBeGreaterThan(before.getTime());
    });
  });

  describe("softDeleteConversation", () => {
    it("sets deletedAt and returns the updated row", async () => {
      const deleted = { id: "conv-1", deletedAt: new Date() };
      const chain = makeUpdateChain([deleted]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.softDeleteConversation("conv-1");

      expect(db.update).toHaveBeenCalledWith(chatConversations);
      expect(chain.set).toHaveBeenCalledWith(
        expect.objectContaining({ deletedAt: expect.any(Date) }),
      );
      expect(result).toEqual(deleted);
    });

    it("throws notFound if conversation is already deleted (no row returned)", async () => {
      const chain = makeUpdateChain([]);
      db.update.mockReturnValue(chain);

      const svc = chatService(db as any);
      await expect(svc.softDeleteConversation("gone-id")).rejects.toMatchObject({
        status: 404,
      });
    });
  });

  describe("addMessage", () => {
    it("inserts a message row with conversationId and role", async () => {
      const message = { id: "msg-1", conversationId: "conv-1", role: "user", content: "Hi" };
      const insertChain = makeInsertChain([message]);
      const updateChain = makeUpdateChain([]);

      db.insert.mockReturnValue(insertChain);
      db.update.mockReturnValue(updateChain);

      const svc = chatService(db as any);
      const result = await svc.addMessage("conv-1", { role: "user", content: "Hi" });

      expect(db.insert).toHaveBeenCalledWith(chatMessages);
      expect(insertChain.values).toHaveBeenCalledWith(
        expect.objectContaining({ conversationId: "conv-1", role: "user", content: "Hi" }),
      );
      expect(result).toEqual({ ...message, files: [] });
    });

    it("bumps conversation updatedAt after insert (Pitfall 3)", async () => {
      const message = { id: "msg-1", conversationId: "conv-1", role: "user", content: "Hi" };
      const insertChain = makeInsertChain([message]);
      const updateChain = makeUpdateChain([]);

      db.insert.mockReturnValue(insertChain);
      db.update.mockReturnValue(updateChain);

      const svc = chatService(db as any);
      await svc.addMessage("conv-1", { role: "user", content: "Hi" });

      expect(db.update).toHaveBeenCalledWith(chatConversations);
    });

    it("auto-sets title from first user message when title is null (Pitfall 5)", async () => {
      const content = "This is a long message that should be truncated to 60 chars when used as title";
      const message = { id: "msg-1", conversationId: "conv-1", role: "user", content };
      const insertChain = makeInsertChain([message]);
      const updateChain = makeUpdateChain([]);

      db.insert.mockReturnValue(insertChain);
      db.update.mockReturnValue(updateChain);

      const svc = chatService(db as any);
      await svc.addMessage("conv-1", { role: "user", content });

      // Should be called twice: once for updatedAt bump, once for title auto-set
      expect(db.update).toHaveBeenCalledTimes(2);
    });

    it("does not auto-set title for assistant/system messages", async () => {
      const content = "Response from assistant";
      const message = { id: "msg-1", conversationId: "conv-1", role: "assistant", content };
      const insertChain = makeInsertChain([message]);
      const updateChain = makeUpdateChain([]);

      db.insert.mockReturnValue(insertChain);
      db.update.mockReturnValue(updateChain);

      const svc = chatService(db as any);
      await svc.addMessage("conv-1", { role: "assistant", content });

      // Only one update: updatedAt bump (no auto-title for non-user messages)
      expect(db.update).toHaveBeenCalledTimes(1);
    });
  });

  describe("listMessages", () => {
    it("returns messages for conversation sorted by createdAt DESC", async () => {
      const rows = [
        { id: "msg-2", createdAt: new Date("2024-01-02") },
        { id: "msg-1", createdAt: new Date("2024-01-01") },
      ];

      const chain = makeSelectChain(rows);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      await svc.listMessages("conv-1", {});

      expect(db.select).toHaveBeenCalled();
      expect(chain.orderBy).toHaveBeenCalledWith(desc(chatMessages.createdAt));
    });

    it("supports cursor-based pagination", async () => {
      const rows = Array.from({ length: 51 }, (_, i) => ({
        id: `msg-${i}`,
        createdAt: new Date(2024, 0, 51 - i),
      }));

      const chain = makeSelectChain(rows);
      db.select.mockReturnValue(chain);

      const svc = chatService(db as any);
      const result = await svc.listMessages("conv-1", { limit: 50 });

      expect(result.hasMore).toBe(true);
      expect(result.items.length).toBe(50);
    });
  });

  describe("searchMessages", () => {
    it.todo("returns ranked results for matching term");
    it.todo("returns empty for no match");
    it.todo("respects companyId scope");
  });

  describe("toggleBookmark", () => {
    it.todo("creates bookmark when not exists");
    it.todo("removes bookmark when exists");
  });

  describe("branchConversation", () => {
    it.todo("creates child conversation with copied messages");
    it.todo("throws not found for invalid message id");
  });

  describe("exportConversation", () => {
    it.todo("exports as markdown with agent names");
    it.todo("exports as JSON with all messages");
  });
});
