/**
 * embedding-factory.test.ts - Tests for createEmbeddingProvider factory.
 *
 * Verifies the resolution precedence:
 *   1. explicit `kind` argument
 *   2. QMD_EMBED_PROVIDER env
 *   3. QMD_EMBED_ENDPOINT env (forces openai)
 *   4. config file `embedProvider.kind` / `embedProvider.endpoint`
 *   5. missing commercial configuration: typed HOLD
 */

import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  resolveProviderKind,
  createEmbeddingProvider,
  loadConfigFile,
  assertCommercialEndpoint,
} from "../src/embedding/factory.js";
import { OpenAIEmbeddingsProvider } from "../src/embedding/openai.js";
import { CommercialApiHoldError } from "../src/model-policy.js";

let workDir: string;
let configPath: string;

beforeEach(() => {
  workDir = mkdtempSync(join(tmpdir(), "qmd-factory-test-"));
  mkdirSync(join(workDir, "qmd"), { recursive: true });
  configPath = join(workDir, "qmd", "config.json");
});

afterEach(() => {
  rmSync(workDir, { recursive: true, force: true });
});

// ─────────────────────────── Helpers ─────────────────────────────────────────

function writeConfig(obj: Record<string, unknown>) {
  writeFileSync(configPath, JSON.stringify(obj));
}

const EMPTY_ENV: Record<string, string | undefined> = {};

// ─────────────────────────── resolveProviderKind ─────────────────────────────

describe("resolveProviderKind", () => {
  test("explicit local kind is rejected", () => {
    expect(() => resolveProviderKind({
        kind: "local",
        env: { QMD_EMBED_ENDPOINT: "https://x" },
        configPath,
      })).toThrow(CommercialApiHoldError);
    expect(
      resolveProviderKind({
        kind: "openai",
        env: EMPTY_ENV,
        configPath,
      }),
    ).toBe("openai");
  });

  test("QMD_EMBED_PROVIDER=local is rejected even with an endpoint", () => {
    expect(() => resolveProviderKind({
        env: { QMD_EMBED_PROVIDER: "local", QMD_EMBED_ENDPOINT: "https://x" },
        configPath,
      })).toThrow(CommercialApiHoldError);
  });

  test("QMD_EMBED_ENDPOINT presence → openai", () => {
    expect(
      resolveProviderKind({
        env: { QMD_EMBED_ENDPOINT: "https://ai.example.com" },
        configPath,
      }),
    ).toBe("openai");
  });

  test("QMD_EMBED_ENDPOINT empty string resolves commercial kind then holds at construction", () => {
    expect(
      resolveProviderKind({
        env: { QMD_EMBED_ENDPOINT: "" },
        configPath,
      }),
    ).toBe("openai");
  });

  test("config file embedProvider.kind respected", () => {
    writeConfig({ embedProvider: { kind: "openai", endpoint: "https://ai.example.com" } });
    expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
  });

  test("config file embedProvider.endpoint alone → openai", () => {
    writeConfig({ embedProvider: { endpoint: "https://ai.example.com" } });
    expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
  });

  test("no signal anywhere keeps commercial-only kind", () => {
    expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
  });

  test("invalid env QMD_EMBED_PROVIDER is ignored", () => {
    expect(
      resolveProviderKind({
        env: { QMD_EMBED_PROVIDER: "garbage" },
        configPath,
      }),
    ).toBe("openai");
  });

  test("uppercase env QMD_EMBED_PROVIDER normalized", () => {
    expect(
      resolveProviderKind({
        env: { QMD_EMBED_PROVIDER: "OPENAI", QMD_EMBED_ENDPOINT: "https://x" },
        configPath,
      }),
    ).toBe("openai");
  });
});

// ─────────────────────────── createEmbeddingProvider ─────────────────────────

