import { describe, it, expect, vi, beforeEach } from "vitest";
import { chatFileService, deriveCategory } from "../services/chat-files.js";

// Mock @paperclipai/db module
const mockReturning = vi.fn();
const mockThen = vi.fn();
const mockLimit = vi.fn();
const mockOrderBy = vi.fn();
const mockWhere = vi.fn();
const mockSet = vi.fn();
const mockValues = vi.fn();
const mockInsert = vi.fn();
const mockSelect = vi.fn();
const mockFrom = vi.fn();
const mockUpdate = vi.fn();

vi.mock("@paperclipai/db", () => ({
  chatFiles: { id: "id", companyId: "company_id", conversationId: "conversation_id", messageId: "message_id" },
  chatFileReferences: { id: "id", fileId: "file_id" },
}));

function makeDb() {
  const returning = vi.fn();
  const limit = vi.fn();
  const orderBy = vi.fn();
  const where = vi.fn();
  const set = vi.fn();
  const values = vi.fn();
  const from = vi.fn();

  // Chain setup
  const insertChain = {
    values: values.mockReturnValue({ returning }),
  };

  const selectChain = {
    from: from.mockReturnValue({ where: where.mockReturnValue({ orderBy: orderBy.mockReturnValue({ limit }), then: undefined as unknown as typeof Promise.prototype.then }) }),
  };

  const updateChain = {
    set: set.mockReturnValue({ where: where.mockReturnValue({ returning }) }),
  };

  const db = {
    insert: vi.fn().mockReturnValue(insertChain),
    select: vi.fn().mockReturnValue(selectChain),
    update: vi.fn().mockReturnValue(updateChain),
  };

  return { db, returning, limit, orderBy, where, set, values, from };
}

describe("deriveCategory", () => {
  it("returns 'image' for image/* mime types", () => {
    expect(deriveCategory("image/png")).toBe("image");
    expect(deriveCategory("image/jpeg")).toBe("image");
    expect(deriveCategory("image/gif")).toBe("image");
    expect(deriveCategory("image/webp")).toBe("image");
    expect(deriveCategory("image/svg+xml")).toBe("image");
  });

  it("returns 'code' for code mime types", () => {
    expect(deriveCategory("text/javascript")).toBe("code");
    expect(deriveCategory("text/typescript")).toBe("code");
    expect(deriveCategory("text/css")).toBe("code");
    expect(deriveCategory("text/html")).toBe("code");
    expect(deriveCategory("application/json")).toBe("code");
    expect(deriveCategory("text/x-python")).toBe("code");
    expect(deriveCategory("text/x-java")).toBe("code");
  });

  it("returns 'document' for document mime types", () => {
    expect(deriveCategory("application/pdf")).toBe("document");
    expect(deriveCategory("text/plain")).toBe("document");
    expect(deriveCategory("text/markdown")).toBe("document");
    expect(deriveCategory("text/csv")).toBe("document");
  });

  it("returns 'other' for unrecognized mime types", () => {
    expect(deriveCategory("application/octet-stream")).toBe("other");
    expect(deriveCategory("video/mp4")).toBe("other");
    expect(deriveCategory("audio/mpeg")).toBe("other");
    expect(deriveCategory("application/zip")).toBe("other");
  });

  it("is case-insensitive", () => {
    expect(deriveCategory("IMAGE/PNG")).toBe("image");
    expect(deriveCategory("TEXT/JAVASCRIPT")).toBe("code");
    expect(deriveCategory("APPLICATION/PDF")).toBe("document");
  });
});

