import type { HttpService } from "@nestjs/axios";
import { ServiceUnavailableException } from "@nestjs/common";
import type { AgentProviderConfig } from "@prisma/client";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { EncryptionService } from "../../../security/encryption.service";
import { OpenClawSseBridge } from "./openclaw-sse.bridge";
import { OpenClawProvider } from "./openclaw.provider";

describe("OpenClawProvider", () => {
  let provider: OpenClawProvider;
  let httpService: {
    axiosRef: {
      get: ReturnType<typeof vi.fn>;
      post: ReturnType<typeof vi.fn>;
    };
  };
  let encryptionService: {
    decryptIfNeeded: ReturnType<typeof vi.fn>;
  };
  let sseBridge: {
    streamSession: ReturnType<typeof vi.fn>;
  };

  const config: AgentProviderConfig = {
    id: "cfg-openclaw-1",
    workspaceId: "workspace-1",
    name: "openclaw-home",
    provider: "openclaw",
    gatewayUrl: "https://gateway.example.com/",
    credentials: {
      apiToken: "enc:token-value",
      displayName: "Home OpenClaw",
    },
    isActive: true,
    createdAt: new Date("2026-03-07T15:00:00.000Z"),
    updatedAt: new Date("2026-03-07T15:00:00.000Z"),
  };

  beforeEach(() => {
    httpService = {
      axiosRef: {
        get: vi.fn(),
        post: vi.fn(),
      },
    };

    encryptionService = {
      decryptIfNeeded: vi.fn().mockReturnValue("plain-token"),
    };

    sseBridge = {
      streamSession: vi.fn(),
    };

    provider = new OpenClawProvider(
      config,
      encryptionService as unknown as EncryptionService,
      httpService as unknown as HttpService,
      sseBridge as unknown as OpenClawSseBridge
    );
  });

  it("maps listSessions from OpenClaw API", async () => {
    httpService.axiosRef.get.mockResolvedValue({
      data: {
        sessions: [
          {
            id: "session-1",
            status: "running",
            createdAt: "2026-03-07T15:01:00.000Z",
            updatedAt: "2026-03-07T15:02:00.000Z",
          },
        ],
        total: 1,
        cursor: "next-cursor",
      },
    });

    const result = await provider.listSessions("cursor-1", 25);

    expect(httpService.axiosRef.get).toHaveBeenCalledWith(
      "https://gateway.example.com/api/sessions",
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
        params: {
          cursor: "cursor-1",
          limit: 25,
        },
      }
    );
    expect(result).toEqual({
      sessions: [
        {
          id: "session-1",
          providerId: "openclaw-home",
          providerType: "openclaw",
          status: "active",
          createdAt: new Date("2026-03-07T15:01:00.000Z"),
          updatedAt: new Date("2026-03-07T15:02:00.000Z"),
        },
      ],
      total: 1,
      cursor: "next-cursor",
    });
    expect(encryptionService.decryptIfNeeded).toHaveBeenCalledWith("enc:token-value");
  });

  it("returns null from getSession when OpenClaw returns 404", async () => {
    httpService.axiosRef.get.mockRejectedValue({
      response: {
        status: 404,
      },
    });

    await expect(provider.getSession("missing-session")).resolves.toBeNull();
  });

  it("maps getMessages response", async () => {
    httpService.axiosRef.get.mockResolvedValue({
      data: {
        messages: [
          {
            id: "message-1",
            sessionId: "session-1",
            role: "agent",
            content: "hello",
            timestamp: "2026-03-07T15:03:00.000Z",
            metadata: {
              tokens: 128,
            },
          },
        ],
      },
    });

    const result = await provider.getMessages("session-1", 20, "before-cursor");

    expect(httpService.axiosRef.get).toHaveBeenCalledWith(
      "https://gateway.example.com/api/messages",
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
        params: {
          sessionId: "session-1",
          limit: 20,
          before: "before-cursor",
        },
      }
    );
    expect(result).toEqual([
      {
        id: "message-1",
        sessionId: "session-1",
        role: "assistant",
        content: "hello",
        timestamp: new Date("2026-03-07T15:03:00.000Z"),
        metadata: {
          tokens: 128,
        },
      },
    ]);
  });

  it("maps inject and control endpoints", async () => {
    httpService.axiosRef.post
      .mockResolvedValueOnce({
        data: {
          accepted: true,
          messageId: "message-2",
        },
      })
      .mockResolvedValueOnce({ data: {} })
      .mockResolvedValueOnce({ data: {} })
      .mockResolvedValueOnce({ data: {} });

    await expect(provider.injectMessage("session-1", "barge in")).resolves.toEqual({
      accepted: true,
      messageId: "message-2",
    });

    await provider.pauseSession("session-1");
    await provider.resumeSession("session-1");
    await provider.killSession("session-1", false);

    expect(httpService.axiosRef.post).toHaveBeenNthCalledWith(
      1,
      "https://gateway.example.com/api/sessions/session-1/inject",
      { content: "barge in" },
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
      }
    );
    expect(httpService.axiosRef.post).toHaveBeenNthCalledWith(
      2,
      "https://gateway.example.com/api/sessions/session-1/pause",
      {},
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
      }
    );
    expect(httpService.axiosRef.post).toHaveBeenNthCalledWith(
      3,
      "https://gateway.example.com/api/sessions/session-1/resume",
      {},
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
      }
    );
    expect(httpService.axiosRef.post).toHaveBeenNthCalledWith(
      4,
      "https://gateway.example.com/api/sessions/session-1/kill",
      { force: false },
      {
        headers: {
          Authorization: "Bearer plain-token",
        },
      }
    );
  });

  it("delegates streaming to OpenClawSseBridge", async () => {
    const streamedMessage = {
      id: "message-stream",
      sessionId: "session-stream",
      role: "assistant",
      content: "stream hello",
      timestamp: new Date("2026-03-07T16:00:00.000Z"),
    };

    sseBridge.streamSession.mockReturnValue(
      (async function* () {
        yield streamedMessage;
      })()
    );

    const messages: Array<unknown> = [];
    for await (const message of provider.streamMessages("session-stream")) {
      messages.push(message);
    }

    expect(sseBridge.streamSession).toHaveBeenCalledWith(
      "https://gateway.example.com",
      "session-stream",
      {
        Authorization: "Bearer plain-token",
      }
    );
    expect(messages).toEqual([streamedMessage]);
  });

  it("throws ServiceUnavailableException for request failures", async () => {
    httpService.axiosRef.get.mockRejectedValue(new Error("gateway unreachable"));

    await expect(provider.listSessions()).rejects.toBeInstanceOf(ServiceUnavailableException);
  });

  it("returns false from isAvailable when gateway check fails", async () => {
    httpService.axiosRef.get.mockRejectedValue(new Error("gateway unreachable"));

    await expect(provider.isAvailable()).resolves.toBe(false);
  });
});
