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

import {
  resolveBigModelBatchModel,
} from "../standardization/bigmodel-batch-client.ts";
import {
  buildFixedSearchBatchCustomId,
  buildFixedSearchBatchRequestLines,
  buildFixedSearchBatchUserPayload,
  judgeCandidatesWithLlm,
  limitCandidatesForLlm,
  resolveFixedSearchLlmCandidateLimit,
  resolveFixedSearchLlmInputTokenBudget,
  type FixedSearchLlmContext,
} from "./fixed-search-llm.ts";

import type { ScoredSearchCandidate } from "./fixed-search-runtime.ts";

const originalEnv = { ...process.env };

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

function buildCandidate(index: number, score = 100 - index): ScoredSearchCandidate {
  return {
    input: "电磁阀 4V-310-1O",
    "title-text": `气动电磁阀 4V310 候选商品 ${index}`,
    url: `https://detail.1688.com/offer/${index}.html`,
    score,
    search_round: 1,
    round_label: "主搜索词",
    source_product: {
      title: `气动电磁阀 4V310 候选商品 ${index}`,
      offer_url: `https://detail.1688.com/offer/${index}.html`,
      merchant_name: `商家 ${index}`,
      merchant_url: "",
      image_url: "",
      wangwang_url: "",
      contact_page_url: "",
      offer_screenshot_path: "",
    },
  };
}

function buildBatchClient(outputContent: string, status = "completed") {
  return {
    uploadBatchFile: vi.fn(async () => ({ id: "file-input" })),
    createBatchTask: vi.fn(async () => ({ id: "batch-1", status: "in_progress" })),
    retrieveBatchTask: vi.fn(async () => ({
      id: "batch-1",
      status,
      output_file_id: status === "completed" ? "file-output" : "",
      error_file_id: status === "completed" ? "" : "file-error",
      total: 1,
      completed: status === "completed" ? 1 : 0,
      failed: status === "completed" ? 0 : 1,
    })),
    downloadBatchFileContent: vi.fn(async (fileId: string) => (
      fileId === "file-error" ? "{\"error\":\"batch failed\"}" : outputContent
    )),
  };
}

const context: FixedSearchLlmContext = {
  primary_search_term: "电磁阀 4V-310-1O",
  standardized_name: "电磁阀",
  brand: "",
  standardized_spec: "4V-310-1O",
  category: "气动控制",
  ai_reasoning: "原始需求为电磁阀，规格型号为4V-310-1O。4V系列是气动行业常用的电磁换向阀型号。",
  product_type: "独立零部件",
};

describe("limitCandidatesForLlm", () => {
  it("keeps only the configured leading candidates", () => {
    const candidates = Array.from({ length: 8 }, (_, index) => buildCandidate(index + 1));

    expect(limitCandidatesForLlm(candidates, 5).map((candidate) => candidate.url)).toEqual(
      candidates.slice(0, 5).map((candidate) => candidate.url),
    );
  });

  it("keeps all candidates when the pool is below the limit", () => {
    const candidates = Array.from({ length: 3 }, (_, index) => buildCandidate(index + 1));

    expect(limitCandidatesForLlm(candidates, 500)).toHaveLength(3);
  });

  it("also respects the estimated input token budget", () => {
    const candidates = Array.from({ length: 8 }, (_, index) => buildCandidate(index + 1));

    expect(limitCandidatesForLlm(candidates, 500, 80).length).toBeLessThan(8);
  });
});

describe("resolveFixedSearchLlmCandidateLimit", () => {
  it("defaults to 500 and accepts a positive override", () => {
    expect(resolveFixedSearchLlmCandidateLimit("")).toBe(500);
    expect(resolveFixedSearchLlmCandidateLimit("300")).toBe(300);
  });
});

describe("resolveFixedSearchLlmInputTokenBudget", () => {
  it("defaults to a generous budget and accepts a positive override", () => {
    expect(resolveFixedSearchLlmInputTokenBudget("")).toBe(100_000);
    expect(resolveFixedSearchLlmInputTokenBudget("50000")).toBe(50_000);
  });
});

describe("fixed search batch request", () => {
  it("only puts ai_reasoning and one candidate in the user payload", () => {
    const payload = buildFixedSearchBatchUserPayload(context, buildCandidate(1));

    expect(Object.keys(payload).sort()).toEqual(["ai_reasoning", "candidate"]);
    expect(Object.keys(payload.candidate).sort()).toEqual(["input", "score", "title-text", "url"]);
  });

  it("builds glm-4-plus jsonl-compatible request lines", () => {
    const lines = buildFixedSearchBatchRequestLines({
      taskId: "CG-20260420-001-ss-006",
      materialId: "material-1",
      context,
      candidates: [buildCandidate(1)],
    });

    expect(lines).toHaveLength(1);
    expect(lines[0]?.custom_id).toMatch(/^fm_[A-Za-z0-9_-]{12}_[A-Za-z0-9_-]{12}_0$/);
    expect((lines[0]?.custom_id ?? "").length).toBeLessThanOrEqual(64);
    expect(lines[0]?.custom_id).not.toContain(":");
    expect(lines[0]?.url).toBe("/v4/chat/completions");
    expect(lines[0]?.body.model).toBe("glm-4-plus");
    const userPayload = JSON.parse(lines[0]?.body.messages[1]?.content ?? "{}");
    expect(Object.keys(userPayload).sort()).toEqual(["ai_reasoning", "candidate"]);
  });

  it("uses FIXED_SEARCH_LLM_MODEL for jsonl request lines when configured", () => {
    process.env.FIXED_SEARCH_LLM_MODEL = "glm-4-air";

    const lines = buildFixedSearchBatchRequestLines({
      taskId: "task-1",
      materialId: "material-1",
      context,
      candidates: [buildCandidate(1), buildCandidate(2)],
    });

    expect(resolveBigModelBatchModel()).toBe("glm-4-air");
    expect(lines.map((line) => line.body.model)).toEqual(["glm-4-air", "glm-4-air"]);
  });

  it("rejects unsupported FIXED_SEARCH_LLM_MODEL values before submitting batch requests", () => {
    process.env.FIXED_SEARCH_LLM_MODEL = "not-a-bigmodel-batch-model";

    expect(() => resolveBigModelBatchModel()).toThrow(/FIXED_SEARCH_LLM_MODEL.*not-a-bigmodel-batch-model.*不受支持/);
  });
});

