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

import type {
  KeywordInputRow,
  RawCsvRow,
} from "../core/types.ts";
import { resetEnvCacheForTesting } from "../core/env.ts";
import {
  analyzeProductCombined,
  analyzeProductsCombinedBulk,
  analyzeProductForKeyword,
  refreshLlmClient,
} from "./standardization-llm.ts";

const createLlmClientMock = vi.hoisted(() => vi.fn());
const completionsCreateMock = vi.hoisted(() => vi.fn());
const resolveTextPromptMock = vi.hoisted(() => vi.fn());

vi.mock("../internal/db.ts", () => ({
  getActiveLlmConfig: vi.fn(async () => null),
}));

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

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

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

const rawRow: RawCsvRow = {
  发票种类: "增值税专票",
  名称: "不锈钢栏杆",
  规格型号: "H=1200",
  品牌: "",
  数量: "10",
  单位: "米",
  "备注/附件": "室外工程",
  收货地址: "上海",
};

const keywordRow: KeywordInputRow = {
  batch_id: "batch-1",
  material_id: "mat-1",
  invoice_type: "增值税专票",
  name: "不锈钢栏杆",
  spec: "H=1200",
  brand: "",
  quantity: "10",
  unit: "米",
  notes: "室外工程",
  delivery_address: "上海",
  material: "不锈钢",
  usage: "护栏",
  simplified_spec: "H=1200",
  standardized_brand: "",
};

function combinedPayload(overrides: Record<string, unknown> = {}) {
  return JSON.stringify({
    material: "不锈钢",
    usage: "护栏",
    simplified_spec: "H=1200",
    brand_term: "",
    keyword_parameter_terms: ["H=1200"],
    modifier_terms: [],
    alternative_terms: ["不锈钢护栏"],
    reasoning: "可采购",
    suggested_keyword: "不锈钢护栏",
    product_type: "成品",
    structure_summary: "栏杆系统",
    component_roles: "主体",
    purchase_type: "可直接采购",
    outcome: "success",
    can_search_1688: true,
    missing_required_fields: [],
    failure_reason: "",
    human_fix_hint: "",
    required_attributes_summary: "材质和高度明确",
    parameter_interpretations: [],
    ...overrides,
  });
}

function keywordPayload(overrides: Record<string, unknown> = {}) {
  return JSON.stringify({
    reasoning: "信息足够",
    suggested_keyword: "不锈钢护栏",
    product_type: "成品",
    structure_summary: "栏杆系统",
    component_roles: "主体",
    purchase_type: "可直接采购",
    outcome: "success",
    can_search_1688: true,
    missing_required_fields: [],
    failure_reason: "",
    human_fix_hint: "",
    required_attributes_summary: "材质和高度明确",
    parameter_interpretations: [],
    ...overrides,
  });
}

function completion(content: string, finishReason = "stop") {
  return {
    choices: [
      {
        finish_reason: finishReason,
        message: { content },
      },
    ],
  };
}

function setDeepSeekEnv(overrides: NodeJS.ProcessEnv = {}) {
  process.env = {
    ...originalEnv,
    NODE_ENV: "test",
    VITEST: "true",
    SUPABASE_URL: "https://example.supabase.co",
    SUPABASE_SERVICE_ROLE_KEY: "service-role-key",
    DEEPSEEK_LLM_BASE_URL: "https://api.deepseek.com",
    DEEPSEEK_LLM_API_KEY: "test-key",
    LLM_QUEUE_MIN_INTERVAL_MS: "1",
    LLM_RETRY_BASE_DELAY_MS: "1",
    LLM_RETRY_MAX_ATTEMPTS: "2",
    ...overrides,
  };
  resetEnvCacheForTesting();
}

beforeEach(() => {
  vi.clearAllMocks();
  refreshLlmClient();
  setDeepSeekEnv({
    STANDARDIZATION_LLM_MODEL: "deepseek-v4-pro",
    KEYWORD_SEMANTIC_LLM_MODEL: "deepseek-v4-flash",
  });
  createLlmClientMock.mockResolvedValue({
    client: {
      chat: {
        completions: {
          create: completionsCreateMock,
        },
      },
    },
    config: {
      baseURL: "https://api.deepseek.com",
      apiKey: "test-key",
      model: "global-model",
      provider: "deepseek",
    },
  });
  resolveTextPromptMock.mockResolvedValue({ text: "请完成采购物料分析。" });
  completionsCreateMock.mockResolvedValue(completion(combinedPayload()));
});

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

