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

const pdfParseMock = vi.hoisted(() => {
  const getText = vi.fn();
  const destroy = vi.fn();
  const PDFParse = vi.fn(() => ({ getText, destroy }));
  return { PDFParse, getText, destroy };
});

vi.mock("pdf-parse", () => ({
  PDFParse: pdfParseMock.PDFParse,
}));

import {
  extractMaterialsFromOriginalFile,
  resolveDocumentExtractionModel,
  resolveDocumentOcrModel,
} from "./document-extraction-llm.ts";
import { createLlmClient } from "../core/llm-client.ts";
import { resetEnvCacheForTesting } from "../core/env.ts";
import { resolveTextPrompt } from "../internal/prompt-runtime.js";

vi.mock("../core/llm-client.ts", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../core/llm-client.ts")>();
  return {
    ...actual,
    createLlmClient: vi.fn(),
  };
});

vi.mock("../internal/prompt-runtime.js", () => ({
  resolveTextPrompt: vi.fn(),
}));

const createMock = vi.mocked(createLlmClient);
const resolveTextPromptMock = vi.mocked(resolveTextPrompt);
const originalEnv = { ...process.env };

function makeMockRuntime(provider: string, model: string, content: string) {
  const completionsCreate = vi.fn().mockResolvedValue({
    choices: [{ message: { content } }],
  });
  return {
    client: {
      chat: {
        completions: {
          create: completionsCreate,
        },
      },
    },
    config: {
      baseURL: provider === "deepseek" ? "https://api.deepseek.com" : `https://${provider}.example.test/v1`,
      apiKey: "redacted",
      model,
      provider,
    },
  } as unknown as Awaited<ReturnType<typeof createLlmClient>>;
}

function mockGlmResponse(content: string) {
  const runtime = makeMockRuntime("glm", "some-runtime-model", content);
  createMock.mockResolvedValue(runtime);
  const completionsCreate = runtime.client.chat.completions.create as ReturnType<typeof vi.fn>;
  return completionsCreate;
}

const validPayload = JSON.stringify({
  items: [
    {
      material_name: "球墨铸铁井盖",
      specification: "DN700",
      quantity: "2 套",
      unit: "套",
    },
  ],
  extraction_summary: "提取 1 项",
  warnings: ["品牌未指定"],
});