describe("judgeCandidatesWithLlm", () => {
  it("does not send score=0 candidates to batch and keeps them in audit results", async () => {
    const batchCustomId = buildFixedSearchBatchCustomId({
      taskId: "task-1",
      materialId: "material-1",
      candidateIndex: 0,
    });
    const client = buildBatchClient(JSON.stringify({
      custom_id: batchCustomId,
      response: {
        status_code: 200,
        body: {
          choices: [{ message: { content: "{\"is_match\":true,\"reason\":\"型号匹配\"}" } }],
        },
      },
    }));

    const results = await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88), buildCandidate(2, 0)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    const uploadedContent = client.uploadBatchFile.mock.calls[0]?.[0].content ?? "";
    expect(uploadedContent.split("\n")).toHaveLength(1);
    expect(results.map((result) => result.judge_status)).toEqual(["judged", "skipped_zero_score"]);
    expect(results[1]?.is_match).toBe(false);
  });

  it("stores the resolved env model in batch metadata", async () => {
    process.env.FIXED_SEARCH_LLM_MODEL = "glm-4-air";
    const batchCustomId = buildFixedSearchBatchCustomId({
      taskId: "task-1",
      materialId: "material-1",
      candidateIndex: 0,
    });
    const client = buildBatchClient(JSON.stringify({
      custom_id: batchCustomId,
      response: {
        status_code: 200,
        body: {
          choices: [{ message: { content: "{\"is_match\":true,\"reason\":\"型号匹配\"}" } }],
        },
      },
    }));

    await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    expect(client.createBatchTask.mock.calls[0]?.[0].metadata?.model).toBe("glm-4-air");
  });

  it("restores output order by custom_id", async () => {
    const firstId = buildFixedSearchBatchCustomId({ taskId: "task-1", materialId: "material-1", candidateIndex: 0 });
    const secondId = buildFixedSearchBatchCustomId({ taskId: "task-1", materialId: "material-1", candidateIndex: 1 });
    const client = buildBatchClient([
      JSON.stringify({
        custom_id: secondId,
        response: { status_code: 200, body: { choices: [{ message: { content: "{\"is_match\":false,\"reason\":\"不匹配\"}" } }] } },
      }),
      JSON.stringify({
        custom_id: firstId,
        response: { status_code: 200, body: { choices: [{ message: { content: "{\"is_match\":true,\"reason\":\"匹配\"}" } }] } },
      }),
    ].join("\n"));

    const results = await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88), buildCandidate(2, 87)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    expect(results.map((result) => result.is_match)).toEqual([true, false]);
  });

  it("marks line-level errors as llm_error with null match", async () => {
    const customId = buildFixedSearchBatchCustomId({ taskId: "task-1", materialId: "material-1", candidateIndex: 0 });
    const client = buildBatchClient(JSON.stringify({
      custom_id: customId,
      response: { status_code: 429, body: { error: { message: "rate limit" } } },
    }));

    const results = await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    expect(results[0]).toMatchObject({
      judge_status: "llm_error",
      is_match: null,
      reason: "rate limit",
    });
  });

  it("marks whole-batch failure as llm_error", async () => {
    process.env.FIXED_SEARCH_LLM_MODEL = "glm-4-air";
    const client = buildBatchClient("", "failed");

    const results = await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    expect(results[0]?.judge_status).toBe("llm_error");
    expect(results[0]?.is_match).toBeNull();
    expect(results[0]?.reason).toContain("glm-4-air 离线 Batch 商品匹配失败");
    expect(results[0]?.reason).not.toContain("glm-4-plus 离线 Batch 商品匹配失败");
  });

  it("marks upload failures as llm_error instead of throwing", async () => {
    const client = {
      ...buildBatchClient(""),
      uploadBatchFile: vi.fn(async () => {
        throw new Error("upload rejected");
      }),
    };

    const results = await judgeCandidatesWithLlm({
      context,
      taskId: "task-1",
      materialId: "material-1",
      candidates: [buildCandidate(1, 88)],
      batchClient: client,
      batchPollIntervalMs: 1,
    });

    expect(results[0]).toMatchObject({
      judge_status: "llm_error",
      is_match: null,
    });
    expect(results[0]?.reason).toContain("upload rejected");
    expect(client.createBatchTask).not.toHaveBeenCalled();
  });
});