describe("standardization LLM stage model config", () => {
  it("uses STANDARDIZATION_LLM_MODEL for DeepSeek standardization requests and disables thinking by default", async () => {
    await analyzeProductCombined(rawRow);

    expect(createLlmClientMock).toHaveBeenCalledWith({ maxRetries: 0, stage: "standardization" });
    const request = completionsCreateMock.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(request.model).toBe("deepseek-v4-pro");
    expect(request.response_format).toEqual({ type: "json_object" });
    expect(request.max_tokens).toBe(8192);
    expect(request.thinking).toEqual({ type: "disabled" });
    expect(request.messages).toEqual(expect.arrayContaining([
      expect.objectContaining({
        role: "system",
        content: expect.stringMatching(/json/i),
      }),
    ]));
    expect(resolveTextPromptMock).toHaveBeenCalledWith(
      "automation.standardization.combined.system",
      { provider: "deepseek", model: "deepseek-v4-pro" },
    );
  });

  it("uses KEYWORD_SEMANTIC_LLM_MODEL for keyword semantic requests", async () => {
    completionsCreateMock.mockResolvedValue(completion(keywordPayload()));

    await analyzeProductForKeyword(keywordRow);

    expect(createLlmClientMock).toHaveBeenCalledWith({ maxRetries: 0, stage: "keyword_semantic" });
    const request = completionsCreateMock.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(request.model).toBe("deepseek-v4-flash");
    expect(resolveTextPromptMock).toHaveBeenCalledWith(
      "automation.standardization.keyword_semantic.system",
      { provider: "deepseek", model: "deepseek-v4-flash" },
    );
  });

  it("passes DeepSeek thinking and reasoning options when enabled", async () => {
    setDeepSeekEnv({
      STANDARDIZATION_LLM_MODEL: "deepseek-v4-pro",
      STANDARDIZATION_LLM_THINKING: "enabled",
      STANDARDIZATION_LLM_REASONING_EFFORT: "high",
      STANDARDIZATION_LLM_MAX_TOKENS: "12000",
    });

    await analyzeProductCombined(rawRow);

    const request = completionsCreateMock.mock.calls[0]?.[0] as Record<string, unknown>;
    expect(request.max_tokens).toBe(12000);
    expect(request.thinking).toEqual({ type: "enabled" });
    expect(request.reasoning_effort).toBe("high");
  });

  it("retries empty LLM content before parsing the standardization result", async () => {
    completionsCreateMock
      .mockResolvedValueOnce(completion(""))
      .mockResolvedValueOnce(completion(combinedPayload({ suggested_keyword: "二次成功" })));

    const result = await analyzeProductCombined(rawRow);

    expect(completionsCreateMock).toHaveBeenCalledTimes(2);
    expect(result.suggested_keyword).toBe("二次成功");
  });

  it("throws a clear max token hint when DeepSeek truncates the response", async () => {
    completionsCreateMock.mockResolvedValue(completion(combinedPayload(), "length"));

    await expect(analyzeProductCombined(rawRow)).rejects.toThrow(/STANDARDIZATION_LLM_MAX_TOKENS/);
  });

  it("runs bulk standardization on MiMo and falls back failed chunks to DeepSeek", async () => {
    setDeepSeekEnv({
      MIMO_LLM_BASE_URL: "https://mimo.example.test/v1",
      MIMO_LLM_API_KEY: "mimo-key",
      DEEPSEEK_LLM_BASE_URL: "https://deepseek.example.test",
      DEEPSEEK_LLM_API_KEY: "deepseek-key",
      STANDARDIZATION_BULK_LLM_MODEL: "mimo-v2.5",
      STANDARDIZATION_BULK_LLM_CHUNK_SIZE: "2",
      STANDARDIZATION_BULK_LLM_CONCURRENCY: "1",
      STANDARDIZATION_BULK_LLM_FALLBACK_MODEL: "deepseek-v4-pro",
    });
    const originalFetch = globalThis.fetch;
    const calls: Array<{ url: string; body: Record<string, unknown>; headers: Headers }> = [];
    globalThis.fetch = vi.fn(async (url, init) => {
      const body = JSON.parse(String(init?.body ?? "{}"));
      calls.push({
        url: String(url),
        body,
        headers: new Headers(init?.headers),
      });
      if (String(url).includes("mimo.example.test")) {
        return new Response(JSON.stringify({ error: { message: "rate limit" } }), { status: 429 });
      }
      return new Response(JSON.stringify(completion(JSON.stringify({
        items: [
          { material_id: "mat-1", ...JSON.parse(combinedPayload({ suggested_keyword: "fallback-1" })) },
          { material_id: "mat-2", ...JSON.parse(combinedPayload({ suggested_keyword: "fallback-2" })) },
        ],
      }))), { status: 200, headers: { "Content-Type": "application/json" } });
    }) as typeof fetch;

    try {
      const result = await analyzeProductsCombinedBulk([
        { materialId: "mat-1", row: rawRow },
        { materialId: "mat-2", row: { ...rawRow, 名称: "铝合金栏杆" } },
      ]);

      expect(calls[0]?.url).toBe("https://mimo.example.test/v1/chat/completions");
      expect(calls[0]?.headers.get("api-key")).toBe("mimo-key");
      expect(calls[1]?.url).toBe("https://mimo.example.test/v1/chat/completions");
      expect(calls[2]?.url).toBe("https://deepseek.example.test/chat/completions");
      expect(calls[2]?.headers.get("authorization")).toBe("Bearer deepseek-key");
      expect(calls[2]?.body.model).toBe("deepseek-v4-pro");
      expect(result.results.get("mat-1")?.suggested_keyword).toBe("fallback-1");
      expect(result.modelByMaterialId.get("mat-1")).toBe("deepseek-v4-pro");
      expect(result.providerByMaterialId.get("mat-1")).toBe("deepseek");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("runs even a single standardization row through MiMo bulk with DeepSeek configured as fallback", async () => {
    setDeepSeekEnv({
      MIMO_LLM_BASE_URL: "https://mimo.example.test/v1",
      MIMO_LLM_API_KEY: "mimo-key",
      DEEPSEEK_LLM_BASE_URL: "https://deepseek.example.test",
      DEEPSEEK_LLM_API_KEY: "deepseek-key",
      STANDARDIZATION_BULK_LLM_MODEL: "mimo-v2.5",
      STANDARDIZATION_BULK_LLM_FALLBACK_MODEL: "deepseek-v4-pro",
    });
    const originalFetch = globalThis.fetch;
    const calls: Array<{ url: string; body: Record<string, unknown>; headers: Headers }> = [];
    globalThis.fetch = vi.fn(async (url, init) => {
      const body = JSON.parse(String(init?.body ?? "{}"));
      calls.push({
        url: String(url),
        body,
        headers: new Headers(init?.headers),
      });
      return new Response(JSON.stringify(completion(JSON.stringify({
        items: [
          { material_id: "mat-1", ...JSON.parse(combinedPayload({ suggested_keyword: "single-mimo" })) },
        ],
      }))), { status: 200, headers: { "Content-Type": "application/json" } });
    }) as typeof fetch;

    try {
      const result = await analyzeProductsCombinedBulk([
        { materialId: "mat-1", row: rawRow },
      ]);

      expect(calls).toHaveLength(1);
      expect(calls[0]?.url).toBe("https://mimo.example.test/v1/chat/completions");
      expect(calls[0]?.headers.get("api-key")).toBe("mimo-key");
      expect(calls[0]?.body.model).toBe("mimo-v2.5");
      expect(result.results.get("mat-1")?.suggested_keyword).toBe("single-mimo");
      expect(result.modelByMaterialId.get("mat-1")).toBe("mimo-v2.5");
      expect(result.providerByMaterialId.get("mat-1")).toBe("mimo");
    } finally {
      globalThis.fetch = originalFetch;
    }
  });

  it("requires DeepSeek fallback configuration before starting bulk standardization", async () => {
    setDeepSeekEnv({
      MIMO_LLM_BASE_URL: "https://mimo.example.test/v1",
      MIMO_LLM_API_KEY: "mimo-key",
      DEEPSEEK_LLM_BASE_URL: "",
      DEEPSEEK_LLM_API_KEY: "",
    });

    await expect(analyzeProductsCombinedBulk([
      { materialId: "mat-1", row: rawRow },
    ])).rejects.toThrow(/DeepSeek 兜底 LLM 配置/);
  });
});
