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

import * as client from "../client";

describe("generateValidatedJson", () => {
  const originalProvider = process.env.LLM_PROVIDER;
  const originalDeepSeekKey = process.env.DEEPSEEK_API_KEY;

  afterEach(() => {
    process.env.LLM_PROVIDER = originalProvider;
    process.env.DEEPSEEK_API_KEY = originalDeepSeekKey;
    delete process.env.LLM_LOG_RAW_LIMIT;
    delete process.env.DEEPSEEK_MODEL;
    client.setGenerateJsonForValidation(client.generateJson);
    vi.restoreAllMocks();
  });

  it("throws clearly when no provider key is configured", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    delete process.env.DEEPSEEK_API_KEY;

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        prompt: "Return JSON.",
      }),
    ).rejects.toThrow("DEEPSEEK_API_KEY is missing");
  });

  it("returns parsed data when the model response matches the schema", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    client.setGenerateJsonForValidation(async () => ({ value: "from-llm" }));

    const result = await client.generateValidatedJson({
      schema: z.object({ value: z.string() }),
      prompt: "Return JSON.",
    });

    expect(result).toEqual({ value: "from-llm" });
  });

  it("throws clearly when the model response fails schema validation", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    client.setGenerateJsonForValidation(async () => ({ value: 42 }));

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        prompt: "Return JSON.",
      }),
    ).rejects.toThrow("LLM response failed schema validation");
  });

  it("throws clearly when the provider call rejects", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    client.setGenerateJsonForValidation(async () => {
      throw new Error("provider down");
    });

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        prompt: "Return JSON.",
      }),
    ).rejects.toThrow("provider down");
  });

  it("logs validation success with the supplied task label", async () => {
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    client.setGenerateJsonForValidation(async () => ({ value: "from-llm" }));

    const result = await client.generateValidatedJson({
      schema: z.object({ value: z.string() }),
      prompt: "Return JSON.",
      task: "article_optimizer",
    });

    expect(result).toEqual({ value: "from-llm" });
    expect(infoSpy).toHaveBeenCalledWith(
      "[llm:validated] task=article_optimizer ok=true",
    );
  });

  it("logs validation failure with a compact Zod summary", async () => {
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    client.setGenerateJsonForValidation(async () => ({ value: 42 }));

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        prompt: "Return JSON.",
        task: "fact_extractor",
      }),
    ).rejects.toThrow("LLM response failed schema validation");
    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining("[llm:validated] task=fact_extractor ok=false"),
    );
    expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("value"));
  });

  it("logs provider, model, task, raw response snippet, and validation success", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    process.env.DEEPSEEK_MODEL = "deepseek-test";
    process.env.LLM_LOG_RAW_LIMIT = "80";
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    client.setGenerateJsonForValidation(async () => ({
      value: "from-llm",
      longField: "x".repeat(200),
    }));

    const result = await client.generateValidatedJson({
      schema: z.object({ value: z.string(), longField: z.string() }),
      task: "article_optimizer",
      prompt: "Return JSON.",
    });

    expect(result?.value).toBe("from-llm");
    expect(infoSpy).toHaveBeenCalledWith(
      expect.stringContaining(
        "[llm:start] provider=deepseek model=deepseek-test task=article_optimizer",
      ),
    );
    expect(infoSpy).toHaveBeenCalledWith(
      expect.stringContaining("[llm:response] task=article_optimizer"),
    );
    expect(infoSpy).toHaveBeenCalledWith(
      expect.stringContaining('"value":"from-llm"'),
    );
    expect(infoSpy).toHaveBeenCalledWith(
      expect.stringContaining("[llm:validated] task=article_optimizer ok=true"),
    );
    expect(warnSpy).not.toHaveBeenCalled();
  });

  it("does not duplicate start and response logs when using the default JSON generator", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    client.setChatCompletionForTesting(async () => ({
      choices: [{ message: { content: '{"value":"from-llm"}' } }],
    }));

    const result = await client.generateValidatedJson({
      schema: z.object({ value: z.string() }),
      prompt: "Return JSON.",
      task: "article_optimizer",
    });

    expect(result).toEqual({ value: "from-llm" });
    const startLogs = infoSpy.mock.calls
      .map((call) => call[0])
      .filter((line) => line.startsWith("[llm:start]"));
    const responseLogs = infoSpy.mock.calls
      .map((call) => call[0])
      .filter((line) => line.startsWith("[llm:response]"));
    expect(startLogs).toHaveLength(1);
    expect(responseLogs).toHaveLength(1);
  });

  it("logs validation failure without leaking provider secrets", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "super-secret-key";
    process.env.DEEPSEEK_MODEL = "deepseek-test";
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    client.setGenerateJsonForValidation(async () => ({ value: 42 }));

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        task: "fact_extractor",
        prompt: "Return JSON.",
      }),
    ).rejects.toThrow("LLM response failed schema validation");
    const allLogs = [...infoSpy.mock.calls, ...warnSpy.mock.calls]
      .flat()
      .join("\n");
    expect(allLogs).toContain("[llm:validated] task=fact_extractor ok=false");
    expect(allLogs).toContain("zod_error=");
    expect(allLogs).not.toContain("super-secret-key");
  });

  it("logs provider call failure for validated JSON calls", async () => {
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
    client.setGenerateJsonForValidation(async () => {
      throw new Error("provider unavailable");
    });

    await expect(
      client.generateValidatedJson({
        schema: z.object({ value: z.string() }),
        task: "quality_inspector",
        prompt: "Return JSON.",
      }),
    ).rejects.toThrow("provider unavailable");
    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining("[llm:error] task=quality_inspector"),
    );
    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining("provider unavailable"),
    );
  });
});

