import { describe, expect, it } from "vitest";
import {
  createOpenAICompatiblePiAdapter,
} from "../src/index.js";

describe("pi-adapter structured errors", () => {
  it("returns ok:false with no_content when LLM returns empty", async () => {
    const server = await startMockLLM({ responseContent: "" });
    try {
      const pi = createOpenAICompatiblePiAdapter({
        baseUrl: server.baseUrl,
        apiKey: "test",
        defaultModel: "fast",
      });
      const result = await pi.chat({
        message: "hi",
        model: "fast",
        docs: [],
        availableActions: [],
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.kind).toBe("no_content");
        expect(result.fallback.answer).toBe("");
        expect(result.fallback.recommended_actions).toEqual([]);
      }
    } finally {
      await server.stop();
    }
  });

  it("returns ok:false with json_parse when response has no JSON object", async () => {
    const server = await startMockLLM({ responseContent: "Just plain text answer" });
    try {
      const pi = createOpenAICompatiblePiAdapter({
        baseUrl: server.baseUrl,
        apiKey: "test",
        defaultModel: "fast",
      });
      const result = await pi.chat({
        message: "hi",
        model: "fast",
        docs: [],
        availableActions: [],
      });
      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.kind).toBe("json_parse");
        expect(result.fallback.answer).toBe("Just plain text answer");
      }
    } finally {
      await server.stop();
    }
  });

  it("returns ok:true when response is well-formed JSON", async () => {
    const server = await startMockLLM({
      responseContent: JSON.stringify({
        answer: "All good",
        recommended_actions: [],
        internal_docs: [],
      }),
    });
    try {
      const pi = createOpenAICompatiblePiAdapter({
        baseUrl: server.baseUrl,
        apiKey: "test",
        defaultModel: "fast",
      });
      const result = await pi.chat({
        message: "hi",
        model: "fast",
        docs: [],
        availableActions: [],
      });
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.result.answer).toBe("All good");
      }
    } finally {
      await server.stop();
    }
  });

  it("throws on non-OK HTTP response (transport error, not parse error)", async () => {
    const server = await startMockLLM({ status: 500, responseContent: "" });
    try {
      const pi = createOpenAICompatiblePiAdapter({
        baseUrl: server.baseUrl,
        apiKey: "test",
        defaultModel: "fast",
      });
      await expect(
        pi.chat({
          message: "hi",
          model: "fast",
          docs: [],
          availableActions: [],
        }),
      ).rejects.toThrow(/llm_request_failed:500/);
    } finally {
      await server.stop();
    }
  });

  it("filters recommended_actions to known ids and clamps scores", async () => {
    const server = await startMockLLM({
      responseContent: JSON.stringify({
        answer: "ok",
        recommended_actions: [
          { type: "webhook", id: "dns-flush", confidence: 2.5, reason: "x" },
          { type: "webhook", id: "unknown-id", confidence: 0.9, reason: "y" },
        ],
        internal_docs: [],
      }),
    });
    try {
      const pi = createOpenAICompatiblePiAdapter({
        baseUrl: server.baseUrl,
        apiKey: "test",
        defaultModel: "fast",
      });
      const result = await pi.chat({
        message: "hi",
        model: "fast",
        docs: [],
        availableActions: [
          { type: "webhook", id: "dns-flush", confidence: 0, reason: "r", requires_confirmation: true },
        ],
      });
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.result.recommended_actions).toHaveLength(1);
        expect(result.result.recommended_actions[0]?.id).toBe("dns-flush");
        expect(result.result.recommended_actions[0]?.confidence).toBe(1);
      }
    } finally {
      await server.stop();
    }
  });
});

import { createServer, type Server } from "node:http";

async function startMockLLM(opts: { responseContent: string; status?: number }): Promise<{
  baseUrl: string;
  stop: () => Promise<void>;
}> {
  let s: Server;
  return await new Promise((resolve) => {
    s = createServer((_req, res) => {
      res.writeHead(opts.status ?? 200, { "content-type": "application/json" });
      res.end(
        JSON.stringify({
          id: "mock",
          object: "chat.completion",
          created: 0,
          model: "fast",
          choices: [
            {
              index: 0,
              message: { role: "assistant", content: opts.responseContent },
              finish_reason: "stop",
            },
          ],
        }),
      );
    });
    s.listen(0, "127.0.0.1", () => {
      const address = s.address();
      const port = typeof address === "object" && address ? address.port : 0;
      resolve({
        baseUrl: `http://127.0.0.1:${port}/v1`,
        stop: () =>
          new Promise<void>((res) => {
            s!.close(() => res());
          }),
      });
    });
  });
}
