import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "openclaw/plugin-sdk/agent-runtime";
import { vi } from "vitest";
import {
  removeAckReactionAfterReply,
  shouldAckReaction,
} from "../../../src/channels/ack-reactions.js";
import {
  implicitMentionKindWhen,
  resolveInboundMentionDecision,
} from "../../../src/channels/mention-gating.js";
import type { PluginRuntime } from "../../../src/plugins/runtime/types.js";

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends (...args: never[]) => unknown
    ? T[K]
    : T[K] extends ReadonlyArray<unknown>
      ? T[K]
      : T[K] extends object
        ? DeepPartial<T[K]>
        : T[K];
};

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function mergeDeep<T>(base: T, overrides: DeepPartial<T>): T {
  const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
  for (const [key, overrideValue] of Object.entries(overrides as Record<string, unknown>)) {
    if (overrideValue === undefined) {
      continue;
    }
    const baseValue = result[key];
    if (isObject(baseValue) && isObject(overrideValue)) {
      result[key] = mergeDeep(baseValue, overrideValue);
      continue;
    }
    result[key] = overrideValue;
  }
  return result as T;
}

export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
  const taskFlow = {
    bindSession: vi.fn(() => ({
      sessionKey: "agent:main:main",
      createManaged: vi.fn(),
      get: vi.fn(),
      list: vi.fn(() => []),
      findLatest: vi.fn(),
      resolve: vi.fn(),
      getTaskSummary: vi.fn(),
      setWaiting: vi.fn(),
      resume: vi.fn(),
      finish: vi.fn(),
      fail: vi.fn(),
      requestCancel: vi.fn(),
      cancel: vi.fn(),
      runTask: vi.fn(),
    })) as unknown as PluginRuntime["taskFlow"]["bindSession"],
    fromToolContext: vi.fn(() => ({
      sessionKey: "agent:main:main",
      createManaged: vi.fn(),
      get: vi.fn(),
      list: vi.fn(() => []),
      findLatest: vi.fn(),
      resolve: vi.fn(),
      getTaskSummary: vi.fn(),
      setWaiting: vi.fn(),
      resume: vi.fn(),
      finish: vi.fn(),
      fail: vi.fn(),
      requestCancel: vi.fn(),
      cancel: vi.fn(),
      runTask: vi.fn(),
    })) as unknown as PluginRuntime["taskFlow"]["fromToolContext"],
  };
  const base: PluginRuntime = {
    version: "1.0.0-test",
    config: {
      loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
      writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
    },
    agent: {
      defaults: {
        model: DEFAULT_MODEL,
        provider: DEFAULT_PROVIDER,
      },
      resolveAgentDir: vi.fn(
        () => "/tmp/agent",
      ) as unknown as PluginRuntime["agent"]["resolveAgentDir"],
      resolveAgentWorkspaceDir: vi.fn(
        () => "/tmp/workspace",
      ) as unknown as PluginRuntime["agent"]["resolveAgentWorkspaceDir"],
      resolveAgentIdentity: vi.fn(() => ({
        name: "test-agent",
      })) as unknown as PluginRuntime["agent"]["resolveAgentIdentity"],
      resolveThinkingDefault: vi.fn(
        () => "off",
      ) as unknown as PluginRuntime["agent"]["resolveThinkingDefault"],
      runEmbeddedPiAgent: vi.fn().mockResolvedValue({
        payloads: [],
        meta: {},
      }) as unknown as PluginRuntime["agent"]["runEmbeddedPiAgent"],
      runEmbeddedAgent: vi.fn().mockResolvedValue({
        payloads: [],
        meta: {},
      }) as unknown as PluginRuntime["agent"]["runEmbeddedAgent"],
      resolveAgentTimeoutMs: vi.fn(
        () => 30_000,
      ) as unknown as PluginRuntime["agent"]["resolveAgentTimeoutMs"],
      ensureAgentWorkspace: vi
        .fn()
        .mockResolvedValue(undefined) as unknown as PluginRuntime["agent"]["ensureAgentWorkspace"],
      session: {
        resolveStorePath: vi.fn(
          () => "/tmp/agent-sessions.json",
        ) as unknown as PluginRuntime["agent"]["session"]["resolveStorePath"],
        loadSessionStore: vi.fn(
          () => ({}),
        ) as unknown as PluginRuntime["agent"]["session"]["loadSessionStore"],
        saveSessionStore: vi
          .fn()
          .mockResolvedValue(
            undefined,
          ) as unknown as PluginRuntime["agent"]["session"]["saveSessionStore"],
        resolveSessionFilePath: vi.fn(
          (sessionId: string) => `/tmp/${sessionId}.json`,
        ) as unknown as PluginRuntime["agent"]["session"]["resolveSessionFilePath"],
      },
    },
    system: {
      enqueueSystemEvent: vi.fn() as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
      requestHeartbeatNow: vi.fn() as unknown as PluginRuntime["system"]["requestHeartbeatNow"],
      runHeartbeatOnce: vi.fn(async () => ({
        status: "ran" as const,
        durationMs: 0,
      })) as unknown as PluginRuntime["system"]["runHeartbeatOnce"],
      runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
      formatNativeDependencyHint: vi.fn(
        () => "",
      ) as unknown as PluginRuntime["system"]["formatNativeDependencyHint"],
    },
    media: {
      loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
      detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
      mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
      isVoiceCompatibleAudio:
        vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
      getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
      resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
    },
    tts: {
      textToSpeech: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeech"],
      textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"],
      listVoices: vi.fn() as unknown as PluginRuntime["tts"]["listVoices"],
    },
    mediaUnderstanding: {
      runFile: vi.fn() as unknown as PluginRuntime["mediaUnderstanding"]["runFile"],
      describeImageFile:
        vi.fn() as unknown as PluginRuntime["mediaUnderstanding"]["describeImageFile"],
      describeImageFileWithModel:
        vi.fn() as unknown as PluginRuntime["mediaUnderstanding"]["describeImageFileWithModel"],
      describeVideoFile:
        vi.fn() as unknown as PluginRuntime["mediaUnderstanding"]["describeVideoFile"],
      transcribeAudioFile:
        vi.fn() as unknown as PluginRuntime["mediaUnderstanding"]["transcribeAudioFile"],
    },
    imageGeneration: {
      generate: vi.fn() as unknown as PluginRuntime["imageGeneration"]["generate"],
      listProviders: vi.fn() as unknown as PluginRuntime["imageGeneration"]["listProviders"],
    },
    musicGeneration: {
      generate: vi.fn() as unknown as PluginRuntime["musicGeneration"]["generate"],
      listProviders: vi.fn() as unknown as PluginRuntime["musicGeneration"]["listProviders"],
    },
    videoGeneration: {
      generate: vi.fn() as unknown as PluginRuntime["videoGeneration"]["generate"],
      listProviders: vi.fn() as unknown as PluginRuntime["videoGeneration"]["listProviders"],
    },
    webSearch: {
      listProviders: vi.fn() as unknown as PluginRuntime["webSearch"]["listProviders"],
      search: vi.fn() as unknown as PluginRuntime["webSearch"]["search"],
    },
    stt: {
      transcribeAudioFile: vi.fn() as unknown as PluginRuntime["stt"]["transcribeAudioFile"],
    },
    channel: {
      text: {
        chunkByNewline: vi.fn((text: string) => (text ? [text] : [])),
        chunkMarkdownText: vi.fn((text: string) => [text]),
        chunkMarkdownTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
        chunkText: vi.fn((text: string) => (text ? [text] : [])),
        chunkTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
        resolveChunkMode: vi.fn(
          () => "length",
        ) as unknown as PluginRuntime["channel"]["text"]["resolveChunkMode"],
        resolveTextChunkLimit: vi.fn(() => 4000),
        hasControlCommand: vi.fn(() => false),
        resolveMarkdownTableMode: vi.fn(
          () => "code",
        ) as unknown as PluginRuntime["channel"]["text"]["resolveMarkdownTableMode"],
        convertMarkdownTables: vi.fn((text: string) => text),
      },
      reply: {
        dispatchReplyWithBufferedBlockDispatcher: vi.fn(
          async () => undefined,
        ) as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
        createReplyDispatcherWithTyping:
          vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
        resolveEffectiveMessagesConfig:
          vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
        resolveHumanDelayConfig:
          vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
        dispatchReplyFromConfig:
          vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
        withReplyDispatcher: vi.fn(async ({ dispatcher, run, onSettled }) => {
          try {
            return await run();
          } finally {
            dispatcher.markComplete();
            try {
              await dispatcher.waitForIdle();
            } finally {
              await onSettled?.();
            }
          }
        }) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
        finalizeInboundContext: vi.fn(
          (ctx: Record<string, unknown>) => ctx,
        ) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
        formatAgentEnvelope: vi.fn(
          (opts: { body: string }) => opts.body,
        ) as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
        formatInboundEnvelope: vi.fn(
          (opts: { body: string }) => opts.body,
        ) as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
        resolveEnvelopeFormatOptions: vi.fn(() => ({
          template: "channel+name+time",
        })) as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
      },
      routing: {
        buildAgentSessionKey: vi.fn(
          ({
            agentId,
            channel,
            peer,
          }: {
            agentId: string;
            channel: string;
            peer?: { kind?: string; id?: string };
          }) => `agent:${agentId}:${channel}:${peer?.kind ?? "direct"}:${peer?.id ?? "peer"}`,
        ) as unknown as PluginRuntime["channel"]["routing"]["buildAgentSessionKey"],
        resolveAgentRoute: vi.fn(() => ({
          agentId: "main",
          accountId: "default",
          sessionKey: "agent:main:test:dm:peer",
        })) as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
      },
      pairing: {
        buildPairingReply: vi.fn(
          () => "Pairing code: TESTCODE",
        ) as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
        readAllowFromStore: vi
          .fn()
          .mockResolvedValue(
            [],
          ) as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
        upsertPairingRequest: vi.fn().mockResolvedValue({
          code: "TESTCODE",
          created: true,
        }) as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
      },
      media: {
        fetchRemoteMedia:
          vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
        saveMediaBuffer: vi.fn().mockResolvedValue({
          path: "/tmp/test-media.jpg",
          contentType: "image/jpeg",
        }) as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
      },
      session: {
        resolveStorePath: vi.fn(
          () => "/tmp/sessions.json",
        ) as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
        readSessionUpdatedAt: vi.fn(
          () => undefined,
        ) as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
        recordSessionMetaFromInbound:
          vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
        recordInboundSession:
          vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
        updateLastRoute:
          vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
      },
      mentions: {
        buildMentionRegexes: vi.fn(() => [
          /\bbert\b/i,
        ]) as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
        matchesMentionPatterns: vi.fn((text: string, regexes: RegExp[]) =>
          regexes.some((regex) => regex.test(text)),
        ) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
        matchesMentionWithExplicit: vi.fn(
          (params: { text: string; mentionRegexes: RegExp[]; explicitWasMentioned?: boolean }) =>
            params.explicitWasMentioned === true
              ? true
              : params.mentionRegexes.some((regex) => regex.test(params.text)),
        ) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
        implicitMentionKindWhen,
        resolveInboundMentionDecision,
      },
      reactions: {
        shouldAckReaction,
        removeAckReactionAfterReply,
      },
      groups: {
        resolveGroupPolicy: vi.fn(
          () => "open",
        ) as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
        resolveRequireMention: vi.fn(
          () => false,
        ) as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
      },
      debounce: {
        createInboundDebouncer: vi.fn(
          (params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
            enqueue: async (item: unknown) => {
              await params.onFlush([item]);
            },
            flushKey: vi.fn(),
          }),
        ) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
        resolveInboundDebounceMs: vi.fn(
          () => 0,
        ) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
      },
      commands: {
        resolveCommandAuthorizedFromAuthorizers: vi.fn(
          () => false,
        ) as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
        isControlCommandMessage:
          vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
        shouldComputeCommandAuthorized:
          vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
        shouldHandleTextCommands:
          vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
      },
      outbound: {
        loadAdapter: vi.fn() as unknown as PluginRuntime["channel"]["outbound"]["loadAdapter"],
      },
      threadBindings: {
        setIdleTimeoutBySessionKey:
          vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setIdleTimeoutBySessionKey"],
        setMaxAgeBySessionKey:
          vi.fn() as unknown as PluginRuntime["channel"]["threadBindings"]["setMaxAgeBySessionKey"],
      },
      runtimeContexts: {
        register: vi.fn(({ abortSignal }: { abortSignal?: AbortSignal }) => {
          const lease = { dispose: vi.fn() };
          abortSignal?.addEventListener("abort", lease.dispose, { once: true });
          return lease;
        }) as unknown as PluginRuntime["channel"]["runtimeContexts"]["register"],
        get: vi.fn() as unknown as PluginRuntime["channel"]["runtimeContexts"]["get"],
        watch: vi.fn(() =>
          vi.fn(),
        ) as unknown as PluginRuntime["channel"]["runtimeContexts"]["watch"],
      },
      activity: {} as PluginRuntime["channel"]["activity"],
    },
    events: {
      onAgentEvent: vi.fn(() => () => {}) as unknown as PluginRuntime["events"]["onAgentEvent"],
      onSessionTranscriptUpdate: vi.fn(
        () => () => {},
      ) as unknown as PluginRuntime["events"]["onSessionTranscriptUpdate"],
    },
    logging: {
      shouldLogVerbose: vi.fn(() => false),
      getChildLogger: vi.fn(() => ({
        info: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
        debug: vi.fn(),
      })),
    },
    state: {
      resolveStateDir: vi.fn(() => "/tmp/openclaw"),
    },
    tasks: {
      runs: {
        bindSession: vi.fn(),
        fromToolContext: vi.fn(),
      } as PluginRuntime["tasks"]["runs"],
      flows: {
        bindSession: vi.fn(),
        fromToolContext: vi.fn(),
      } as PluginRuntime["tasks"]["flows"],
      flow: taskFlow,
    },
    taskFlow,
    modelAuth: {
      getApiKeyForModel: vi.fn() as unknown as PluginRuntime["modelAuth"]["getApiKeyForModel"],
      getRuntimeAuthForModel:
        vi.fn() as unknown as PluginRuntime["modelAuth"]["getRuntimeAuthForModel"],
      resolveApiKeyForProvider:
        vi.fn() as unknown as PluginRuntime["modelAuth"]["resolveApiKeyForProvider"],
    },
    subagent: {
      run: vi.fn(),
      waitForRun: vi.fn(),
      getSessionMessages: vi.fn(),
      getSession: vi.fn(),
      deleteSession: vi.fn(),
    },
  };

  return mergeDeep(base, overrides);
}