describe("generateJson logging", () => {
  const originalProvider = process.env.LLM_PROVIDER;
  const originalDeepSeekKey = process.env.DEEPSEEK_API_KEY;

  afterEach(() => {
    process.env.LLM_PROVIDER = originalProvider;
    process.env.DEEPSEEK_API_KEY = originalDeepSeekKey;
    client.setChatCompletionForTesting(null);
    vi.restoreAllMocks();
  });

  it("logs provider, model, task, duration, and truncated raw response", async () => {
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "test-key";
    const longContent = `{"value":"${"x".repeat(4100)}"}`;
    client.setChatCompletionForTesting(async () => ({
      choices: [{ message: { content: longContent } }],
    }));

    const result = await client.generateJson<{ value: string }>({
      prompt: "Return JSON.",
      task: "article_optimizer",
    });

    expect(result.value).toHaveLength(4100);
    expect(infoSpy).toHaveBeenCalledWith(
      "[llm:start] provider=deepseek model=deepseek-v4-pro task=article_optimizer",
    );
    const responseLog = infoSpy.mock.calls
      .map((call) => call[0])
      .find((line) => line.startsWith("[llm:response]"));
    expect(responseLog).toContain("task=article_optimizer");
    expect(responseLog).toContain("duration_ms=");
    expect(responseLog).toContain("raw=");
    expect(responseLog?.length).toBeLessThan(4200);
  });

  it("logs provider errors without leaking API keys", async () => {
    const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
    const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
    process.env.LLM_PROVIDER = "deepseek";
    process.env.DEEPSEEK_API_KEY = "super-secret-key";
    client.setChatCompletionForTesting(async () => {
      throw new Error("upstream unavailable");
    });

    await expect(
      client.generateJson({
        prompt: "Return JSON.",
        task: "quality_inspector",
      }),
    ).rejects.toThrow("LLM provider error: upstream unavailable");

    expect(infoSpy).toHaveBeenCalledWith(
      "[llm:start] provider=deepseek model=deepseek-v4-pro task=quality_inspector",
    );
    const errorLog = errorSpy.mock.calls.map((call) => call[0]).join("\n");
    expect(errorLog).toContain("[llm:error] task=quality_inspector");
    expect(errorLog).not.toContain("super-secret-key");
  });
});
