import { ServiceUnavailableException } from "@nestjs/common";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ChatProxyService } from "./chat-proxy.service";

describe("ChatProxyService", () => {
  const userId = "user-123";

  const prisma = {
    userAgentConfig: {
      findUnique: vi.fn(),
    },
  };

  const containerLifecycle = {
    ensureRunning: vi.fn(),
    touch: vi.fn(),
  };

  let service: ChatProxyService;
  let fetchMock: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    fetchMock = vi.fn();
    vi.stubGlobal("fetch", fetchMock);
    service = new ChatProxyService(prisma as never, containerLifecycle as never);
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    vi.clearAllMocks();
  });

  describe("getContainerUrl", () => {
    it("calls ensureRunning and touch for the user", async () => {
      containerLifecycle.ensureRunning.mockResolvedValue({
        url: "http://mosaic-user-user-123:19000",
        token: "gateway-token",
      });
      containerLifecycle.touch.mockResolvedValue(undefined);

      const url = await service.getContainerUrl(userId);

      expect(url).toBe("http://mosaic-user-user-123:19000");
      expect(containerLifecycle.ensureRunning).toHaveBeenCalledWith(userId);
      expect(containerLifecycle.touch).toHaveBeenCalledWith(userId);
    });
  });

  describe("proxyChat", () => {
    it("forwards the request to the user's OpenClaw container", async () => {
      containerLifecycle.ensureRunning.mockResolvedValue({
        url: "http://mosaic-user-user-123:19000",
        token: "gateway-token",
      });
      containerLifecycle.touch.mockResolvedValue(undefined);
      fetchMock.mockResolvedValue(new Response("event: token\ndata: hello\n\n"));

      const messages = [{ role: "user", content: "Hello from Mosaic" }];
      const response = await service.proxyChat(userId, messages);

      expect(response).toBeInstanceOf(Response);
      expect(fetchMock).toHaveBeenCalledWith(
        "http://mosaic-user-user-123:19000/v1/chat/completions",
        expect.objectContaining({
          method: "POST",
          headers: {
            Authorization: "Bearer gateway-token",
            "Content-Type": "application/json",
          },
        })
      );

      const [, request] = fetchMock.mock.calls[0] as [string, RequestInit];
      const parsedBody = JSON.parse(String(request.body));
      expect(parsedBody).toEqual({
        messages,
        model: "openclaw:default",
        stream: true,
      });
    });

    it("throws ServiceUnavailableException on connection refused errors", async () => {
      containerLifecycle.ensureRunning.mockResolvedValue({
        url: "http://mosaic-user-user-123:19000",
        token: "gateway-token",
      });
      containerLifecycle.touch.mockResolvedValue(undefined);
      fetchMock.mockRejectedValue(new Error("connect ECONNREFUSED 127.0.0.1:19000"));

      await expect(service.proxyChat(userId, [])).rejects.toBeInstanceOf(
        ServiceUnavailableException
      );
    });

    it("throws ServiceUnavailableException on timeout errors", async () => {
      containerLifecycle.ensureRunning.mockResolvedValue({
        url: "http://mosaic-user-user-123:19000",
        token: "gateway-token",
      });
      containerLifecycle.touch.mockResolvedValue(undefined);
      fetchMock.mockRejectedValue(new Error("The operation was aborted due to timeout"));

      await expect(service.proxyChat(userId, [])).rejects.toBeInstanceOf(
        ServiceUnavailableException
      );
    });
  });
});