describe("chatFileService", () => {
  describe("create", () => {
    it("inserts a row and returns the inserted record", async () => {
      const mockFile = {
        id: "file-1",
        companyId: "company-1",
        filename: "hello.txt",
        originalFilename: "hello.txt",
        mimeType: "text/plain",
        sizeBytes: 100,
        objectKey: "chat-files/hello.txt",
        sha256: "abc123",
        source: "user_upload",
        category: "document",
        conversationId: "conv-1",
        messageId: null,
        projectId: null,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      const returningFn = vi.fn().mockResolvedValue([mockFile]);
      const valuesFn = vi.fn().mockReturnValue({ returning: returningFn });
      const insertFn = vi.fn().mockReturnValue({ values: valuesFn });

      const db = { insert: insertFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      const result = await svc.create("company-1", {
        filename: "hello.txt",
        originalFilename: "hello.txt",
        mimeType: "text/plain",
        sizeBytes: 100,
        objectKey: "chat-files/hello.txt",
        sha256: "abc123",
        source: "user_upload",
        category: "document",
        conversationId: "conv-1",
        messageId: null,
        projectId: null,
      });

      expect(result).toEqual(mockFile);
      expect(insertFn).toHaveBeenCalled();
      expect(valuesFn).toHaveBeenCalledWith(
        expect.objectContaining({ companyId: "company-1", filename: "hello.txt" }),
      );
    });
  });

  describe("getById", () => {
    it("returns null when no row found", async () => {
      const thenFn = vi.fn().mockImplementation((cb: (rows: unknown[]) => unknown) => Promise.resolve(cb([])));
      const whereFn = vi.fn().mockReturnValue({ then: thenFn });
      const fromFn = vi.fn().mockReturnValue({ where: whereFn });
      const selectFn = vi.fn().mockReturnValue({ from: fromFn });

      const db = { select: selectFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      const result = await svc.getById("nonexistent-id");
      expect(result).toBeNull();
    });

    it("returns the row when found", async () => {
      const mockFile = { id: "file-1", companyId: "company-1" };
      const thenFn = vi.fn().mockImplementation((cb: (rows: unknown[]) => unknown) => Promise.resolve(cb([mockFile])));
      const whereFn = vi.fn().mockReturnValue({ then: thenFn });
      const fromFn = vi.fn().mockReturnValue({ where: whereFn });
      const selectFn = vi.fn().mockReturnValue({ from: fromFn });

      const db = { select: selectFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      const result = await svc.getById("file-1");
      expect(result).toEqual(mockFile);
    });
  });

  describe("listByConversation", () => {
    it("queries by conversationId with default limit 50", async () => {
      const mockFiles = [{ id: "file-1" }, { id: "file-2" }];
      const limitFn = vi.fn().mockResolvedValue(mockFiles);
      const orderByFn = vi.fn().mockReturnValue({ limit: limitFn });
      const whereFn = vi.fn().mockReturnValue({ orderBy: orderByFn });
      const fromFn = vi.fn().mockReturnValue({ where: whereFn });
      const selectFn = vi.fn().mockReturnValue({ from: fromFn });

      const db = { select: selectFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      const result = await svc.listByConversation("conv-1");
      expect(result).toEqual(mockFiles);
      expect(limitFn).toHaveBeenCalledWith(50);
    });

    it("respects custom limit", async () => {
      const limitFn = vi.fn().mockResolvedValue([]);
      const orderByFn = vi.fn().mockReturnValue({ limit: limitFn });
      const whereFn = vi.fn().mockReturnValue({ orderBy: orderByFn });
      const fromFn = vi.fn().mockReturnValue({ where: whereFn });
      const selectFn = vi.fn().mockReturnValue({ from: fromFn });

      const db = { select: selectFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      await svc.listByConversation("conv-1", { limit: 10 });
      expect(limitFn).toHaveBeenCalledWith(10);
    });
  });

  describe("attachToMessage", () => {
    it("updates messageId and returns updated row", async () => {
      const mockFile = { id: "file-1", messageId: "msg-1" };
      const returningFn = vi.fn().mockResolvedValue([mockFile]);
      const whereFn = vi.fn().mockReturnValue({ returning: returningFn });
      const setFn = vi.fn().mockReturnValue({ where: whereFn });
      const updateFn = vi.fn().mockReturnValue({ set: setFn });

      const db = { update: updateFn } as unknown as Parameters<typeof chatFileService>[0];
      const svc = chatFileService(db);

      const result = await svc.attachToMessage("file-1", "msg-1");
      expect(result).toEqual(mockFile);
      expect(setFn).toHaveBeenCalledWith(
        expect.objectContaining({ messageId: "msg-1" }),
      );
    });
  });
});
