import { expect, it } from "vitest";
import type {
  ChannelDirectoryEntry,
  ChannelFocusedBindingContext,
  ChannelReplyTransport,
  ChannelThreadingToolContext,
} from "../../../src/channels/plugins/types.core.js";
import type { ChannelPlugin } from "../../../src/channels/plugins/types.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { createNonExitingRuntime } from "../../../src/runtime.js";

const contractRuntime = createNonExitingRuntime();

function expectDirectoryEntryShape(entry: ChannelDirectoryEntry) {
  expect(["user", "group", "channel"]).toContain(entry.kind);
  expect(typeof entry.id).toBe("string");
  expect(entry.id.trim()).not.toBe("");
  if (entry.name !== undefined) {
    expect(typeof entry.name).toBe("string");
  }
  if (entry.handle !== undefined) {
    expect(typeof entry.handle).toBe("string");
  }
  if (entry.avatarUrl !== undefined) {
    expect(typeof entry.avatarUrl).toBe("string");
  }
  if (entry.rank !== undefined) {
    expect(typeof entry.rank).toBe("number");
  }
}

function expectThreadingToolContextShape(context: ChannelThreadingToolContext) {
  if (context.currentChannelId !== undefined) {
    expect(typeof context.currentChannelId).toBe("string");
  }
  if (context.currentChannelProvider !== undefined) {
    expect(typeof context.currentChannelProvider).toBe("string");
  }
  if (context.currentThreadTs !== undefined) {
    expect(typeof context.currentThreadTs).toBe("string");
  }
  if (context.currentMessageId !== undefined) {
    expect(["string", "number"]).toContain(typeof context.currentMessageId);
  }
  if (context.replyToMode !== undefined) {
    expect(["off", "first", "all"]).toContain(context.replyToMode);
  }
  if (context.hasRepliedRef !== undefined) {
    expect(typeof context.hasRepliedRef).toBe("object");
  }
  if (context.skipCrossContextDecoration !== undefined) {
    expect(typeof context.skipCrossContextDecoration).toBe("boolean");
  }
}

function expectReplyTransportShape(transport: ChannelReplyTransport) {
  if (transport.replyToId !== undefined && transport.replyToId !== null) {
    expect(typeof transport.replyToId).toBe("string");
  }
  if (transport.threadId !== undefined && transport.threadId !== null) {
    expect(["string", "number"]).toContain(typeof transport.threadId);
  }
}

function expectFocusedBindingShape(binding: ChannelFocusedBindingContext) {
  expect(typeof binding.conversationId).toBe("string");
  expect(binding.conversationId.trim()).not.toBe("");
  if (binding.parentConversationId !== undefined) {
    expect(typeof binding.parentConversationId).toBe("string");
  }
  expect(["current", "child"]).toContain(binding.placement);
  expect(typeof binding.labelNoun).toBe("string");
  expect(binding.labelNoun.trim()).not.toBe("");
}

export function installChannelThreadingContractSuite(params: {
  plugin: Pick<ChannelPlugin, "id" | "threading">;
}) {
  it("exposes the base threading contract", () => {
    expect(params.plugin.threading).toBeDefined();
  });

  it("keeps threading return values normalized", () => {
    const threading = params.plugin.threading;
    expect(threading).toBeDefined();

    if (threading?.resolveReplyToMode) {
      expect(
        ["off", "first", "all"].includes(
          threading.resolveReplyToMode({
            cfg: {} as OpenClawConfig,
            accountId: "default",
            chatType: "group",
          }),
        ),
      ).toBe(true);
    }

    const repliedRef = { value: false };
    const toolContext = threading?.buildToolContext?.({
      cfg: {} as OpenClawConfig,
      accountId: "default",
      context: {
        Channel: "group:test",
        From: "user:test",
        To: "group:test",
        ChatType: "group",
        CurrentMessageId: "msg-1",
        ReplyToId: "msg-0",
        ReplyToIdFull: "thread-0",
        MessageThreadId: "thread-0",
        NativeChannelId: "native:test",
      },
      hasRepliedRef: repliedRef,
    });

    if (toolContext) {
      expectThreadingToolContextShape(toolContext);
      if (toolContext.hasRepliedRef) {
        expect(toolContext.hasRepliedRef).toBe(repliedRef);
      }
    }

    const autoThreadId = threading?.resolveAutoThreadId?.({
      cfg: {} as OpenClawConfig,
      accountId: "default",
      to: "group:test",
      toolContext,
      replyToId: null,
    });
    if (autoThreadId !== undefined) {
      expect(typeof autoThreadId).toBe("string");
      expect(autoThreadId.trim()).not.toBe("");
    }

    const replyTransport = threading?.resolveReplyTransport?.({
      cfg: {} as OpenClawConfig,
      accountId: "default",
      threadId: "thread-0",
      replyToId: "msg-0",
    });
    if (replyTransport) {
      expectReplyTransportShape(replyTransport);
    }

    const focusedBinding = threading?.resolveFocusedBinding?.({
      cfg: {} as OpenClawConfig,
      accountId: "default",
      context: {
        Channel: "group:test",
        From: "user:test",
        To: "group:test",
        ChatType: "group",
        CurrentMessageId: "msg-1",
        ReplyToId: "msg-0",
        ReplyToIdFull: "thread-0",
        MessageThreadId: "thread-0",
        NativeChannelId: "native:test",
      },
    });
    if (focusedBinding) {
      expectFocusedBindingShape(focusedBinding);
    }
  });
}

export function installChannelDirectoryContractSuite(params: {
  plugin: Pick<ChannelPlugin, "id" | "directory">;
  coverage?: "lookups" | "presence";
  cfg?: OpenClawConfig;
  accountId?: string;
}) {
  it("exposes the base directory contract", async () => {
    const directory = params.plugin.directory;
    expect(directory).toBeDefined();

    if (params.coverage === "presence") {
      return;
    }
    const self = await directory?.self?.({
      cfg: params.cfg ?? ({} as OpenClawConfig),
      accountId: params.accountId ?? "default",
      runtime: contractRuntime,
    });
    if (self) {
      expectDirectoryEntryShape(self);
    }

    const peers =
      (await directory?.listPeers?.({
        cfg: params.cfg ?? ({} as OpenClawConfig),
        accountId: params.accountId ?? "default",
        query: "",
        limit: 5,
        runtime: contractRuntime,
      })) ?? [];
    expect(Array.isArray(peers)).toBe(true);
    for (const peer of peers) {
      expectDirectoryEntryShape(peer);
    }

    const groups =
      (await directory?.listGroups?.({
        cfg: params.cfg ?? ({} as OpenClawConfig),
        accountId: params.accountId ?? "default",
        query: "",
        limit: 5,
        runtime: contractRuntime,
      })) ?? [];
    expect(Array.isArray(groups)).toBe(true);
    for (const group of groups) {
      expectDirectoryEntryShape(group);
    }

    if (directory?.listGroupMembers && groups[0]?.id) {
      const members = await directory.listGroupMembers({
        cfg: params.cfg ?? ({} as OpenClawConfig),
        accountId: params.accountId ?? "default",
        groupId: groups[0].id,
        limit: 5,
        runtime: contractRuntime,
      });
      expect(Array.isArray(members)).toBe(true);
      for (const member of members) {
        expectDirectoryEntryShape(member);
      }
    }
  });
}