describe("extractMaterialsFromOriginalFile", () => {
  beforeEach(() => {
    createMock.mockReset();
    resolveTextPromptMock.mockReset();
    pdfParseMock.PDFParse.mockClear();
    pdfParseMock.getText.mockReset();
    pdfParseMock.destroy.mockReset();
    pdfParseMock.getText.mockResolvedValue({ text: "PDF 文本：球墨铸铁井盖 DN700 数量 2 套" });
    pdfParseMock.destroy.mockResolvedValue(undefined);
    process.env = {
      ...originalEnv,
      NODE_ENV: "test",
      VITEST: "true",
      SUPABASE_URL: "https://example.supabase.co",
      SUPABASE_SERVICE_ROLE_KEY: "service-role-key",
      LLM_MODEL: "global-runtime-model",
      DOCUMENT_OCR_LLM_MODEL: "ocr-stage-model",
      DOCUMENT_EXTRACTION_LLM_MODEL: "extraction-stage-model",
    };
    resetEnvCacheForTesting();
    resolveTextPromptMock.mockResolvedValue({ text: "Return fixed JSON only." } as Awaited<ReturnType<typeof resolveTextPrompt>>);
  });

  afterEach(() => {
    process.env = { ...originalEnv };
    resetEnvCacheForTesting();
  });

  it("resolves OCR and document extraction stages independently", () => {
    expect(resolveDocumentOcrModel()).toBe("ocr-stage-model");
    expect(resolveDocumentExtractionModel()).toBe("extraction-stage-model");

    process.env.DOCUMENT_OCR_LLM_MODEL = "";
    process.env.DOCUMENT_EXTRACTION_LLM_MODEL = "";
    resetEnvCacheForTesting();

    expect(resolveDocumentOcrModel()).toBe("global-runtime-model");
    expect(resolveDocumentExtractionModel()).toBe("global-runtime-model");

    process.env.LLM_MODEL = "";
    resetEnvCacheForTesting();

    expect(resolveDocumentOcrModel()).toBe("mimo-v2.5");
    expect(resolveDocumentExtractionModel()).toBe("mimo-v2.5");
  });

  it("runs image files through document OCR before extracting fixed JSON from OCR text", async () => {
    const ocrRuntime = makeMockRuntime("glm", "ocr-runtime-model", "材料名称：球墨铸铁井盖\n规格：DN700\n数量：2套");
    const extractionRuntime = makeMockRuntime("glm", "extraction-runtime-model", validPayload);
    createMock
      .mockResolvedValueOnce(ocrRuntime)
      .mockResolvedValueOnce(extractionRuntime);
    const ocrCreate = ocrRuntime.client.chat.completions.create as ReturnType<typeof vi.fn>;
    const extractionCreate = extractionRuntime.client.chat.completions.create as ReturnType<typeof vi.fn>;

    const result = await extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("image bytes"),
      mimeType: "image/png",
      batchId: "batch-001",
      sourceFileName: "清单.png",
    });

    expect(result.items).toHaveLength(1);
    expect(result.summary).toBe("提取 1 项");
    expect(result.model).toBe("extraction-stage-model");
    expect(createMock).toHaveBeenNthCalledWith(1, { maxRetries: 2, stage: "document_ocr" });
    expect(createMock).toHaveBeenNthCalledWith(2, { maxRetries: 2, stage: "document_extraction" });
    expect(ocrCreate).toHaveBeenCalledWith(expect.objectContaining({
      model: "ocr-stage-model",
      temperature: 0.1,
    }));
    expect(extractionCreate).toHaveBeenCalledWith(expect.objectContaining({
      model: "extraction-stage-model",
      temperature: 0.1,
      response_format: { type: "json_object" },
    }));
    expect(resolveTextPromptMock).toHaveBeenCalledWith(
      "automation.document_extraction.materials.system",
      { provider: "glm", model: "extraction-stage-model" },
    );
    const ocrRequest = ocrCreate.mock.calls[0]?.[0] as {
      messages: Array<{ role: string; content: unknown }>;
    };
    const ocrUserContent = ocrRequest.messages.find((message) => message.role === "user")?.content as Array<Record<string, unknown>>;
    expect(ocrUserContent).toEqual(expect.arrayContaining([
      expect.objectContaining({
        type: "image_url",
        image_url: expect.objectContaining({
          url: "data:image/png;base64,aW1hZ2UgYnl0ZXM=",
        }),
      }),
    ]));
    const extractionRequest = extractionCreate.mock.calls[0]?.[0] as {
      messages: Array<{ role: string; content: unknown }>;
    };
    const extractionUserContent = extractionRequest.messages.find((message) => message.role === "user")?.content;
    expect(extractionUserContent).toContain("OCR 文本");
    expect(extractionUserContent).toContain("材料名称：球墨铸铁井盖");
  });

  it("uses MiMo OCR request options when the document OCR stage is routed to Xiaomi", async () => {
    process.env.DOCUMENT_OCR_LLM_MODEL = "mimo-v2.5";
    resetEnvCacheForTesting();
    const ocrRuntime = makeMockRuntime("mimo", "mimo-v2.5", "材料名称：塑料保护罩\n规格：200x150x100mm\n数量：500个");
    const extractionRuntime = makeMockRuntime("glm", "extraction-runtime-model", validPayload);
    createMock
      .mockResolvedValueOnce(ocrRuntime)
      .mockResolvedValueOnce(extractionRuntime);
    const ocrCreate = ocrRuntime.client.chat.completions.create as ReturnType<typeof vi.fn>;

    await extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("image bytes"),
      mimeType: "image/jpeg",
      batchId: "batch-001",
      sourceFileName: "清单.jpg",
    });

    expect(ocrCreate).toHaveBeenCalledWith(expect.objectContaining({
      model: "mimo-v2.5",
      temperature: 0.1,
      max_completion_tokens: 2048,
    }));
    const request = ocrCreate.mock.calls[0]?.[0] as {
      messages: Array<{ role: string; content: unknown }>;
    };
    const userContent = request.messages.find((message) => message.role === "user")?.content as Array<Record<string, unknown>>;
    expect(userContent).toEqual(expect.arrayContaining([
      expect.objectContaining({
        type: "image_url",
        image_url: expect.objectContaining({
          url: "data:image/jpeg;base64,aW1hZ2UgYnl0ZXM=",
        }),
      }),
    ]));
  });

  it("fails before sending image OCR requests to the official DeepSeek text-only API", async () => {
    const deepseekRuntime = makeMockRuntime("deepseek", "deepseek-v4-pro", "");
    createMock.mockResolvedValue(deepseekRuntime);
    const completionsCreate = deepseekRuntime.client.chat.completions.create as ReturnType<typeof vi.fn>;

    await expect(extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("image bytes"),
      mimeType: "image/png",
      batchId: "batch-001",
      sourceFileName: "清单.png",
    })).rejects.toThrow(/DOCUMENT_OCR_LLM_PROVIDER.*deepseek.*API 不支持图片输入/);

    expect(createMock).toHaveBeenCalledWith({ maxRetries: 2, stage: "document_ocr" });
    expect(createMock).toHaveBeenCalledTimes(1);
    expect(completionsCreate).not.toHaveBeenCalled();
  });

  it("does not run document extraction when image OCR returns empty text", async () => {
    const ocrRuntime = makeMockRuntime("glm", "ocr-runtime-model", " ");
    createMock.mockResolvedValue(ocrRuntime);

    await expect(extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("image bytes"),
      mimeType: "image/png",
      batchId: "batch-001",
      sourceFileName: "清单.png",
    })).rejects.toThrow(/文档 OCR 返回内容为空/);

    expect(createMock).toHaveBeenCalledWith({ maxRetries: 2, stage: "document_ocr" });
    expect(createMock).toHaveBeenCalledTimes(1);
  });

  it("extracts PDF text locally for GLM before sending OCR text to document extraction", async () => {
    const completionsCreate = mockGlmResponse(validPayload);

    await extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("pdf bytes"),
      mimeType: "application/pdf",
      batchId: "batch-001",
      sourceFileName: "清单.pdf",
    });

    expect(pdfParseMock.PDFParse).toHaveBeenCalledWith({ data: Buffer.from("pdf bytes") });
    expect(pdfParseMock.getText).toHaveBeenCalled();
    expect(pdfParseMock.destroy).toHaveBeenCalled();

    const request = completionsCreate.mock.calls[0]?.[0] as {
      messages: Array<{ role: string; content: unknown }>;
    };
    const userContent = request.messages.find((message) => message.role === "user")?.content;
    expect(userContent).toContain("OCR 文本");
    expect(userContent).toContain("PDF 文本：球墨铸铁井盖 DN700 数量 2 套");
    expect(Array.isArray(userContent)).toBe(false);
  });

  it.each([
    ["Word", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "清单.docx"],
  ])("sends %s documents as original file attachments", async (_label, mimeType, sourceFileName) => {
    const completionsCreate = mockGlmResponse(validPayload);

    await extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("document bytes"),
      mimeType,
      batchId: "batch-001",
      sourceFileName,
    });

    const request = completionsCreate.mock.calls[0]?.[0] as {
      messages: Array<{ role: string; content: unknown }>;
    };
    const userContent = request.messages.find((message) => message.role === "user")?.content as Array<Record<string, unknown>>;
    expect(userContent).toEqual(expect.arrayContaining([
      expect.objectContaining({
        type: "file",
        file: expect.objectContaining({
          filename: sourceFileName,
          file_data: `data:${mimeType};base64,${Buffer.from("document bytes").toString("base64")}`,
        }),
      }),
    ]));
  });

  it("throws before returning materials when GLM returns invalid JSON", async () => {
    mockGlmResponse("not json");

    await expect(extractMaterialsFromOriginalFile({
      fileBuffer: Buffer.from("pdf bytes"),
      mimeType: "application/pdf",
      batchId: "batch-001",
      sourceFileName: "清单.pdf",
    })).rejects.toThrow(/文档提取模型返回内容不是合法 JSON/);
  });
});