describe("createEmbeddingProvider", () => {
  test("openai kind w/ endpoint env → OpenAIEmbeddingsProvider", () => {
    const p = createEmbeddingProvider({
      env: { QMD_EMBED_ENDPOINT: "https://ai.example.com" },
      configPath,
    });
    expect(p).toBeInstanceOf(OpenAIEmbeddingsProvider);
    expect(p.kind).toBe("openai");
    expect(p.getModelId()).toBe("embeddinggemma");
  });

  test("openai kind w/ explicit options merges over env", () => {
    const p = createEmbeddingProvider({
      env: { QMD_EMBED_ENDPOINT: "https://env.example.com", QMD_EMBED_API_KEY: "env-key" },
      configPath,
      openai: { endpoint: "https://override.example.com" },
    });
    // Cast to access internal properties for verification
    const inner = p as OpenAIEmbeddingsProvider & { endpoint: string; apiKey: string };
    expect(inner["endpoint"]).toBe("https://override.example.com");
    // apiKey should still come from env since we didn't override it
    expect(inner["apiKey"]).toBe("env-key");
  });

  test("openai kind reads modelId from env", () => {
    const p = createEmbeddingProvider({
      env: {
        QMD_EMBED_ENDPOINT: "https://ai.example.com",
        QMD_EMBED_MODEL_ID: "custom-model",
      },
      configPath,
    });
    expect(p.getModelId()).toBe("custom-model");
  });

  test("openai kind reads upstream model from env", () => {
    const p = createEmbeddingProvider({
      env: {
        QMD_EMBED_ENDPOINT: "https://ai.example.com",
        QMD_EMBED_UPSTREAM_MODEL: "embeddinggemma:300m",
      },
      configPath,
    }) as OpenAIEmbeddingsProvider & { upstreamModel: string };
    expect(p["upstreamModel"]).toBe("embeddinggemma:300m");
  });

  test("openai kind reads batch size and timeout from env", () => {
    const p = createEmbeddingProvider({
      env: {
        QMD_EMBED_ENDPOINT: "https://ai.example.com",
        QMD_EMBED_BATCH_SIZE: "32",
        QMD_EMBED_TIMEOUT_MS: "5000",
      },
      configPath,
    }) as OpenAIEmbeddingsProvider & { batchSize: number; timeoutMs: number };
    expect(p["batchSize"]).toBe(32);
    expect(p["timeoutMs"]).toBe(5000);
  });

  test("openai kind merges config file values", () => {
    writeConfig({
      embedProvider: {
        kind: "openai",
        endpoint: "https://config.example.com",
        apiKey: "config-key",
        modelId: "config-model",
        batchSize: 16,
      },
    });
    const p = createEmbeddingProvider({
      env: EMPTY_ENV,
      configPath,
    }) as OpenAIEmbeddingsProvider & {
      endpoint: string;
      apiKey: string;
      batchSize: number;
    };
    expect(p["endpoint"]).toBe("https://config.example.com");
    expect(p["apiKey"]).toBe("config-key");
    expect(p.getModelId()).toBe("config-model");
    expect(p["batchSize"]).toBe(16);
  });

  test("env wins over config file", () => {
    writeConfig({
      embedProvider: {
        endpoint: "https://config.example.com",
      },
    });
    const p = createEmbeddingProvider({
      env: { QMD_EMBED_ENDPOINT: "https://env.example.com" },
      configPath,
    }) as OpenAIEmbeddingsProvider & { endpoint: string };
    expect(p["endpoint"]).toBe("https://env.example.com");
  });

  test("openai kind without endpoint throws", () => {
    expect(() =>
      createEmbeddingProvider({ kind: "openai", env: EMPTY_ENV, configPath }),
    ).toThrow(/endpoint/);
  });

  test("local kind explicitly requested → typed HOLD", () => {
    expect(() => createEmbeddingProvider({
      kind: "local",
      env: EMPTY_ENV,
      configPath,
    })).toThrow(CommercialApiHoldError);
  });

  test("missing endpoint → typed HOLD", () => {
    expect(() => createEmbeddingProvider({ env: EMPTY_ENV, configPath }))
      .toThrow(CommercialApiHoldError);
  });

  test("legacy auto-fallback request → typed HOLD", () => {
    expect(() => createEmbeddingProvider({
      env: { QMD_EMBED_ENDPOINT: "https://commercial.example.com" },
      autoFallback: true,
      configPath,
    })).toThrow(CommercialApiHoldError);
  });

  test("self-hosted and local endpoints → typed HOLD", () => {
    for (const endpoint of [
      "http://models:8082",
      "http://127.0.0.1:8082",
      "https://10.0.2.162/v1",
      "https://localhost/v1",
    ]) {
      expect(() => assertCommercialEndpoint(endpoint)).toThrow(CommercialApiHoldError);
    }
  });

  test("commercial HTTPS endpoint is accepted", () => {
    expect(() => assertCommercialEndpoint("https://generativelanguage.googleapis.com/v1beta/openai"))
      .not.toThrow();
  });
});

// ─────────────────────────── loadConfigFile ──────────────────────────────────

describe("loadConfigFile", () => {
  test("missing file → empty object", () => {
    expect(loadConfigFile(join(workDir, "missing.json"))).toEqual({});
  });

  test("invalid JSON → empty object (no throw)", () => {
    writeFileSync(configPath, "not json");
    expect(loadConfigFile(configPath)).toEqual({});
  });

  test("valid JSON parsed", () => {
    writeConfig({ embedProvider: { kind: "openai" } });
    expect(loadConfigFile(configPath)).toEqual({
      embedProvider: { kind: "openai" },
    });
  });
});
