import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import express from "express";
import request from "supertest";
import { Readable } from "node:stream";
import { MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
import { chatFileRoutes } from "../routes/chat-files.js";
import type { StorageService } from "../storage/types.js";

const { mockCreate, mockGetById, mockListByConversation, mockCreateReference, mockAttachToMessage } = vi.hoisted(
  () => ({
    mockCreate: vi.fn(),
    mockGetById: vi.fn(),
    mockListByConversation: vi.fn(),
    mockCreateReference: vi.fn(),
    mockAttachToMessage: vi.fn(),
  }),
);

const { mockGetConversation } = vi.hoisted(() => ({
  mockGetConversation: vi.fn(),
}));

vi.mock("../services/chat-files.js", () => ({
  chatFileService: vi.fn(() => ({
    create: mockCreate,
    getById: mockGetById,
    listByConversation: mockListByConversation,
    createReference: mockCreateReference,
    attachToMessage: mockAttachToMessage,
  })),
  deriveCategory: (mimeType: string) => {
    if (mimeType.startsWith("image/")) return "image";
    if (mimeType === "application/pdf" || mimeType.startsWith("text/")) return "document";
    return "other";
  },
}));

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

function createStorageService(contentType = "image/png"): StorageService {
  return {
    provider: "local_disk" as const,
    putFile: vi.fn(async (input: { companyId: string; namespace: string; originalFilename: string | null; contentType: string; body: Buffer }) => ({
      provider: "local_disk" as const,
      objectKey: `${input.namespace}/${input.originalFilename ?? "upload"}`,
      contentType: contentType || input.contentType,
      byteSize: input.body.length,
      sha256: "sha256-test",
      originalFilename: input.originalFilename,
    })),
    getObject: vi.fn(),
    headObject: vi.fn(),
    deleteObject: vi.fn(),
  };
}

function createApp(storage: StorageService) {
  const app = express();
  app.use(express.json());
  app.use((req, _res, next) => {
    (req as any).actor = {
      type: "board",
      source: "local_implicit",
      userId: "user-1",
    };
    next();
  });
  app.use("/api", chatFileRoutes({} as any, storage));
  return app;
}

const FILE_ID = "00000000-0000-0000-0000-000000000001";
const CONV_ID = "00000000-0000-0000-0000-000000000002";

function makeFile() {
  return {
    id: FILE_ID,
    companyId: "company-1",
    conversationId: CONV_ID,
    messageId: null,
    filename: "test.png",
    originalFilename: "test.png",
    mimeType: "image/png",
    sizeBytes: 40,
    objectKey: "chat-files/test.png",
    sha256: "sha256-test",
    source: "user_upload",
    category: "image",
    projectId: null,
    createdAt: new Date("2026-01-01T00:00:00.000Z"),
    updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  };
}

describe("chatFileRoutes", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockGetConversation.mockResolvedValue({ id: "conv-1", companyId: "company-1" });
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  describe("POST /api/conversations/:id/files", () => {
    it("uploads a file and returns 201 with file and contentPath", async () => {
      const storage = createStorageService("image/png");
      const app = createApp(storage);
      const mockFile = makeFile();
      mockCreate.mockResolvedValue(mockFile);

      const res = await request(app)
        .post("/api/conversations/conv-1/files")
        .attach("file", Buffer.from("png-data"), { filename: "test.png", contentType: "image/png" });

      expect(res.status).toBe(201);
      expect(res.body.contentPath).toBe(`/api/files/${FILE_ID}/content`);
      expect(res.body.file.id).toBe(FILE_ID);
      expect(mockCreate).toHaveBeenCalledTimes(1);
    });

    it("returns 400 when no file field is provided", async () => {
      const storage = createStorageService();
      const app = createApp(storage);

      const res = await request(app)
        .post("/api/conversations/conv-1/files")
        .field("source", "user_upload");

      expect(res.status).toBe(400);
      expect(res.body.error).toContain("Missing file field");
    });

    it("returns 422 when content type is not allowed", async () => {
      const storage = createStorageService();
      const app = createApp(storage);

      const res = await request(app)
        .post("/api/conversations/conv-1/files")
        .attach("file", Buffer.from("video-data"), { filename: "video.mp4", contentType: "video/mp4" });

      expect(res.status).toBe(422);
      expect(res.body.error).toContain("Unsupported file type");
    });

    it("returns 422 when file exceeds size limit", async () => {
      const storage = createStorageService();
      const app = createApp(storage);

      const oversized = Buffer.alloc(MAX_ATTACHMENT_BYTES + 1);

      const res = await request(app)
        .post("/api/conversations/conv-1/files")
        .attach("file", oversized, { filename: "big.png", contentType: "image/png" });

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

  describe("GET /api/conversations/:id/files", () => {
    it("returns list of files for the conversation", async () => {
      const storage = createStorageService();
      const app = createApp(storage);
      const files = [makeFile()];
      mockListByConversation.mockResolvedValue(files);

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

      expect(res.status).toBe(200);
      expect(res.body.items).toHaveLength(1);
      expect(res.body.items[0].id).toBe(FILE_ID);
    });
  });

  describe("GET /api/files/:fileId/content", () => {
    it("streams file content with correct MIME type", async () => {
      const storage = createStorageService("image/png");
      const app = createApp(storage);
      const mockFile = makeFile();
      mockGetById.mockResolvedValue(mockFile);

      const imageBytes = Buffer.from("image-bytes");
      (storage.getObject as ReturnType<typeof vi.fn>).mockResolvedValue({
        stream: Readable.from([imageBytes]),
        contentType: "image/png",
        contentLength: imageBytes.length,
      });

      const res = await request(app).get(`/api/files/${FILE_ID}/content`);

      expect(res.status).toBe(200);
      expect(res.headers["content-type"]).toContain("image/png");
      expect(res.headers["x-content-type-options"]).toBe("nosniff");
    });

    it("returns 404 when file not found", async () => {
      const storage = createStorageService();
      const app = createApp(storage);
      mockGetById.mockResolvedValue(null);

      const res = await request(app).get("/api/files/nonexistent/content");

      expect(res.status).toBe(404);
      expect(res.body.error).toContain("not found");
    });
  });

  describe("POST /api/files/:fileId/references", () => {
    it("creates a cross-conversation reference and returns 201", async () => {
      const storage = createStorageService();
      const app = createApp(storage);
      const mockFile = makeFile();
      mockGetById.mockResolvedValue(mockFile);

      const reference = {
        id: "00000000-0000-0000-0000-000000000099",
        fileId: FILE_ID,
        conversationId: CONV_ID,
        messageId: null,
        createdAt: new Date("2026-01-01T00:00:00.000Z"),
      };
      mockCreateReference.mockResolvedValue(reference);

      const res = await request(app)
        .post(`/api/files/${FILE_ID}/references`)
        .send({ messageId: "00000000-0000-0000-0000-000000000001" });

      expect(res.status).toBe(201);
      expect(res.body.id).toBe("00000000-0000-0000-0000-000000000099");
    });
  });

  describe("PATCH /api/files/:fileId", () => {
    it("attaches file to a message and returns updated file", async () => {
      const storage = createStorageService();
      const app = createApp(storage);
      const mockFile = makeFile();
      mockGetById.mockResolvedValue(mockFile);

      const updated = { ...mockFile, messageId: "msg-1" };
      mockAttachToMessage.mockResolvedValue(updated);

      const res = await request(app)
        .patch(`/api/files/${FILE_ID}`)
        .send({ messageId: "msg-1" });

      expect(res.status).toBe(200);
      expect(res.body.messageId).toBe("msg-1");
    });

    it("returns 400 when messageId is missing", async () => {
      const storage = createStorageService();
      const app = createApp(storage);
      const mockFile = makeFile();
      mockGetById.mockResolvedValue(mockFile);

      const res = await request(app)
        .patch(`/api/files/${FILE_ID}`)
        .send({});

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