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

import {
  loadEnv,
  resetEnvCacheForTesting,
  type AutomationEnv,
} from "./env.ts";
import {
  resolveLlmProvider,
  resolveStageLlmModel,
} from "./llm-client.ts";

const minimalValidEnv: NodeJS.ProcessEnv = {
  SUPABASE_URL: "https://example.supabase.co",
  SUPABASE_SERVICE_ROLE_KEY: "service-role-key",
};

afterEach(() => {
  resetEnvCacheForTesting();
});

describe("loadEnv", () => {
  it("accepts a minimal valid env and applies defaults", () => {
    const env = loadEnv(minimalValidEnv);

    expect(env.supabaseUrl).toBe("https://example.supabase.co");
    expect(env.supabaseKey).toBe("service-role-key");
    expect(env.automationPort).toBe(3030);
    expect(env.standardizeConcurrency).toBe(1);
    expect(env.llmQueueMinIntervalMs).toBe(3000);
    expect(env.llmRetryMaxAttempts).toBe(4);
    expect(env.llmRetryBaseDelayMs).toBe(1500);
    expect(env.promptEnv).toBe("prod");
    expect(env.promptCacheTtlMs).toBe(300_000);
    expect(env.llmBaseUrl).toBeUndefined();
    expect(env.llmApiKey).toBeUndefined();
    expect(env.llmProviderCredentials.deepseek).toBeUndefined();
    expect(env.llmProviderCredentials.glm).toBeUndefined();
    expect(env.llmStageProviders.standardization).toBeUndefined();
    expect(env.llmStageModels.document_ocr).toBeUndefined();
    expect(env.llmStageModels.document_extraction).toBeUndefined();
    expect(env.llmStageModels.standardization).toBeUndefined();
    expect(env.llmStageModels.keyword_semantic).toBeUndefined();
    expect(env.llmStageModels.search_vision).toBeUndefined();
    expect(env.llmStageModels.fixed_search_batch).toBeUndefined();
    expect(env.llmStageModels.chatbot_intent).toBeUndefined();
  });

  it("falls back to VITE_SUPABASE_URL when SUPABASE_URL is missing", () => {
    const env = loadEnv({
      VITE_SUPABASE_URL: "https://vite.supabase.co",
      SUPABASE_SERVICE_ROLE_KEY: "service-role-key",
    });
    expect(env.supabaseUrl).toBe("https://vite.supabase.co");
  });

  it("treats empty strings as missing for optional vars", () => {
    const env = loadEnv({
      ...minimalValidEnv,
      LLM_BASE_URL: "  ",
      LLM_API_KEY: "",
      DEEPSEEK_LLM_BASE_URL: "  ",
      DEEPSEEK_LLM_API_KEY: "",
      STANDARDIZATION_LLM_PROVIDER: " ",
      DOCUMENT_OCR_LLM_MODEL: "  ",
      DOCUMENT_EXTRACTION_LLM_MODEL: "",
      STANDARDIZATION_LLM_MODEL: "  ",
      KEYWORD_SEMANTIC_LLM_MODEL: "",
      SEARCH_VISION_LLM_MODEL: "  ",
      FIXED_SEARCH_LLM_MODEL: "",
      CHATBOT_INTENT_LLM_MODEL: "  ",
    });
    expect(env.llmBaseUrl).toBeUndefined();
    expect(env.llmApiKey).toBeUndefined();
    expect(env.llmProviderCredentials.deepseek).toBeUndefined();
    expect(env.llmStageProviders.standardization).toBeUndefined();
    expect(env.llmStageModels.document_ocr).toBeUndefined();
    expect(env.llmStageModels.document_extraction).toBeUndefined();
    expect(env.llmStageModels.standardization).toBeUndefined();
    expect(env.llmStageModels.keyword_semantic).toBeUndefined();
    expect(env.llmStageModels.search_vision).toBeUndefined();
    expect(env.llmStageModels.fixed_search_batch).toBeUndefined();
    expect(env.llmStageModels.chatbot_intent).toBeUndefined();
  });

  it("parses provider credentials and stage provider routes", () => {
    const env = loadEnv({
      ...minimalValidEnv,
      DEEPSEEK_LLM_BASE_URL: " https://api.deepseek.com ",
      DEEPSEEK_LLM_API_KEY: " deepseek-key ",
      GLM_LLM_BASE_URL: "https://open.bigmodel.cn/api/paas/v4",
      GLM_LLM_API_KEY: "glm-key",
      MIMO_LLM_BASE_URL: "https://api.xiaomimimo.com/v1",
      MIMO_LLM_API_KEY: "mimo-key",
      DOCUMENT_OCR_LLM_PROVIDER: "mimo",
      STANDARDIZATION_LLM_PROVIDER: " deepseek ",
      KEYWORD_SEMANTIC_LLM_PROVIDER: "deepseek",
      FIXED_SEARCH_LLM_PROVIDER: "glm",
    });

    expect(env.llmProviderCredentials.deepseek).toEqual({
      baseURL: "https://api.deepseek.com",
      apiKey: "deepseek-key",
    });
    expect(env.llmProviderCredentials.glm).toEqual({
      baseURL: "https://open.bigmodel.cn/api/paas/v4",
      apiKey: "glm-key",
    });
    expect(env.llmProviderCredentials.mimo).toEqual({
      baseURL: "https://api.xiaomimimo.com/v1",
      apiKey: "mimo-key",
    });
    expect(env.llmStageProviders.document_ocr).toBe("mimo");
    expect(env.llmStageProviders.standardization).toBe("deepseek");
    expect(env.llmStageProviders.keyword_semantic).toBe("deepseek");
    expect(env.llmStageProviders.fixed_search_batch).toBe("glm");
    expect(env.llmStageProviders.document_extraction).toBeUndefined();
  });

  it("resolves stage models from stage env, fallback, global env, and code defaults", () => {
    const env = loadEnv({
      ...minimalValidEnv,
      LLM_MODEL: "global-model",
      STANDARDIZATION_LLM_MODEL: "stage-standardization-model",
    });

    expect(resolveStageLlmModel("standardization", { env })).toBe("stage-standardization-model");
    expect(resolveStageLlmModel("document_extraction", { env, fallbackModel: "callsite-model" })).toBe("callsite-model");
    expect(resolveStageLlmModel("document_ocr", { env })).toBe("global-model");

    const envWithoutGlobal = {
      ...env,
      llmModel: undefined,
      llmStageModels: {
        ...env.llmStageModels,
        standardization: undefined,
      },
    } satisfies AutomationEnv;
    expect(resolveStageLlmModel("standardization", { env: envWithoutGlobal })).toBe("deepseek-v4-pro");
  });

  it("keeps SEARCH_VISION_MODEL as a lower-priority legacy alias", () => {
    const aliasOnly = loadEnv({
      ...minimalValidEnv,
      SEARCH_VISION_MODEL: "legacy-vision-model",
    });
    expect(aliasOnly.llmStageModels.search_vision).toBe("legacy-vision-model");
    expect(resolveStageLlmModel("search_vision", { env: aliasOnly })).toBe("legacy-vision-model");

    resetEnvCacheForTesting();
    const explicit = loadEnv({
      ...minimalValidEnv,
      SEARCH_VISION_MODEL: "legacy-vision-model",
      SEARCH_VISION_LLM_MODEL: "stage-vision-model",
    });
    expect(explicit.llmStageModels.search_vision).toBe("stage-vision-model");
  });

  it("identifies DeepSeek provider from api.deepseek.com base URL", () => {
    expect(resolveLlmProvider({
      provider: "env",
      baseURL: "https://api.deepseek.com",
      model: "deepseek-chat",
    })).toBe("deepseek");
  });

  it("parses numeric env vars and rejects non-positive ports", () => {
    expect(() =>
      loadEnv({ ...minimalValidEnv, AUTOMATION_PORT: "0" }),
    ).toThrow(/automationPort/);
    expect(() =>
      loadEnv({ ...minimalValidEnv, AUTOMATION_PORT: "not-a-number" }),
    ).toThrow(/automationPort/);
  });

  it("falls back to default when numeric env var is empty", () => {
    const env = loadEnv({
      ...minimalValidEnv,
      AUTOMATION_PORT: "  ",
      LLM_RETRY_MAX_ATTEMPTS: "",
    });
    expect(env.automationPort).toBe(3030);
    expect(env.llmRetryMaxAttempts).toBe(4);
  });

  it("rejects negative llmQueueMinIntervalMs", () => {
    expect(() =>
      loadEnv({ ...minimalValidEnv, LLM_QUEUE_MIN_INTERVAL_MS: "-1" }),
    ).toThrow(/llmQueueMinIntervalMs/);
  });

  it("throws a helpful error when Supabase config is missing", () => {
    expect(() => loadEnv({})).toThrow(/SUPABASE_URL/);
  });

  it("memoizes the result across calls", () => {
    const a = loadEnv(minimalValidEnv);
    const b = loadEnv({
      ...minimalValidEnv,
      AUTOMATION_PORT: "9999", // ignored — cached value already returned
    });
    expect(a).toBe(b);
    expect(b.automationPort).toBe(3030);
  });

  it("re-parses after resetEnvCacheForTesting()", () => {
    loadEnv(minimalValidEnv);
    resetEnvCacheForTesting();
    const env = loadEnv({ ...minimalValidEnv, AUTOMATION_PORT: "4040" });
    expect(env.automationPort).toBe(4040);
  });
});
