// Telegram tests cover send plugin behavior.
import fs from "node:fs";
import type { Bot } from "grammy";
import {
  createPluginStateKeyedStoreForTests,
  createPluginStateSyncKeyedStoreForTests,
  resetPluginStateStoreForTests,
} from "openclaw/plugin-sdk/plugin-state-test-runtime";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { markdownToTelegramHtml } from "./format.js";
import {
  buildTelegramConversationContext,
  createTelegramMessageCache,
  resolveTelegramMessageCacheScope,
  resetTelegramMessageCacheBucketsForTest,
} from "./message-cache.js";
import { clearTelegramRuntime, setTelegramRuntime } from "./runtime.js";
import type { TelegramRuntime } from "./runtime.types.js";
import type { TelegramApiOverride } from "./send.js";
import {
  getTelegramSendTestMocks,
  importTelegramSendModule,
  installTelegramSendTestHooks,
} from "./send.test-harness.js";
import {
  TELEGRAM_SENT_MESSAGE_CACHE_MAX_ENTRIES,
  TELEGRAM_SENT_MESSAGE_CACHE_NAMESPACE,
  clearSentMessageCache,
  recordSentMessage,
  resetSentMessageCacheForTest,
  setTelegramSentMessageStoreForTest,
  wasSentByBot,
} from "./sent-message-cache.js";

installTelegramSendTestHooks();

const {
  botApi,
  botRawApi,
  botConfigUseSpy,
  botCtorSpy,
  imageMetadata,
  loadConfig,
  loadWebMedia,
  maybePersistResolvedTelegramTarget,
  probeVideoDimensions,
} = getTelegramSendTestMocks();
const telegramSendModule = await importTelegramSendModule();
const { resetLogger, setLoggerOverride } = await import("openclaw/plugin-sdk/runtime-env");
const {
  buildInlineKeyboard,
  createForumTopicTelegram,
  deleteMessageTelegram,
  editForumTopicTelegram,
  editMessageTelegram,
  pinMessageTelegram,
  reactMessageTelegram,
  renameForumTopicTelegram,
  sendMessageTelegram: sendMessageTelegramImported,
  sendTypingTelegram,
  sendPollTelegram,
  sendStickerTelegram,
  unpinMessageTelegram,
} = telegramSendModule;
const sendMessageTelegramImpl = sendMessageTelegramImported;

type RichSendCallParams = {
  rich_message?: { markdown?: string; html?: string };
  reply_markup?: unknown;
};

type RichRawTextTestApi = Omit<TelegramApiOverride, "raw" | "sendMessage"> & {
  raw?: {
    sendRichMessage?: (params: {
      chat_id: number | string;
      rich_message: { markdown?: string; html?: string; skip_entity_detection?: boolean };
      [key: string]: unknown;
    }) => Promise<unknown>;
  };
  sendMessage?: (
    chatId: number | string,
    text: string,
    params?: Record<string, unknown>,
  ) => Promise<unknown>;
};

function richTextForTest(richMessage: { markdown?: string; html?: string }): string {
  return richMessage.markdown != null
    ? markdownToTelegramHtml(richMessage.markdown)
    : (richMessage.html ?? "");
}

function richSendCallParams(): RichSendCallParams[] {
  return botRawApi.sendRichMessage.mock.calls.map(([params]) => params);
}

function withRichRawTextTestApi(
  api: TelegramApiOverride | undefined,
): TelegramApiOverride | undefined {
  if (!api) {
    return undefined;
  }
  const textApi = api as RichRawTextTestApi;
  if (textApi.raw?.sendRichMessage || !textApi.sendMessage) {
    return api;
  }
  textApi.raw = {
    ...textApi.raw,
    sendRichMessage: async ({ chat_id, rich_message, ...params }) =>
      await textApi.sendMessage?.(chat_id, richTextForTest(rich_message), {
        parse_mode: "HTML",
        ...(rich_message.skip_entity_detection === true ? { skip_entity_detection: true } : {}),
        ...params,
      }),
  };
  return api;
}

const sendMessageTelegram: typeof sendMessageTelegramImpl = async (to, text, opts) =>
  await sendMessageTelegramImpl(
    to,
    text,
    opts
      ? {
          ...opts,
          api: withRichRawTextTestApi(opts.api),
        }
      : opts,
  );

const TELEGRAM_TEST_CFG = {};
let sentMessageStore: NonNullable<Parameters<typeof setTelegramSentMessageStoreForTest>[0]>;

function markdownTable(columns: number): string {
  return [
    Array.from({ length: columns }, (_, index) => `H${index + 1}`).join(" | "),
    Array.from({ length: columns }, () => "---").join(" | "),
    Array.from({ length: columns }, (_, index) => String(index + 1)).join(" | "),
  ]
    .map((row) => `| ${row} |`)
    .join("\n");
}

beforeEach(() => {
  resetPluginStateStoreForTests({ closeDatabase: false });
  installTelegramStateRuntimeForTest();
  sentMessageStore = createPluginStateSyncKeyedStoreForTests("telegram", {
    namespace: TELEGRAM_SENT_MESSAGE_CACHE_NAMESPACE,
    maxEntries: TELEGRAM_SENT_MESSAGE_CACHE_MAX_ENTRIES,
  });
  sentMessageStore.clear();
  setTelegramSentMessageStoreForTest(sentMessageStore);
});

function installTelegramStateRuntimeForTest(): void {
  setTelegramRuntime({
    state: {
      openKeyedStore: ((options) =>
        createPluginStateKeyedStoreForTests(
          "telegram",
          options,
        )) as TelegramRuntime["state"]["openKeyedStore"],
      openSyncKeyedStore: ((options) =>
        createPluginStateSyncKeyedStoreForTests(
          "telegram",
          options,
        )) as TelegramRuntime["state"]["openSyncKeyedStore"],
    },
    channel: {},
  } as TelegramRuntime);
}

async function expectChatNotFoundWithChatId(
  action: Promise<unknown>,
  expectedChatId: string,
): Promise<void> {
  try {
    await action;
    throw new Error("Expected action to reject with chat-not-found context");
  } catch (error) {
    if (
      error instanceof Error &&
      error.message === "Expected action to reject with chat-not-found context"
    ) {
      throw error;
    }
    const message = error instanceof Error ? error.message : String(error);
    expect(message).toMatch(/chat not found/i);
    expect(message).toMatch(new RegExp(`chat_id=${expectedChatId}`));
  }
}

async function expectTelegramMembershipErrorWithChatId(
  action: Promise<unknown>,
  expectedChatId: string,
  expectedDetail: RegExp,
): Promise<void> {
  try {
    await action;
    throw new Error("Expected action to reject with membership error context");
  } catch (error) {
    if (
      error instanceof Error &&
      error.message === "Expected action to reject with membership error context"
    ) {
      throw error;
    }
    const message = error instanceof Error ? error.message : String(error);
    expect(message).toMatch(/not a member of the chat, was blocked, or was kicked/i);
    expect(message).toMatch(expectedDetail);
    expect(message).toMatch(/Fix: Add the bot to the channel\/group/i);
    expect(message).toMatch(new RegExp(`chat_id=${expectedChatId}`));
  }
}

function mockLoadedMedia({
  buffer = Buffer.from("media"),
  contentType,
  fileName,
}: {
  buffer?: Buffer;
  contentType?: string;
  fileName?: string;
}): void {
  loadWebMedia.mockResolvedValueOnce({
    buffer,
    ...(contentType ? { contentType } : {}),
    ...(fileName ? { fileName } : {}),
  });
}

function requireMockCall<T extends unknown[]>(call: T | undefined, label: string): T {
  if (!call) {
    throw new Error(`expected ${label}`);
  }
  return call;
}

function mockCall(mock: ReturnType<typeof vi.fn>, index: number, label: string): unknown[] {
  const calls = mock.mock.calls;
  const resolvedIndex = index < 0 ? calls.length + index : index;
  return requireMockCall(calls[resolvedIndex], label);
}

function firstMockCall(mock: ReturnType<typeof vi.fn>, label: string): unknown[] {
  return mockCall(mock, 0, label);
}

function requireString(value: unknown, label: string): string {
  if (typeof value !== "string") {
    throw new Error(`expected ${label} to be a string`);
  }
  return value;
}

function requireRecord(value: unknown, label: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null) {
    throw new Error(`expected ${label}`);
  }
  return value as Record<string, unknown>;
}

function expectMediaSendCall(
  call: unknown[] | undefined,
  label: string,
  chatId: string,
  expectedParams: Record<string, unknown>,
): void {
  const [actualChatId, media, actualParams] = requireMockCall(call, label);
  expect(actualChatId).toBe(chatId);
  if (media === undefined) {
    throw new Error(`expected ${label} media`);
  }
  expect(actualParams).toEqual(expectedParams);
}

function expectPersistedTarget(fields: Record<string, unknown>): void {
  const [target] = requireMockCall(
    mockCall(maybePersistResolvedTelegramTarget, -1, "persisted Telegram target"),
    "persisted Telegram target",
  );
  const record = requireRecord(target, "persisted Telegram target");
  for (const [key, value] of Object.entries(fields)) {
    expect(record[key]).toEqual(value);
  }
}

let logCaptureCounter = 0;

function captureInfoLogs(): string {
  logCaptureCounter += 1;
  const logFile = `/tmp/openclaw-telegram-send-log-${process.pid}-${logCaptureCounter}.jsonl`;
  fs.rmSync(logFile, { force: true });
  setLoggerOverride({ level: "info", consoleLevel: "silent", file: logFile });
  return logFile;
}

function capturedLogText(logFile: string): string {
  return fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
}

afterEach(() => {
  clearTelegramRuntime();
  clearSentMessageCache();
  setTelegramSentMessageStoreForTest(undefined);
  resetPluginStateStoreForTests();
  setLoggerOverride(null);
  resetLogger();
  resetTelegramMessageCacheBucketsForTest();
  vi.restoreAllMocks();
});

describe("sent-message-cache", () => {
  afterEach(() => {
    vi.useRealTimers();
  });

  it("records and retrieves sent messages", () => {
    recordSentMessage(123, 1);
    recordSentMessage(123, 2);
    recordSentMessage(456, 10);

    expect(wasSentByBot(123, 1)).toBe(true);
    expect(wasSentByBot(123, 2)).toBe(true);
    expect(wasSentByBot(456, 10)).toBe(true);
    expect(wasSentByBot(123, 3)).toBe(false);
    expect(wasSentByBot(789, 1)).toBe(false);
  });

  it("handles string chat IDs", () => {
    recordSentMessage("123", 1);
    expect(wasSentByBot("123", 1)).toBe(true);
    expect(wasSentByBot(123, 1)).toBe(true);
  });

  it("clears cache", () => {
    recordSentMessage(123, 1);
    expect(wasSentByBot(123, 1)).toBe(true);

    clearSentMessageCache();
    expect(wasSentByBot(123, 1)).toBe(false);
  });

  it("keeps sent-message cache storage failures best-effort", () => {
    setTelegramSentMessageStoreForTest({
      ...sentMessageStore,
      entries() {
        throw new Error("read boom");
      },
      register() {
        throw new Error("write boom");
      },
    });

    expect(() => recordSentMessage(123, 1)).not.toThrow();
    expect(wasSentByBot(123, 1)).toBe(true);
  });

  it("persists sent-message rows with their remaining logical ttl", () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-01-26T12:00:00.000Z"));
    const ttlByMessageId = new Map<string, number>();
    setTelegramSentMessageStoreForTest({
      ...sentMessageStore,
      register(key, value, options) {
        sentMessageStore.register(key, value, options);
        ttlByMessageId.set(value.messageId, options?.ttlMs ?? 0);
      },
    });

    recordSentMessage(123, 1);
    vi.advanceTimersByTime(60 * 60 * 1000);
    recordSentMessage(123, 2);

    expect(ttlByMessageId.get("1")).toBe(23 * 60 * 60 * 1000);
    expect(ttlByMessageId.get("2")).toBe(24 * 60 * 60 * 1000);
  });

  it("keeps sent-message ownership across restart", async () => {
    const persistedStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-restart.json`;
    const sentMessageCfg = { session: { store: persistedStorePath } };

    recordSentMessage(123, 1, sentMessageCfg);
    expect(wasSentByBot(123, 1, sentMessageCfg)).toBe(true);

    resetSentMessageCacheForTest();

    const restartedCache = await importFreshModule<typeof import("./sent-message-cache.js")>(
      import.meta.url,
      "./sent-message-cache.js?scope=restart",
    );
    restartedCache.setTelegramSentMessageStoreForTest(sentMessageStore);

    try {
      expect(restartedCache.wasSentByBot(123, 1, sentMessageCfg)).toBe(true);
    } finally {
      restartedCache.clearSentMessageCache();
      restartedCache.setTelegramSentMessageStoreForTest(undefined);
    }
  });

  it("keeps expired custom-store cleanup away from the default store", () => {
    const customStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-custom-cleanup.json`;
    const customCfg = { session: { store: customStorePath } };
    const startedAt = new Date("2026-01-01T00:00:00.000Z");
    vi.useFakeTimers();
    vi.setSystemTime(startedAt);

    try {
      recordSentMessage(123, 2, customCfg);

      vi.setSystemTime(startedAt.getTime() + 24 * 60 * 60 * 1000 + 1);
      recordSentMessage(123, 1);

      expect(wasSentByBot(123, 2, customCfg)).toBe(false);
      expect(wasSentByBot(123, 1)).toBe(true);
    } finally {
      fs.rmSync(customStorePath, { force: true });
      fs.rmSync(`${customStorePath}.telegram-sent-messages.json`, { force: true });
    }
  });

  it("keeps default and custom stores isolated while both are loaded", () => {
    const customStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-custom-isolated.json`;
    const customCfg = { session: { store: customStorePath } };

    try {
      recordSentMessage(123, 1);
      recordSentMessage(123, 2, customCfg);

      expect(wasSentByBot(123, 1)).toBe(true);
      expect(wasSentByBot(123, 2)).toBe(false);
      expect(wasSentByBot(123, 1, customCfg)).toBe(false);
      expect(wasSentByBot(123, 2, customCfg)).toBe(true);
    } finally {
      fs.rmSync(customStorePath, { force: true });
      fs.rmSync(`${customStorePath}.telegram-sent-messages.json`, { force: true });
    }
  });

  it("shares sent-message state across distinct module instances", async () => {
    const cacheA = await importFreshModule<typeof import("./sent-message-cache.js")>(
      import.meta.url,
      "./sent-message-cache.js?scope=shared-a",
    );
    const cacheB = await importFreshModule<typeof import("./sent-message-cache.js")>(
      import.meta.url,
      "./sent-message-cache.js?scope=shared-b",
    );
    cacheA.setTelegramSentMessageStoreForTest(sentMessageStore);
    cacheB.setTelegramSentMessageStoreForTest(sentMessageStore);

    cacheA.clearSentMessageCache();

    try {
      cacheA.recordSentMessage(123, 1);
      expect(cacheB.wasSentByBot(123, 1)).toBe(true);

      cacheB.clearSentMessageCache();
      expect(cacheA.wasSentByBot(123, 1)).toBe(false);
    } finally {
      cacheA.clearSentMessageCache();
      cacheA.setTelegramSentMessageStoreForTest(undefined);
      cacheB.setTelegramSentMessageStoreForTest(undefined);
    }
  });
});

describe("buildInlineKeyboard", () => {
  it("normalizes keyboard inputs", () => {
    const cases: Array<{
      name: string;
      input: Parameters<typeof buildInlineKeyboard>[0];
      expected: ReturnType<typeof buildInlineKeyboard>;
    }> = [
      {
        name: "empty input",
        input: undefined,
        expected: undefined,
      },
      {
        name: "empty rows",
        input: [],
        expected: undefined,
      },
      {
        name: "valid rows",
        input: [
          [{ text: "Option A", callback_data: "cmd:a" }],
          [
            { text: "Option B", callback_data: "cmd:b" },
            { text: "Option C", callback_data: "cmd:c" },
          ],
        ],
        expected: {
          inline_keyboard: [
            [{ text: "Option A", callback_data: "cmd:a" }],
            [
              { text: "Option B", callback_data: "cmd:b" },
              { text: "Option C", callback_data: "cmd:c" },
            ],
          ],
        },
      },
      {
        name: "keeps button style fields",
        input: [
          [
            {
              text: "Option A",
              callback_data: "cmd:a",
              style: "primary",
            },
          ],
        ],
        expected: {
          inline_keyboard: [
            [
              {
                text: "Option A",
                callback_data: "cmd:a",
                style: "primary",
              },
            ],
          ],
        },
      },
      {
        name: "keeps url buttons",
        input: [[{ text: "Open", url: "https://example.com" }]],
        expected: {
          inline_keyboard: [[{ text: "Open", url: "https://example.com" }]],
        },
      },
      {
        name: "keeps web app buttons",
        input: [[{ text: "Launch", web_app: { url: "https://example.com/app" } }]],
        expected: {
          inline_keyboard: [[{ text: "Launch", web_app: { url: "https://example.com/app" } }]],
        },
      },
      {
        name: "prefers url over callback data when both are present",
        input: [[{ text: "Open", callback_data: "cmd:open", url: "https://example.com" }]],
        expected: {
          inline_keyboard: [[{ text: "Open", url: "https://example.com" }]],
        },
      },
      {
        name: "filters invalid buttons and empty rows",
        input: [
          [
            { text: "", callback_data: "cmd:skip" },
            { text: "Ok", callback_data: "cmd:ok" },
          ],
          [{ text: "Missing data", callback_data: "" }],
          [{ text: "Missing action" }],
          [],
        ],
        expected: {
          inline_keyboard: [[{ text: "Ok", callback_data: "cmd:ok" }]],
        },
      },
    ];
    for (const testCase of cases) {
      const input = testCase.input?.map((row) => row.map((button) => ({ ...button })));
      expect(buildInlineKeyboard(input), testCase.name).toEqual(testCase.expected);
    }
  });
});

describe("sendMessageTelegram", () => {
  it("sends typing to the resolved chat and topic", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.sendChatAction.mockResolvedValue(true);

    await sendTypingTelegram("telegram:group:-1001234567890:topic:271", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
    });

    expect(botApi.sendChatAction).toHaveBeenCalledWith("-1001234567890", "typing", {
      message_thread_id: 271,
    });
  });

  it("pins and unpins Telegram messages", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.pinChatMessage.mockResolvedValue(true);
    botApi.unpinChatMessage.mockResolvedValue(true);

    await pinMessageTelegram("-1001234567890", 101, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
    });
    await unpinMessageTelegram("-1001234567890", 101, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
    });

    expect(botApi.pinChatMessage).toHaveBeenCalledWith("-1001234567890", 101, {
      disable_notification: true,
    });
    expect(botApi.unpinChatMessage).toHaveBeenCalledWith("-1001234567890", 101);
  });

  it("honors Telegram pin notification requests", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.pinChatMessage.mockResolvedValue(true);

    await pinMessageTelegram("-1001234567890", 101, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
      notify: true,
    });

    expect(botApi.pinChatMessage).toHaveBeenCalledWith("-1001234567890", 101, {
      disable_notification: false,
    });
  });

  it("renames a Telegram forum topic", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.editForumTopic.mockResolvedValue(true);

    await renameForumTopicTelegram("-1001234567890", 271, "Codex Thread", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
    });

    expect(botApi.editForumTopic).toHaveBeenCalledWith("-1001234567890", 271, {
      name: "Codex Thread",
    });
  });

  it("edits a Telegram forum topic name and icon via the shared helper", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.editForumTopic.mockResolvedValue(true);

    await editForumTopicTelegram("-1001234567890", 271, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
      name: "Codex Thread",
      iconCustomEmojiId: "emoji-123",
    });

    expect(botApi.editForumTopic).toHaveBeenCalledWith("-1001234567890", 271, {
      name: "Codex Thread",
      icon_custom_emoji_id: "emoji-123",
    });
  });

  it("strips topic suffixes before editing a Telegram forum topic", async () => {
    loadConfig.mockReturnValue({
      channels: {
        telegram: {
          botToken: "tok",
        },
      },
    });
    botApi.editForumTopic.mockResolvedValue(true);

    await editForumTopicTelegram("telegram:group:-1001234567890:topic:271", 271, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "default",
      name: "Codex Thread",
    });

    expect(botApi.editForumTopic).toHaveBeenCalledWith("-1001234567890", 271, {
      name: "Codex Thread",
    });
  });

  it("rejects empty topic edits", async () => {
    await expect(
      editForumTopicTelegram("-1001234567890", 271, {
        cfg: TELEGRAM_TEST_CFG,
        accountId: "default",
      }),
    ).rejects.toThrow("Telegram forum topic update requires a name or iconCustomEmojiId");
    await expect(
      editForumTopicTelegram("-1001234567890", 271, {
        cfg: TELEGRAM_TEST_CFG,
        accountId: "default",
        iconCustomEmojiId: "   ",
      }),
    ).rejects.toThrow("Telegram forum topic icon custom emoji ID is required");
  });

  it("applies timeoutSeconds config precedence", async () => {
    const cases = [
      {
        name: "global telegram timeout",
        cfg: { channels: { telegram: { timeoutSeconds: 60 } } },
        opts: { cfg: TELEGRAM_TEST_CFG, token: "tok" },
        expectedTimeout: 60,
      },
      {
        name: "per-account timeout override",
        cfg: {
          channels: {
            telegram: {
              timeoutSeconds: 60,
              accounts: { foo: { timeoutSeconds: 61 } },
            },
          },
        },
        opts: { cfg: TELEGRAM_TEST_CFG, token: "tok", accountId: "foo" },
        expectedTimeout: 61,
      },
    ] as const;
    for (const testCase of cases) {
      botCtorSpy.mockClear();
      loadConfig.mockReturnValue(testCase.cfg);
      botApi.sendMessage.mockResolvedValue({
        message_id: 1,
        chat: { id: "123" },
      });
      await sendMessageTelegram("123", "hi", { ...testCase.opts, cfg: testCase.cfg });
      const [token, options] = firstMockCall(botCtorSpy, "bot constructor call");
      expect(token, testCase.name).toBe("tok");
      const client = requireRecord(requireRecord(options, "bot options").client, "bot client");
      expect(client.timeoutSeconds, testCase.name).toBe(testCase.expectedTimeout);
    }
  });

  it("normalizes full Telegram bot endpoint apiRoot before send clients reach grammY", async () => {
    const cfg = {
      channels: {
        telegram: {
          accounts: {
            foo: {
              apiRoot: "https://api.telegram.org/bot123456:ABC/",
            },
          },
        },
      },
    };
    loadConfig.mockReturnValue(cfg);
    botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await sendMessageTelegram("123", "hi", { cfg, token: "tok", accountId: "foo" });

    const [token, options] = firstMockCall(botCtorSpy, "bot constructor call");
    expect(token).toBe("tok");
    const client = requireRecord(requireRecord(options, "bot options").client, "bot client");
    expect(client.apiRoot).toBe("https://api.telegram.org");
  });

  it("installs the shared grammY throttler on send clients", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await sendMessageTelegram("123", "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok" });

    const [middleware] = firstMockCall(botConfigUseSpy, "bot config use call");
    expect(middleware).toBeTypeOf("function");
  });

  it("records sent text messages into the Telegram prompt context cache", async () => {
    const storePath = `/tmp/openclaw-telegram-send-context-${process.pid}-${Date.now()}.json`;
    const cfg = { session: { store: storePath } };
    botApi.sendMessage.mockResolvedValueOnce({
      message_id: 1497,
      date: 1_779_394_740,
      chat: {
        id: "-1003966283270",
        type: "supergroup",
        title: "Keshav and Kelaw - Keshav's Bot",
      },
      from: { id: 42, is_bot: true, first_name: "Kelaw", username: "keshavbotagent" },
      text: "Done already: timeoutSeconds is now 7200s.",
      message_thread_id: 1154,
    });

    await sendMessageTelegram("-1003966283270", "Done already: timeoutSeconds is now 7200s.", {
      cfg,
      token: "tok",
      messageThreadId: 1154,
    });

    const cache = createTelegramMessageCache({
      scope: resolveTelegramMessageCacheScope(storePath),
    });
    await cache.record({
      accountId: "default",
      chatId: "-1003966283270",
      threadId: 1154,
      msg: {
        chat: {
          id: -1003966283270,
          type: "supergroup",
          title: "Keshav and Kelaw - Keshav's Bot",
        },
        message_thread_id: 1154,
        message_id: 1521,
        date: 1_779_425_460,
        text: "Did all Amazon crons run fine",
        from: { id: 5185575566, is_bot: false, first_name: "Keshav" },
      },
    });

    const context = await buildTelegramConversationContext({
      cache,
      accountId: "default",
      chatId: "-1003966283270",
      threadId: 1154,
      messageId: "1521",
      replyChainNodes: [],
      recentLimit: 10,
      replyTargetWindowSize: 2,
    });

    expect(context.map((entry) => entry.node.messageId)).toContain("1497");
    expect(context.map((entry) => entry.node.body)).toContain(
      "Done already: timeoutSeconds is now 7200s.",
    );
  });

  it("normalizes raw code language HTML before sending", async () => {
    const chatId = "123";
    const text = [
      "Yep. Send these in order:",
      "",
      '<code class="language-text">/queue followup debounce:0',
      "</code>",
    ].join("\n");
    const sendMessage = vi.fn().mockResolvedValue({ message_id: 44, chat: { id: chatId } });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    const res = await sendMessageTelegram(chatId, text, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(sendMessage).toHaveBeenCalledWith(
      chatId,
      ["Yep. Send these in order:", "", "<code>/queue followup debounce:0", "</code>"].join("\n"),
      { parse_mode: "HTML" },
    );
    expect(res.chatId).toBe(chatId);
    expect(res.messageId).toBe("44");
  });

  it("skips rich entity detection when link previews are disabled", async () => {
    const cases = [
      {
        name: "html send succeeds",
        text: "hi",
        sendMessage: vi.fn().mockResolvedValue({ message_id: 7, chat: { id: "123" } }),
        expectedCalls: [["123", "hi", { parse_mode: "HTML", skip_entity_detection: true }]],
      },
    ] as const;
    for (const testCase of cases) {
      const cfg = {
        channels: { telegram: { linkPreview: false } },
      };
      loadConfig.mockReturnValue(cfg);
      const api = { sendMessage: testCase.sendMessage } as unknown as {
        sendMessage: typeof testCase.sendMessage;
      };
      await sendMessageTelegram("123", testCase.text, {
        cfg,
        token: "tok",
        api,
      });
      expect(testCase.sendMessage.mock.calls, testCase.name).toEqual(testCase.expectedCalls);
    }
  });

  it("sends raw rich markdown for durable text", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 45, chat: { id: "123" } });

    await sendMessageTelegram("123", "**hi**", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown: "**hi**" },
    });
  });

  it("keeps complex markdown raw for rich message text", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 46, chat: { id: "123" } });
    const markdown = [
      "# Heading",
      "",
      "| Name | Value |",
      "| --- | --- |",
      "| **bold** | _italic_ |",
      "",
      "> quoted `code`",
      "",
      "||spoiler|| and [link](https://example.com)",
    ].join("\n");

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("keeps markdown media syntax on the text-only rich path", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 47, chat: { id: "123" } });

    await sendMessageTelegram("123", "See ![diagram](https://example.com/diagram.png)", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown: "See [diagram](https://example.com/diagram.png)" },
    });
  });

  it("escapes HTML media tags on the text-only rich path", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 48, chat: { id: "123" } });

    await sendMessageTelegram("123", '<b>See</b><img src="https://example.com/diagram.png">', {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      textMode: "html",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: {
        html: "<b>See</b>&lt;img src=&quot;https://example.com/diagram.png&quot;&gt;",
      },
    });
  });

  it("keeps native rich markdown tables within Telegram's column limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 49, chat: { id: "123" } });
    const markdown = markdownTable(20);

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("wraps wide rich markdown tables that exceed Telegram's column limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 50, chat: { id: "123" } });
    const markdown = markdownTable(21);

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown: `\`\`\`\n${markdown}\n\`\`\`` },
    });
  });

  it("leaves wide rich markdown tables alone inside fences", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 51, chat: { id: "123" } });
    const markdown = `~~~\n${markdownTable(25)}\n~~~`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("wraps only wide rich markdown tables outside fences", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 52, chat: { id: "123" } });
    const fencedTable = markdownTable(25);
    const outsideTable = markdownTable(21);
    const markdown = ["Before", "~~~", fencedTable, "~~~", "After", outsideTable].join("\n");

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: {
        markdown: ["Before", "~~~", fencedTable, "~~~", "After", "```", outsideTable, "```"].join(
          "\n",
        ),
      },
    });
  });

  it("sends long rich markdown as one message", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 53, chat: { id: "123" } });
    const markdown = `# Long\n\n${"**section** with _style_ and `code`\n".repeat(800)}`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("chunks rich markdown above the Bot API rich message limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 54, chat: { id: "123" } });
    const markdown = `# Long\n\n${"**section** with _style_ and `code`\n".repeat(3000)}`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage.mock.calls.length).toBeGreaterThan(1);
    const chunks = richSendCallParams().map((params) => params.rich_message?.markdown ?? "");
    const joinedChunks = chunks.join("\n");
    expect(joinedChunks.startsWith("# Long")).toBe(true);
    expect(joinedChunks.match(/\*\*section\*\* with _style_ and `code`/g)?.length).toBe(3000);
    expect(chunks.every((chunk) => chunk.length <= 32_768)).toBe(true);
  });

  it("chunks long inline rich markdown without converting to HTML", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 52, chat: { id: "123" } });
    const markdown = `**${"A".repeat(70_000)}**`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    const chunks = richSendCallParams().map((params) => params.rich_message);
    expect(chunks.length).toBeGreaterThan(1);
    expect(chunks.every((chunk) => chunk?.html === undefined)).toBe(true);
    expect(chunks.every((chunk) => (chunk?.markdown ?? "").length <= 32_768)).toBe(true);
    expect(chunks.map((chunk) => chunk?.markdown ?? "").join("")).toBe(markdown);
  });

  it("chunks rich markdown above Telegram's rich block limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 53, chat: { id: "123" } });
    const markdown = Array.from({ length: 900 }, (_, index) => `Paragraph ${index + 1}`).join(
      "\n\n",
    );

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    const chunks = richSendCallParams().map((params) => params.rich_message?.markdown ?? "");
    expect(chunks).toHaveLength(2);
    expect(
      chunks.every(
        (chunk) => chunk.split(/\n[\t ]*\n+/).filter((block) => block.trim()).length <= 500,
      ),
    ).toBe(true);
    expect(chunks.join("\n\n")).toBe(markdown);
  });

  it("chunks rich markdown headings above Telegram's rich block limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 54, chat: { id: "123" } });
    const markdown = Array.from({ length: 600 }, (_, index) => `# Heading ${index + 1}`).join("\n");

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    const chunks = richSendCallParams().map((params) => params.rich_message?.markdown ?? "");
    expect(chunks).toHaveLength(2);
    expect(chunks.at(0)?.match(/^# /gm)).toHaveLength(500);
    expect(chunks.at(1)?.match(/^# /gm)).toHaveLength(100);
    expect(chunks.join("\n")).toBe(markdown);
  });

  it("keeps long rich markdown lists intact", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 55, chat: { id: "123" } });
    const markdown = Array.from({ length: 600 }, (_, index) => `- Item ${index + 1}`).join("\n");

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("keeps tall rich markdown tables intact", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 56, chat: { id: "123" } });
    const markdown = [
      "| Name | Value |",
      "| --- | --- |",
      ...Array.from({ length: 600 }, (_, index) => `| Row ${index + 1} | ${index + 1} |`),
    ].join("\n");

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("does not split rich block chunks on blank lines inside fences", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 57, chat: { id: "123" } });
    const markdown = `~~~txt\n${Array.from({ length: 900 }, (_, index) => `line ${index + 1}`).join(
      "\n\n",
    )}\n~~~`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("does not split rich heading chunks inside fences", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 58, chat: { id: "123" } });
    const markdown = `~~~md\n${Array.from(
      { length: 600 },
      (_, index) => `# Literal heading ${index + 1}`,
    ).join("\n")}\n~~~`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
    expect(botRawApi.sendRichMessage).toHaveBeenCalledWith({
      chat_id: "123",
      rich_message: { markdown },
    });
  });

  it("chunks long rich markdown fences into bounded markdown chunks", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 59, chat: { id: "123" } });
    const markdown = `~~~ts\n${"const value = 1;\n".repeat(5000)}~~~`;

    await sendMessageTelegram("123", markdown, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
    });

    const chunks = richSendCallParams().map((params) => params.rich_message?.markdown ?? "");
    expect(chunks.length).toBeGreaterThan(1);
    expect(chunks.every((chunk) => chunk.length <= 32_768)).toBe(true);
    expect(chunks.every((chunk) => chunk.startsWith("~~~ts\n"))).toBe(true);
    expect(chunks.every((chunk) => chunk.endsWith("\n~~~") || chunk.endsWith("~~~"))).toBe(true);
  });

  it("chunks explicit rich HTML above the Bot API rich message limit", async () => {
    botApi.sendMessage.mockResolvedValue({ message_id: 60, chat: { id: "123" } });
    const html = `<b>${"A".repeat(70_000)}</b>`;

    await sendMessageTelegram("123", html, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      textMode: "html",
      buttons: [[{ text: "OK", callback_data: "ok" }]],
    });

    expect(botRawApi.sendRichMessage.mock.calls.length).toBeGreaterThan(1);
    const calls = richSendCallParams();
    expect(calls.every((params) => (params.rich_message?.html ?? "").length <= 32_768)).toBe(true);
    expect(calls.at(0)?.rich_message?.html).toMatch(/^<b>A/);
    expect(calls.at(-1)?.rich_message?.html).toMatch(/A<\/b>$/);
    expect(calls.slice(0, -1).every((params) => params.reply_markup === undefined)).toBe(true);
    expect(calls.at(-1)?.reply_markup).toEqual({
      inline_keyboard: [[{ text: "OK", callback_data: "ok" }]],
    });
  });

  it("fails when Telegram text send returns no message_id", async () => {
    const sendMessage = vi.fn().mockResolvedValue({
      chat: { id: "123" },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await expect(
      sendMessageTelegram("123", "hi", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      }),
    ).rejects.toThrow(/returned no message_id/i);
  });

  it("fails when Telegram media send returns no message_id", async () => {
    mockLoadedMedia({ contentType: "image/png", fileName: "photo.png" });
    const sendPhoto = vi.fn().mockResolvedValue({
      chat: { id: "123" },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    await expect(
      sendMessageTelegram("123", "caption", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: "https://example.com/photo.png",
      }),
    ).rejects.toThrow(/returned no message_id/i);
  });

  it("uses native fetch for BAN compatibility when api is omitted", async () => {
    const originalFetch = globalThis.fetch;
    const originalBun = (globalThis as { Bun?: unknown }).Bun;
    const fetchSpy = vi.fn() as unknown as typeof fetch;
    globalThis.fetch = fetchSpy;
    (globalThis as { Bun?: unknown }).Bun = {};
    botApi.sendMessage.mockResolvedValue({
      message_id: 1,
      chat: { id: "123" },
    });
    try {
      await sendMessageTelegram("123", "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok" });
      const clientFetch = (
        firstMockCall(botCtorSpy, "bot constructor call")[1] as {
          client?: { fetch?: unknown };
        }
      )?.client?.fetch;
      expect(clientFetch).toBeTypeOf("function");
      expect(clientFetch).not.toBe(fetchSpy);
    } finally {
      globalThis.fetch = originalFetch;
      if (originalBun === undefined) {
        delete (globalThis as { Bun?: unknown }).Bun;
      } else {
        (globalThis as { Bun?: unknown }).Bun = originalBun;
      }
    }
  });

  it("normalizes chat ids with internal prefixes", async () => {
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 1,
      chat: { id: "123" },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await sendMessageTelegram("telegram:123", "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(sendMessage).toHaveBeenCalledWith("123", "hi", {
      parse_mode: "HTML",
    });
  });

  it("resolves t.me targets to numeric chat ids via getChat", async () => {
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 1,
      chat: { id: "-100123" },
    });
    const getChat = vi.fn().mockResolvedValue({ id: -100123 });
    const api = { sendMessage, getChat } as unknown as {
      sendMessage: typeof sendMessage;
      getChat: typeof getChat;
    };

    await sendMessageTelegram("https://t.me/mychannel", "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      gatewayClientScopes: ["operator.write"],
    });

    expect(getChat).toHaveBeenCalledWith("@mychannel");
    expect(sendMessage).toHaveBeenCalledWith("-100123", "hi", {
      parse_mode: "HTML",
    });
    expectPersistedTarget({
      rawTarget: "https://t.me/mychannel",
      resolvedChatId: "-100123",
      gatewayClientScopes: ["operator.write"],
    });
  });

  it("preserves internal target writeback when gateway scopes are absent", async () => {
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 1,
      chat: { id: "-100123" },
    });
    const getChat = vi.fn().mockResolvedValue({ id: -100123 });
    const api = { sendMessage, getChat } as unknown as {
      sendMessage: typeof sendMessage;
      getChat: typeof getChat;
    };

    await sendMessageTelegram("https://t.me/mychannel", "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(getChat).toHaveBeenCalledWith("@mychannel");
    expectPersistedTarget({
      rawTarget: "https://t.me/mychannel",
      resolvedChatId: "-100123",
      gatewayClientScopes: undefined,
      trustedInternalWriteback: true,
    });
  });

  it("fails clearly when a legacy target cannot be resolved", async () => {
    const getChat = vi.fn().mockRejectedValue(new Error("400: Bad Request: chat not found"));
    const api = { getChat } as unknown as {
      getChat: typeof getChat;
    };

    await expect(
      sendMessageTelegram("@missingchannel", "hi", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      }),
    ).rejects.toThrow(/could not be resolved to a numeric chat ID/i);
  });

  it("includes thread params in media messages", async () => {
    const chatId = "-1001234567890";
    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 58,
      chat: { id: chatId },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    await sendMessageTelegram(chatId, "photo in topic", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
      messageThreadId: 99,
    });

    expectMediaSendCall(firstMockCall(sendPhoto, "send photo call"), "send photo call", chatId, {
      caption: "photo in topic",
      parse_mode: "HTML",
      message_thread_id: 99,
    });
  });

  it("splits long captions into media + text messages when text exceeds 1024 chars", async () => {
    const chatId = "123";
    const longText = "A".repeat(1100);

    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 70,
      chat: { id: chatId },
    });
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 71,
      chat: { id: chatId },
    });
    const api = { sendPhoto, sendMessage } as unknown as {
      sendPhoto: typeof sendPhoto;
      sendMessage: typeof sendMessage;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    const res = await sendMessageTelegram(chatId, longText, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    expectMediaSendCall(firstMockCall(sendPhoto, "send photo call"), "send photo call", chatId, {
      caption: undefined,
    });
    expect(sendMessage).toHaveBeenCalledWith(chatId, longText, {
      parse_mode: "HTML",
    });
    expect(res.messageId).toBe("71");
  });

  it("chunks long default markdown media follow-up text", async () => {
    const chatId = "123";
    const longText = `**${"A".repeat(5000)}**`;

    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 72,
      chat: { id: chatId },
    });
    const sendMessage = vi.fn().mockResolvedValue({ message_id: 74, chat: { id: chatId } });
    const api = { sendPhoto, sendMessage } as unknown as {
      sendPhoto: typeof sendPhoto;
      sendMessage: typeof sendMessage;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    const res = await sendMessageTelegram(chatId, longText, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    expectMediaSendCall(firstMockCall(sendPhoto, "send photo call"), "send photo call", chatId, {
      caption: undefined,
    });
    expect(sendMessage).toHaveBeenCalledTimes(1);
    expect(sendMessage.mock.calls.every((call) => call[2]?.parse_mode === "HTML")).toBe(true);
    expect(sendMessage.mock.calls.map((call) => String(call[1] ?? "")).join("")).toContain("A");
    expect(res.messageId).toBe("74");
  });

  it("uses caption when text is within 1024 char limit", async () => {
    const chatId = "123";
    const shortText = "B".repeat(1024);

    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 72,
      chat: { id: chatId },
    });
    const sendMessage = vi.fn();
    const api = { sendPhoto, sendMessage } as unknown as {
      sendPhoto: typeof sendPhoto;
      sendMessage: typeof sendMessage;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    const res = await sendMessageTelegram(chatId, shortText, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    expectMediaSendCall(firstMockCall(sendPhoto, "send photo call"), "send photo call", chatId, {
      caption: shortText,
      parse_mode: "HTML",
    });
    expect(sendMessage).not.toHaveBeenCalled();
    expect(res.messageId).toBe("72");
  });

  it("renders markdown in media captions", async () => {
    const chatId = "123";
    const caption = "hi **boss**";

    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 90,
      chat: { id: chatId },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    await sendMessageTelegram(chatId, caption, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    expectMediaSendCall(firstMockCall(sendPhoto, "send photo call"), "send photo call", chatId, {
      caption: "hi <b>boss</b>",
      parse_mode: "HTML",
    });
  });

  it("sends video notes when requested and regular videos otherwise", async () => {
    const chatId = "123";

    {
      const text = "ignored caption context";
      const sendVideoNote = vi.fn().mockResolvedValue({
        message_id: 101,
        chat: { id: chatId },
      });
      const sendMessage = vi.fn().mockResolvedValue({
        message_id: 102,
        chat: { id: chatId },
      });
      const api = { sendVideoNote, sendMessage } as unknown as {
        sendVideoNote: typeof sendVideoNote;
        sendMessage: typeof sendMessage;
      };

      mockLoadedMedia({
        buffer: Buffer.from("fake-video"),
        contentType: "video/mp4",
        fileName: "video.mp4",
      });

      const res = await sendMessageTelegram(chatId, text, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: "https://example.com/video.mp4",
        asVideoNote: true,
      });

      expectMediaSendCall(
        firstMockCall(sendVideoNote, "send video note call"),
        "send video note call",
        chatId,
        {},
      );
      expect(sendMessage).toHaveBeenCalledWith(chatId, text, {
        parse_mode: "HTML",
      });
      expect(res.messageId).toBe("102");
    }

    {
      const text = "my caption";
      const sendVideo = vi.fn().mockResolvedValue({
        message_id: 201,
        chat: { id: chatId },
      });
      const api = { sendVideo } as unknown as {
        sendVideo: typeof sendVideo;
      };

      mockLoadedMedia({
        buffer: Buffer.from("fake-video"),
        contentType: "video/mp4",
        fileName: "video.mp4",
      });

      const res = await sendMessageTelegram(chatId, text, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: "https://example.com/video.mp4",
        asVideoNote: false,
      });

      const [actualChatId, media, videoParams] = firstMockCall(sendVideo, "send video call");
      expect(actualChatId).toBe(chatId);
      if (media === undefined) {
        throw new Error("expected send video media");
      }
      const params = requireRecord(videoParams, "send video params");
      expect(typeof params.caption).toBe("string");
      expect(params.parse_mode).toBe("HTML");
      expect(Object.keys(params).toSorted()).toEqual(["caption", "parse_mode"]);
      expect(res.messageId).toBe("201");
    }
  });

  it("passes probed dimensions to regular video sends", async () => {
    const chatId = "123";
    const videoBuffer = Buffer.from("fake-video");
    const sendVideo = vi.fn().mockResolvedValue({
      message_id: 201,
      chat: { id: chatId },
    });
    const api = { sendVideo } as unknown as {
      sendVideo: typeof sendVideo;
    };
    probeVideoDimensions.mockResolvedValueOnce({ width: 720, height: 1280 });

    mockLoadedMedia({
      buffer: videoBuffer,
      contentType: "video/mp4",
      fileName: "video.mp4",
    });

    await sendMessageTelegram(chatId, "my caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/video.mp4",
    });

    expect(probeVideoDimensions).toHaveBeenCalledWith(videoBuffer);
    expectMediaSendCall(firstMockCall(sendVideo, "send video call"), "send video call", chatId, {
      caption: "my caption",
      parse_mode: "HTML",
      width: 720,
      height: 1280,
    });
  });

  it("does not probe video dimensions for video notes", async () => {
    const chatId = "123";
    const sendVideoNote = vi.fn().mockResolvedValue({
      message_id: 101,
      chat: { id: chatId },
    });
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 102,
      chat: { id: chatId },
    });
    const api = { sendVideoNote, sendMessage } as unknown as {
      sendVideoNote: typeof sendVideoNote;
      sendMessage: typeof sendMessage;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-video"),
      contentType: "video/mp4",
      fileName: "video.mp4",
    });

    await sendMessageTelegram(chatId, "ignored caption context", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/video.mp4",
      asVideoNote: true,
    });

    expect(probeVideoDimensions).not.toHaveBeenCalled();
    expectMediaSendCall(
      firstMockCall(sendVideoNote, "send video note call"),
      "send video note call",
      chatId,
      {},
    );
  });

  it("applies reply markup and thread options to split video-note sends", async () => {
    const chatId = "123";
    const cases: Array<{
      text: string;
      options: Partial<NonNullable<Parameters<typeof sendMessageTelegram>[2]>>;
      expectedVideoNote: Record<string, unknown>;
      expectedMessage: Record<string, unknown>;
    }> = [
      {
        text: "Check this out",
        options: {
          buttons: [[{ text: "Btn", callback_data: "dat" }]],
        },
        expectedVideoNote: {},
        expectedMessage: {
          parse_mode: "HTML",
          reply_markup: {
            inline_keyboard: [[{ text: "Btn", callback_data: "dat" }]],
          },
        },
      },
      {
        text: "Threaded reply",
        options: {
          replyToMessageId: 999,
        },
        expectedVideoNote: { reply_to_message_id: 999, allow_sending_without_reply: true },
        expectedMessage: {
          parse_mode: "HTML",
          reply_parameters: {
            message_id: 999,
            allow_sending_without_reply: true,
          },
        },
      },
    ];

    for (const testCase of cases) {
      const sendVideoNote = vi.fn().mockResolvedValue({
        message_id: 301,
        chat: { id: chatId },
      });
      const sendMessage = vi.fn().mockResolvedValue({
        message_id: 302,
        chat: { id: chatId },
      });
      const api = { sendVideoNote, sendMessage } as unknown as {
        sendVideoNote: typeof sendVideoNote;
        sendMessage: typeof sendMessage;
      };

      mockLoadedMedia({
        buffer: Buffer.from("fake-video"),
        contentType: "video/mp4",
        fileName: "video.mp4",
      });

      const sendOptions: NonNullable<Parameters<typeof sendMessageTelegram>[2]> = {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: "https://example.com/video.mp4",
        asVideoNote: true,
      };
      if (
        "replyToMessageId" in testCase.options &&
        testCase.options.replyToMessageId !== undefined
      ) {
        sendOptions.replyToMessageId = testCase.options.replyToMessageId;
      }
      if ("buttons" in testCase.options && testCase.options.buttons) {
        sendOptions.buttons = testCase.options.buttons;
      }
      await sendMessageTelegram(chatId, testCase.text, sendOptions);

      expectMediaSendCall(
        firstMockCall(sendVideoNote, "send video note call"),
        "send video note call",
        chatId,
        testCase.expectedVideoNote,
      );
      expect(sendMessage).toHaveBeenCalledWith(chatId, testCase.text, testCase.expectedMessage);
    }
  });

  it("retries pre-connect send errors and honors retry_after when present", async () => {
    vi.useFakeTimers();
    const chatId = "123";
    const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.telegram.org"), {
      code: "ENOTFOUND",
      parameters: { retry_after: 0.5 },
    });
    const sendMessage = vi
      .fn()
      .mockRejectedValueOnce(err)
      .mockResolvedValueOnce({
        message_id: 1,
        chat: { id: chatId },
      });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };
    const setTimeoutSpy = vi.spyOn(global, "setTimeout");

    const promise = sendMessageTelegram(chatId, "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
    });

    await vi.runAllTimersAsync();
    await expect(promise).resolves.toEqual({ messageId: "1", chatId });
    expect(firstMockCall(setTimeoutSpy, "setTimeout call")[1]).toBe(500);
    setTimeoutSpy.mockRestore();
    vi.useRealTimers();
  });

  it("retries wrapped pre-connect HttpError sends", async () => {
    vi.useFakeTimers();
    const chatId = "123";
    const root = Object.assign(new Error("connect ECONNREFUSED api.telegram.org"), {
      code: "ECONNREFUSED",
    });
    const fetchError = Object.assign(new TypeError("fetch failed"), { cause: root });
    const err = Object.assign(new Error("Network request for 'sendMessage' failed!"), {
      name: "HttpError",
      error: fetchError,
    });
    const sendMessage = vi
      .fn()
      .mockRejectedValueOnce(err)
      .mockResolvedValueOnce({
        message_id: 1,
        chat: { id: chatId },
      });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    const promise = sendMessageTelegram(chatId, "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
    });

    await vi.runAllTimersAsync();
    await expect(promise).resolves.toEqual({ messageId: "1", chatId });
    expect(sendMessage).toHaveBeenCalledTimes(2);
    vi.useRealTimers();
  });

  it("does not retry on non-transient errors", async () => {
    const chatId = "123";
    const sendMessage = vi.fn().mockRejectedValue(new Error("400: Bad Request"));
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await expect(
      sendMessageTelegram(chatId, "hi", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
      }),
    ).rejects.toThrow(/Bad Request/);
    expect(sendMessage).toHaveBeenCalledTimes(1);
  });

  it("does not retry generic grammY failed-after envelopes for non-idempotent sends", async () => {
    const chatId = "123";
    const sendMessage = vi
      .fn()
      .mockRejectedValueOnce(
        new Error("Network request for 'sendMessage' failed after 1 attempts."),
      );
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await expect(
      sendMessageTelegram(chatId, "hi", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
      }),
    ).rejects.toThrow(/failed after 1 attempts/i);
    expect(sendMessage).toHaveBeenCalledTimes(1);
  });

  it("sends GIF media as animation", async () => {
    const chatId = "123";
    const sendAnimation = vi.fn().mockResolvedValue({
      message_id: 9,
      chat: { id: chatId },
    });
    const api = { sendAnimation } as unknown as {
      sendAnimation: typeof sendAnimation;
    };

    mockLoadedMedia({
      buffer: Buffer.from("GIF89a"),
      fileName: "fun.gif",
    });

    const res = await sendMessageTelegram(chatId, "caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/fun",
    });

    expect(sendAnimation).toHaveBeenCalledTimes(1);
    expectMediaSendCall(
      firstMockCall(sendAnimation, "send animation call"),
      "send animation call",
      chatId,
      {
        caption: "caption",
        parse_mode: "HTML",
      },
    );
    expect(probeVideoDimensions).not.toHaveBeenCalled();
    expect(res.messageId).toBe("9");
  });

  it.each([
    {
      name: "images",
      buffer: Buffer.from("fake-image"),
      contentType: "image/png",
      fileName: "photo.png",
      mediaUrl: "https://example.com/photo.png",
    },
    {
      name: "GIFs",
      buffer: Buffer.from("GIF89a"),
      contentType: "image/gif",
      fileName: "fun.gif",
      mediaUrl: "https://example.com/fun.gif",
    },
    {
      name: "videos",
      buffer: Buffer.from("fake-video"),
      contentType: "video/mp4",
      fileName: "clip.mp4",
      mediaUrl: "https://example.com/clip.mp4",
    },
  ])("sends $name as documents when forceDocument is true", async (testCase) => {
    const chatId = "123";
    const sendAnimation = vi.fn();
    const sendDocument = vi.fn().mockResolvedValue({
      message_id: 10,
      chat: { id: chatId },
    });
    const sendPhoto = vi.fn();
    const sendVideo = vi.fn();
    const api = { sendAnimation, sendDocument, sendPhoto, sendVideo } as unknown as {
      sendAnimation: typeof sendAnimation;
      sendDocument: typeof sendDocument;
      sendPhoto: typeof sendPhoto;
      sendVideo: typeof sendVideo;
    };

    mockLoadedMedia({
      buffer: testCase.buffer,
      contentType: testCase.contentType,
      fileName: testCase.fileName,
    });

    const res = await sendMessageTelegram(chatId, "caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: testCase.mediaUrl,
      forceDocument: true,
    });

    expectMediaSendCall(
      firstMockCall(sendDocument, "send document call"),
      `send document call ${testCase.name}`,
      chatId,
      {
        caption: "caption",
        parse_mode: "HTML",
        disable_content_type_detection: true,
      },
    );
    expect(sendPhoto, testCase.name).not.toHaveBeenCalled();
    expect(sendAnimation, testCase.name).not.toHaveBeenCalled();
    expect(sendVideo, testCase.name).not.toHaveBeenCalled();
    expect(probeVideoDimensions, testCase.name).not.toHaveBeenCalled();
    expect(res.messageId).toBe("10");
  });

  it.each([
    { name: "oversized dimensions", width: 6000, height: 5001 },
    { name: "oversized aspect ratio", width: 4000, height: 100 },
  ])("sends images as documents when Telegram rejects $name", async ({ width, height }) => {
    const chatId = "123";
    const sendDocument = vi.fn().mockResolvedValue({
      message_id: 10,
      chat: { id: chatId },
    });
    const sendPhoto = vi.fn();
    const api = { sendDocument, sendPhoto } as unknown as {
      sendDocument: typeof sendDocument;
      sendPhoto: typeof sendPhoto;
    };

    imageMetadata.width = width;
    imageMetadata.height = height;
    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/png",
      fileName: "photo.png",
    });

    const res = await sendMessageTelegram(chatId, "caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.png",
    });

    expectMediaSendCall(
      firstMockCall(sendDocument, "send document call"),
      "send document call",
      chatId,
      {
        caption: "caption",
        parse_mode: "HTML",
      },
    );
    expect(sendPhoto).not.toHaveBeenCalled();
    expect(res.messageId).toBe("10");
  });

  it("sends images as documents when metadata dimensions are unavailable", async () => {
    const chatId = "123";
    const sendDocument = vi.fn().mockResolvedValue({
      message_id: 10,
      chat: { id: chatId },
    });
    const sendPhoto = vi.fn();
    const api = { sendDocument, sendPhoto } as unknown as {
      sendDocument: typeof sendDocument;
      sendPhoto: typeof sendPhoto;
    };

    imageMetadata.width = undefined;
    imageMetadata.height = undefined;
    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/png",
      fileName: "photo.png",
    });

    const res = await sendMessageTelegram(chatId, "caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.png",
    });

    expectMediaSendCall(
      firstMockCall(sendDocument, "send document call"),
      "send document call",
      chatId,
      {
        caption: "caption",
        parse_mode: "HTML",
      },
    );
    expect(sendPhoto).not.toHaveBeenCalled();
    expect(res.messageId).toBe("10");
  });

  it("keeps regular document sends on the default Telegram params", async () => {
    const chatId = "123";
    const sendDocument = vi.fn().mockResolvedValue({
      message_id: 11,
      chat: { id: chatId },
    });
    const api = { sendDocument } as unknown as {
      sendDocument: typeof sendDocument;
    };

    mockLoadedMedia({
      buffer: Buffer.from("%PDF-1.7"),
      contentType: "application/pdf",
      fileName: "report.pdf",
    });

    const res = await sendMessageTelegram(chatId, "caption", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/report.pdf",
    });

    expectMediaSendCall(
      firstMockCall(sendDocument, "send document call"),
      "send document call",
      chatId,
      {
        caption: "caption",
        parse_mode: "HTML",
      },
    );
    expect(res.messageId).toBe("11");
  });

  it("routes audio media to sendAudio/sendVoice based on voice compatibility", async () => {
    const cases: Array<{
      name: string;
      chatId: string;
      text: string;
      mediaUrl: string;
      contentType: string;
      fileName: string;
      asVoice?: boolean;
      messageThreadId?: number;
      replyToMessageId?: number;
      expectedMethod: "sendAudio" | "sendVoice";
      expectedOptions: Record<string, unknown>;
    }> = [
      {
        name: "default audio send",
        chatId: "123",
        text: "caption",
        mediaUrl: "https://example.com/clip.mp3",
        contentType: "audio/mpeg",
        fileName: "clip.mp3",
        expectedMethod: "sendAudio" as const,
        expectedOptions: { caption: "caption", parse_mode: "HTML" },
      },
      {
        name: "voice-compatible media with thread params",
        chatId: "-1001234567890",
        text: "voice note",
        mediaUrl: "https://example.com/note.ogg",
        contentType: "audio/ogg",
        fileName: "note.ogg",
        asVoice: true,
        messageThreadId: 271,
        replyToMessageId: 500,
        expectedMethod: "sendVoice" as const,
        expectedOptions: {
          caption: "voice note",
          parse_mode: "HTML",
          message_thread_id: 271,
          reply_to_message_id: 500,
          allow_sending_without_reply: true,
        },
      },
      {
        name: "asVoice fallback for non-voice media",
        chatId: "123",
        text: "caption",
        mediaUrl: "https://example.com/clip.wav",
        contentType: "audio/wav",
        fileName: "clip.wav",
        asVoice: true,
        expectedMethod: "sendAudio" as const,
        expectedOptions: { caption: "caption", parse_mode: "HTML" },
      },
      {
        name: "asVoice accepts mp3",
        chatId: "123",
        text: "caption",
        mediaUrl: "https://example.com/clip.mp3",
        contentType: "audio/mpeg",
        fileName: "clip.mp3",
        asVoice: true,
        expectedMethod: "sendVoice" as const,
        expectedOptions: { caption: "caption", parse_mode: "HTML" },
      },
      {
        name: "normalizes parameterized audio MIME with mixed casing",
        chatId: "123",
        text: "caption",
        mediaUrl: "https://example.com/note",
        contentType: " Audio/Ogg; codecs=opus ",
        fileName: "note.ogg",
        expectedMethod: "sendAudio" as const,
        expectedOptions: { caption: "caption", parse_mode: "HTML" },
      },
    ];

    for (const testCase of cases) {
      const sendAudio = vi.fn().mockResolvedValue({
        message_id: 10,
        chat: { id: testCase.chatId },
      });
      const sendVoice = vi.fn().mockResolvedValue({
        message_id: 11,
        chat: { id: testCase.chatId },
      });
      const api = { sendAudio, sendVoice } as unknown as {
        sendAudio: typeof sendAudio;
        sendVoice: typeof sendVoice;
      };

      mockLoadedMedia({
        buffer: Buffer.from("audio"),
        contentType: testCase.contentType,
        fileName: testCase.fileName,
      });

      await sendMessageTelegram(testCase.chatId, testCase.text, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: testCase.mediaUrl,
        ...("asVoice" in testCase && testCase.asVoice ? { asVoice: true } : {}),
        ...("messageThreadId" in testCase && testCase.messageThreadId !== undefined
          ? { messageThreadId: testCase.messageThreadId }
          : {}),
        ...("replyToMessageId" in testCase && testCase.replyToMessageId !== undefined
          ? { replyToMessageId: testCase.replyToMessageId }
          : {}),
      });

      const called = testCase.expectedMethod === "sendVoice" ? sendVoice : sendAudio;
      const notCalled = testCase.expectedMethod === "sendVoice" ? sendAudio : sendVoice;
      expectMediaSendCall(
        firstMockCall(called, "called mock call"),
        `${testCase.expectedMethod} call ${testCase.name}`,
        testCase.chatId,
        testCase.expectedOptions,
      );
      expect(notCalled, testCase.name).not.toHaveBeenCalled();
    }
  });

  it("keeps message_thread_id for forum/private/group sends", async () => {
    const cases = [
      {
        name: "forum topic",
        chatId: "-1001234567890",
        text: "hello forum",
        messageId: 55,
      },
      {
        name: "private chat topic (#18974)",
        chatId: "123456789",
        text: "hello private",
        messageId: 56,
      },
      {
        // Group/supergroup chats have negative IDs.
        name: "group chat (#17242)",
        chatId: "-1001234567890",
        text: "hello group",
        messageId: 57,
      },
    ] as const;

    for (const testCase of cases) {
      const sendMessage = vi.fn().mockResolvedValue({
        message_id: testCase.messageId,
        chat: { id: testCase.chatId },
      });
      const api = { sendMessage } as unknown as {
        sendMessage: typeof sendMessage;
      };

      await sendMessageTelegram(testCase.chatId, testCase.text, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        messageThreadId: 271,
      });

      expect(sendMessage, testCase.name).toHaveBeenCalledWith(testCase.chatId, testCase.text, {
        parse_mode: "HTML",
        message_thread_id: 271,
      });
    }
  });

  it("fails topic sends instead of retrying without message_thread_id", async () => {
    const cases = [{ name: "forum", chatId: "-100123", text: "hello forum" }] as const;
    const threadErr = new Error("400: Bad Request: message thread not found");

    for (const testCase of cases) {
      const sendMessage = vi.fn().mockRejectedValueOnce(threadErr);
      const api = { sendMessage } as unknown as {
        sendMessage: typeof sendMessage;
      };

      await expect(
        sendMessageTelegram(testCase.chatId, testCase.text, {
          cfg: TELEGRAM_TEST_CFG,
          token: "tok",
          api,
          messageThreadId: 271,
        }),
      ).rejects.toThrow("message thread not found");

      expect(sendMessage, testCase.name).toHaveBeenCalledTimes(1);
      expect(sendMessage, testCase.name).toHaveBeenCalledWith(testCase.chatId, testCase.text, {
        parse_mode: "HTML",
        message_thread_id: 271,
      });
    }
  });

  it("does not retry private DM topic sends without the topic id", async () => {
    const threadErr = new Error("400: Bad Request: message thread not found");
    const sendMessage = vi.fn().mockRejectedValueOnce(threadErr);
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await expect(
      sendMessageTelegram("123456789", "hello private", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        messageThreadId: 271,
      }),
    ).rejects.toThrow("message thread not found");

    expect(sendMessage).toHaveBeenCalledTimes(1);
    expect(sendMessage).toHaveBeenCalledWith("123456789", "hello private", {
      parse_mode: "HTML",
      message_thread_id: 271,
    });
  });

  it("does not retry on non-retriable thread/chat errors", async () => {
    const cases: Array<{
      chatId: string;
      text: string;
      error: Error;
      opts?: { messageThreadId?: number };
      expectedError: RegExp | string;
      expectedCallArgs: [string, string, { parse_mode: "HTML"; message_thread_id?: number }];
    }> = [
      {
        chatId: "123",
        text: "hello forum",
        error: new Error("400: Bad Request: message thread not found"),
        expectedError: "message thread not found",
        expectedCallArgs: ["123", "hello forum", { parse_mode: "HTML" }],
      },
      {
        chatId: "123456789",
        text: "hello private",
        error: new Error("400: Bad Request: chat not found"),
        opts: { messageThreadId: 271 },
        expectedError: /chat not found/i,
        expectedCallArgs: [
          "123456789",
          "hello private",
          { parse_mode: "HTML", message_thread_id: 271 },
        ],
      },
    ];

    for (const testCase of cases) {
      const sendMessage = vi.fn().mockRejectedValueOnce(testCase.error);
      const api = { sendMessage } as unknown as {
        sendMessage: typeof sendMessage;
      };

      await expect(
        sendMessageTelegram(testCase.chatId, testCase.text, {
          cfg: TELEGRAM_TEST_CFG,
          token: "tok",
          api,
          ...testCase.opts,
        }),
      ).rejects.toThrow(testCase.expectedError);

      expect(sendMessage).toHaveBeenCalledTimes(1);
      expect(sendMessage).toHaveBeenCalledWith(...testCase.expectedCallArgs);
    }
  });

  it("sets disable_notification when silent is true", async () => {
    const chatId = "123";
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 1,
      chat: { id: chatId },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await sendMessageTelegram(chatId, "hi", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      silent: true,
    });

    expect(sendMessage).toHaveBeenCalledWith(chatId, "hi", {
      parse_mode: "HTML",
      disable_notification: true,
    });
  });

  it("parses message_thread_id from recipient string (telegram:group:...:topic:...)", async () => {
    const chatId = "-1001234567890";
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 55,
      chat: { id: chatId },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, "hello forum", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(sendMessage).toHaveBeenCalledWith(chatId, "hello forum", {
      parse_mode: "HTML",
      message_thread_id: 271,
    });
  });

  it("logs successful outbound text delivery without the message body", async () => {
    const logFile = captureInfoLogs();
    const chatId = "-1001234567890";
    const body = "incident reply body should stay private";
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 321,
      chat: { id: chatId },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, body, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "ops",
      api,
      replyToMessageId: 123,
      silent: true,
    });

    const logs = capturedLogText(logFile);
    expect(logs).toContain("outbound send ok");
    expect(logs).toContain("accountId=ops");
    expect(logs).toContain(`chatId=${chatId}`);
    expect(logs).toContain("messageId=321");
    expect(logs).toContain("operation=sendRichMessage");
    expect(logs).toContain("threadId=271");
    expect(logs).toContain("replyToMessageId=123");
    expect(logs).toContain("silent=true");
    expect(logs).toContain("chunkCount=1");
    expect(logs).not.toContain(body);
  });

  it("does not log outbound success when topic text send fails thread lookup", async () => {
    const logFile = captureInfoLogs();
    const chatId = "-1001234567890";
    const body = "topic reply body should stay private";
    const threadErr = new Error("400: Bad Request: message thread not found");
    const sendMessage = vi.fn().mockRejectedValueOnce(threadErr);
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await expect(
      sendMessageTelegram(`telegram:group:${chatId}:topic:271`, body, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        accountId: "ops",
        api,
      }),
    ).rejects.toThrow("message thread not found");

    expect(sendMessage).toHaveBeenCalledTimes(1);
    expect(sendMessage).toHaveBeenCalledWith(chatId, body, {
      parse_mode: "HTML",
      message_thread_id: 271,
    });
    const logs = capturedLogText(logFile);
    expect(logs).not.toContain("outbound send ok");
    expect(logs).not.toContain(body);
  });

  it("logs successful outbound media delivery without caption or media location", async () => {
    const logFile = captureInfoLogs();
    const chatId = "123";
    const caption = "private media caption";
    const mediaUrl = "https://example.com/private-photo.jpg";
    const fileName = "private-photo.jpg";
    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 654,
      chat: { id: chatId },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName,
    });

    await sendMessageTelegram(chatId, caption, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      accountId: "ops",
      api,
      mediaUrl,
      messageThreadId: 45,
    });

    const logs = capturedLogText(logFile);
    expect(logs).toContain("outbound send ok");
    expect(logs).toContain("accountId=ops");
    expect(logs).toContain(`chatId=${chatId}`);
    expect(logs).toContain("messageId=654");
    expect(logs).toContain("operation=sendPhoto");
    expect(logs).toContain("deliveryKind=photo");
    expect(logs).toContain("threadId=45");
    expect(logs).not.toContain(caption);
    expect(logs).not.toContain(mediaUrl);
    expect(logs).not.toContain(fileName);
  });

  it("fails media sends instead of retrying without message_thread_id", async () => {
    const logFile = captureInfoLogs();
    const chatId = "-100123";
    const threadErr = new Error("400: Bad Request: message thread not found");
    const sendPhoto = vi.fn().mockRejectedValueOnce(threadErr);
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    await expect(
      sendMessageTelegram(chatId, "photo", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        mediaUrl: "https://example.com/photo.jpg",
        messageThreadId: 271,
      }),
    ).rejects.toThrow("message thread not found");

    expect(sendPhoto).toHaveBeenCalledTimes(1);
    expectMediaSendCall(
      firstMockCall(sendPhoto, "first send photo call"),
      "first send photo call",
      chatId,
      {
        caption: "photo",
        parse_mode: "HTML",
        message_thread_id: 271,
      },
    );
    const logs = capturedLogText(logFile);
    expect(logs).not.toContain("outbound send ok");
  });

  it("defaults outbound media uploads to 100MB", async () => {
    const chatId = "123";
    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 60,
      chat: { id: chatId },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    await sendMessageTelegram(chatId, "photo", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    const [mediaUrl, options] = requireMockCall(
      firstMockCall(loadWebMedia, "loadWebMedia call"),
      "load web media call",
    );
    expect(mediaUrl).toBe("https://example.com/photo.jpg");
    expect(requireRecord(options, "load web media options").maxBytes).toBe(100 * 1024 * 1024);
  });

  it("uses configured telegram mediaMaxMb for outbound uploads", async () => {
    const chatId = "123";
    const sendPhoto = vi.fn().mockResolvedValue({
      message_id: 61,
      chat: { id: chatId },
    });
    const api = { sendPhoto } as unknown as {
      sendPhoto: typeof sendPhoto;
    };
    const cfg = {
      channels: {
        telegram: {
          mediaMaxMb: 42,
        },
      },
    };
    loadConfig.mockReturnValue(cfg);

    mockLoadedMedia({
      buffer: Buffer.from("fake-image"),
      contentType: "image/jpeg",
      fileName: "photo.jpg",
    });

    await sendMessageTelegram(chatId, "photo", {
      cfg,
      token: "tok",
      api,
      mediaUrl: "https://example.com/photo.jpg",
    });

    const [mediaUrl, options] = requireMockCall(
      firstMockCall(loadWebMedia, "loadWebMedia call"),
      "load web media call",
    );
    expect(mediaUrl).toBe("https://example.com/photo.jpg");
    expect(requireRecord(options, "load web media options").maxBytes).toBe(42 * 1024 * 1024);
  });

  it("sends long html-mode rich text with buttons", async () => {
    const chatId = "123";
    const htmlText = `<b>${"A".repeat(5000)}</b>`;

    const sendMessage = vi.fn().mockResolvedValue({ message_id: 91, chat: { id: chatId } });
    const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };

    const res = await sendMessageTelegram(chatId, htmlText, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      textMode: "html",
      buttons: [[{ text: "OK", callback_data: "ok" }]],
    });

    expect(sendMessage).toHaveBeenCalledTimes(1);
    const firstCall = firstMockCall(sendMessage, "first sendMessage call");
    const firstParams = requireRecord(firstCall[2], "first sendMessage params");
    expect(firstParams.reply_markup).toEqual({
      inline_keyboard: [[{ text: "OK", callback_data: "ok" }]],
    });
    expect(res.messageId).toBe("91");
  });

  it("sends long default markdown rich text with buttons", async () => {
    const chatId = "123";
    const markdownText = `**${"A".repeat(5000)}**`;

    const sendMessage = vi.fn().mockResolvedValue({ message_id: 91, chat: { id: chatId } });
    const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };

    const res = await sendMessageTelegram(chatId, markdownText, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      buttons: [[{ text: "OK", callback_data: "ok" }]],
    });

    expect(sendMessage).toHaveBeenCalledTimes(1);
    const firstCall = firstMockCall(sendMessage, "first sendMessage call");
    const firstParams = requireRecord(firstCall[2], "first sendMessage params");
    const firstText = requireString(firstCall[1], "first sendMessage text");
    expect(firstParams.parse_mode).toBe("HTML");
    expect(firstText).toContain("A");
    expect(firstParams.reply_markup).toEqual({
      inline_keyboard: [[{ text: "OK", callback_data: "ok" }]],
    });
    expect(res.messageId).toBe("91");
  });
});

describe("reactMessageTelegram", () => {
  it.each([
    {
      testName: "sends emoji reactions",
      target: "telegram:123",
      messageId: "456",
      emoji: "✅",
      remove: false,
      expected: [{ type: "emoji", emoji: "✅" }],
    },
    {
      testName: "removes reactions when emoji is empty",
      target: "123",
      messageId: 456,
      emoji: "",
      remove: false,
      expected: [],
    },
    {
      testName: "removes reactions when remove flag is set",
      target: "123",
      messageId: 456,
      emoji: "✅",
      remove: true,
      expected: [],
    },
  ] as const)("$testName", async (testCase) => {
    const setMessageReaction = vi.fn().mockResolvedValue(undefined);
    const api = { setMessageReaction } as unknown as {
      setMessageReaction: typeof setMessageReaction;
    };

    await reactMessageTelegram(testCase.target, testCase.messageId, testCase.emoji, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      ...(testCase.remove ? { remove: true } : {}),
    });

    expect(setMessageReaction).toHaveBeenCalledWith("123", 456, testCase.expected);
  });

  it("resolves legacy telegram targets before reacting", async () => {
    const setMessageReaction = vi.fn().mockResolvedValue(undefined);
    const getChat = vi.fn().mockResolvedValue({ id: -100123 });
    const api = { setMessageReaction, getChat } as unknown as {
      setMessageReaction: typeof setMessageReaction;
      getChat: typeof getChat;
    };

    await reactMessageTelegram("@mychannel", 456, "✅", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(getChat).toHaveBeenCalledWith("@mychannel");
    expect(setMessageReaction).toHaveBeenCalledWith("-100123", 456, [
      { type: "emoji", emoji: "✅" },
    ]);
    expectPersistedTarget({
      rawTarget: "@mychannel",
      resolvedChatId: "-100123",
    });
  });
});

describe("deleteMessageTelegram", () => {
  it.each([
    "400: Bad Request: message to delete not found",
    "400: Bad Request: message can't be deleted",
    "MESSAGE_ID_INVALID",
    "MESSAGE_DELETE_FORBIDDEN",
  ] as const)("returns a warning for benign delete no-op error: %s", async (message) => {
    const deleteMessage = vi.fn().mockRejectedValue(new Error(message));
    const api = { deleteMessage } as unknown as { deleteMessage: typeof deleteMessage };

    const result = await deleteMessageTelegram("123", 456, {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
    });

    expect(deleteMessage).toHaveBeenCalledWith("123", 456);
    expect(result.ok).toBe(false);
    if (result.ok) {
      throw new Error("expected delete warning result");
    }
    expect(result.warning).toContain(message);
  });

  it("throws non-benign delete errors", async () => {
    const deleteMessage = vi.fn().mockRejectedValue(new Error("500: Internal Server Error"));
    const api = { deleteMessage } as unknown as { deleteMessage: typeof deleteMessage };

    await expect(
      deleteMessageTelegram("123", 456, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      }),
    ).rejects.toThrow(/Internal Server Error/);
  });

  it("rejects partial message id strings", async () => {
    const deleteMessage = vi.fn();
    const api = { deleteMessage } as unknown as { deleteMessage: typeof deleteMessage };

    await expect(
      deleteMessageTelegram("123", "456abc", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      }),
    ).rejects.toThrow(/Message id is required/);
    expect(deleteMessage).not.toHaveBeenCalled();
  });
});

describe("sendStickerTelegram", () => {
  const positiveSendCases = [
    {
      name: "sends a sticker by file_id",
      fileId: "CAACAgIAAxkBAAI...sticker_file_id",
      expectedFileId: "CAACAgIAAxkBAAI...sticker_file_id",
      expectedMessageId: 100,
    },
    {
      name: "trims whitespace from fileId",
      fileId: "  fileId123  ",
      expectedFileId: "fileId123",
      expectedMessageId: 106,
    },
  ] as const;

  for (const testCase of positiveSendCases) {
    it(testCase.name, async () => {
      const chatId = "123";
      const sendSticker = vi.fn().mockResolvedValue({
        message_id: testCase.expectedMessageId,
        chat: { id: chatId },
      });
      const api = { sendSticker } as unknown as {
        sendSticker: typeof sendSticker;
      };

      const res = await sendStickerTelegram(chatId, testCase.fileId, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      });

      expect(sendSticker).toHaveBeenCalledWith(chatId, testCase.expectedFileId, undefined);
      expect(res.messageId).toBe(String(testCase.expectedMessageId));
      expect(res.chatId).toBe(chatId);
    });
  }

  it("throws error when fileId is blank", async () => {
    for (const fileId of ["", "   "]) {
      await expect(
        sendStickerTelegram("123", fileId, { cfg: TELEGRAM_TEST_CFG, token: "tok" }),
      ).rejects.toThrow(/file_id is required/i);
    }
  });

  it("fails sticker sends instead of retrying without message_thread_id", async () => {
    const chatId = "-100123";
    const threadErr = new Error("400: Bad Request: message thread not found");
    const sendSticker = vi.fn().mockRejectedValueOnce(threadErr);
    const api = { sendSticker } as unknown as {
      sendSticker: typeof sendSticker;
    };

    await expect(
      sendStickerTelegram(chatId, "fileId123", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        messageThreadId: 271,
      }),
    ).rejects.toThrow("message thread not found");

    expect(sendSticker).toHaveBeenCalledTimes(1);
    expect(sendSticker).toHaveBeenCalledWith(chatId, "fileId123", {
      message_thread_id: 271,
    });
  });

  it("fails when sticker send returns no message_id", async () => {
    const chatId = "123";
    const sendSticker = vi.fn().mockResolvedValue({
      chat: { id: chatId },
    });
    const api = { sendSticker } as unknown as {
      sendSticker: typeof sendSticker;
    };

    await expect(
      sendStickerTelegram(chatId, "fileId123", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
      }),
    ).rejects.toThrow(/returned no message_id/i);
  });

  it("does not retry generic grammY failed envelopes for sticker sends", async () => {
    const chatId = "123";
    const sendSticker = vi
      .fn()
      .mockRejectedValueOnce(new Error("Network request for 'sendSticker' failed!"));
    const api = { sendSticker } as unknown as {
      sendSticker: typeof sendSticker;
    };

    await expect(
      sendStickerTelegram(chatId, "fileId123", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
      }),
    ).rejects.toThrow(/Network request for 'sendSticker' failed!/i);
    expect(sendSticker).toHaveBeenCalledTimes(1);
  });

  it("retries rate-limited sticker sends and honors retry_after", async () => {
    vi.useFakeTimers();
    const chatId = "123";
    const sendSticker = vi
      .fn()
      .mockRejectedValueOnce({
        message: "429 Too Many Requests",
        response: { parameters: { retry_after: 1 } },
      })
      .mockResolvedValueOnce({
        message_id: 109,
        chat: { id: chatId },
      });
    const api = { sendSticker } as unknown as {
      sendSticker: typeof sendSticker;
    };
    const setTimeoutSpy = vi.spyOn(global, "setTimeout");

    const promise = sendStickerTelegram(chatId, "fileId123", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
    });

    await vi.runAllTimersAsync();
    await expect(promise).resolves.toEqual({ messageId: "109", chatId });
    expect(firstMockCall(setTimeoutSpy, "setTimeout call")[1]).toBe(1000);
    expect(sendSticker).toHaveBeenCalledTimes(2);
    setTimeoutSpy.mockRestore();
    vi.useRealTimers();
  });
});

describe("shared send behaviors", () => {
  it("includes reply_to_message_id for threaded replies", async () => {
    const cases = [
      {
        name: "message send",
        run: async () => {
          const chatId = "123";
          const sendMessage = vi.fn().mockResolvedValue({
            message_id: 56,
            chat: { id: chatId },
          });
          const api = { sendMessage } as unknown as {
            sendMessage: typeof sendMessage;
          };
          await sendMessageTelegram(chatId, "reply text", {
            cfg: TELEGRAM_TEST_CFG,
            token: "tok",
            api,
            replyToMessageId: 100,
          });
          expect(sendMessage).toHaveBeenCalledWith(chatId, "reply text", {
            parse_mode: "HTML",
            reply_parameters: {
              message_id: 100,
              allow_sending_without_reply: true,
            },
          });
        },
      },
      {
        name: "sticker send",
        run: async () => {
          const chatId = "123";
          const fileId = "CAACAgIAAxkBAAI...sticker_file_id";
          const sendSticker = vi.fn().mockResolvedValue({
            message_id: 102,
            chat: { id: chatId },
          });
          const api = { sendSticker } as unknown as {
            sendSticker: typeof sendSticker;
          };
          await sendStickerTelegram(chatId, fileId, {
            cfg: TELEGRAM_TEST_CFG,
            token: "tok",
            api,
            replyToMessageId: 500,
          });
          expect(sendSticker).toHaveBeenCalledWith(chatId, fileId, {
            reply_to_message_id: 500,
            allow_sending_without_reply: true,
          });
        },
      },
    ] as const;

    for (const testCase of cases) {
      await testCase.run();
    }
  });

  it("uses native reply parameters for direct quote sends without trimming the quote", async () => {
    const chatId = "123";
    const sendMessage = vi.fn().mockResolvedValue({
      message_id: 56,
      chat: { id: chatId },
    });
    const api = { sendMessage } as unknown as {
      sendMessage: typeof sendMessage;
    };

    await sendMessageTelegram(chatId, "reply text", {
      cfg: TELEGRAM_TEST_CFG,
      token: "tok",
      api,
      replyToMessageId: 100,
      quoteText: " quoted text\n",
    });

    expect(sendMessage).toHaveBeenCalledWith(chatId, "reply text", {
      parse_mode: "HTML",
      reply_parameters: {
        message_id: 100,
        quote: " quoted text\n",
        allow_sending_without_reply: true,
      },
    });
  });

  it("omits invalid reply_to_message_id values before calling Telegram", async () => {
    const invalidReplyToMessageIds = ["session-meta-id", "123abc", Number.NaN] as const;

    for (const invalidReplyToMessageId of invalidReplyToMessageIds) {
      const chatId = "123";
      const sendMessage = vi.fn().mockResolvedValue({
        message_id: 56,
        chat: { id: chatId },
      });
      const sendSticker = vi.fn().mockResolvedValue({
        message_id: 102,
        chat: { id: chatId },
      });
      const api = { sendMessage, sendSticker } as unknown as {
        sendMessage: typeof sendMessage;
        sendSticker: typeof sendSticker;
      };

      await sendMessageTelegram(chatId, "reply text", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        replyToMessageId: invalidReplyToMessageId as unknown as number,
      });
      await sendStickerTelegram(chatId, "CAACAgIAAxkBAAI...sticker_file_id", {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        replyToMessageId: invalidReplyToMessageId as unknown as number,
      });

      expect(sendMessage, String(invalidReplyToMessageId)).toHaveBeenCalledWith(
        chatId,
        "reply text",
        {
          parse_mode: "HTML",
        },
      );
      expect(sendSticker, String(invalidReplyToMessageId)).toHaveBeenCalledWith(
        chatId,
        "CAACAgIAAxkBAAI...sticker_file_id",
        undefined,
      );
    }
  });

  it("wraps chat-not-found with actionable context", async () => {
    const cases = [
      {
        name: "message send",
        run: async () => {
          const chatId = "123";
          const err = new Error("400: Bad Request: chat not found");
          const sendMessage = vi.fn().mockRejectedValue(err);
          const api = { sendMessage } as unknown as {
            sendMessage: typeof sendMessage;
          };
          await expectChatNotFoundWithChatId(
            sendMessageTelegram(chatId, "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
            chatId,
          );
        },
      },
      {
        name: "sticker send",
        run: async () => {
          const chatId = "123";
          const err = new Error("400: Bad Request: chat not found");
          const sendSticker = vi.fn().mockRejectedValue(err);
          const api = { sendSticker } as unknown as {
            sendSticker: typeof sendSticker;
          };
          await expectChatNotFoundWithChatId(
            sendStickerTelegram(chatId, "fileId123", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
            chatId,
          );
        },
      },
    ] as const;

    for (const testCase of cases) {
      await testCase.run();
    }
  });

  it("wraps membership-related 403 errors with actionable context and original detail", async () => {
    const cases = [
      {
        name: "message send",
        errorText: "403: Forbidden: bot is not a member of the channel chat",
        run: async (chatId: string, err: Error) => {
          const sendMessage = vi.fn().mockRejectedValue(err);
          const api = { sendMessage } as unknown as {
            sendMessage: typeof sendMessage;
          };
          await expectTelegramMembershipErrorWithChatId(
            sendMessageTelegram(chatId, "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
            chatId,
            /bot is not a member of the channel chat/i,
          );
        },
      },
      {
        name: "sticker send",
        errorText: "403: Forbidden: bot was kicked from the group chat",
        run: async (chatId: string, err: Error) => {
          const sendSticker = vi.fn().mockRejectedValue(err);
          const api = { sendSticker } as unknown as {
            sendSticker: typeof sendSticker;
          };
          await expectTelegramMembershipErrorWithChatId(
            sendStickerTelegram(chatId, "fileId123", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
            chatId,
            /bot was kicked from the group chat/i,
          );
        },
      },
    ] as const;

    for (const testCase of cases) {
      await testCase.run("123", new Error(testCase.errorText));
    }
  });
});

describe("editMessageTelegram", () => {
  it.each([
    {
      name: "buttons undefined keeps existing keyboard",
      text: "hi",
      buttons: undefined as Parameters<typeof buildInlineKeyboard>[0],
      expectedCalls: 1,
      firstExpectNoReplyMarkup: true,
      parseFallback: false,
    },
    {
      name: "buttons empty clears keyboard",
      text: "hi",
      buttons: [] as Parameters<typeof buildInlineKeyboard>[0],
      expectedCalls: 1,
      firstExpectReplyMarkup: { inline_keyboard: [] } as Record<string, unknown>,
      parseFallback: false,
    },
    {
      name: "rich edit preserves cleared keyboard",
      text: "<bad> html",
      buttons: [] as Parameters<typeof buildInlineKeyboard>[0],
      expectedCalls: 1,
      firstExpectReplyMarkup: { inline_keyboard: [] } as Record<string, unknown>,
      parseFallback: false,
    },
  ])("$name", async (testCase) => {
    if (testCase.parseFallback) {
      botApi.editMessageText
        .mockRejectedValueOnce(new Error("400: Bad Request: can't parse entities"))
        .mockResolvedValueOnce({ message_id: 1, chat: { id: "123" } });
    } else {
      botApi.editMessageText.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
    }

    await editMessageTelegram("123", 1, testCase.text, {
      token: "tok",
      cfg: {},
      buttons: testCase.buttons ? testCase.buttons.map((row) => [...row]) : testCase.buttons,
    });

    expect(botCtorSpy, testCase.name).toHaveBeenCalledTimes(1);
    expect(firstMockCall(botCtorSpy, "bot constructor call")[0], testCase.name).toBe("tok");
    expect(botApi.editMessageText, testCase.name).toHaveBeenCalledTimes(testCase.expectedCalls);

    const firstParams = requireRecord(
      firstMockCall(botApi.editMessageText, "editMessageText call")[3],
      "first edit params",
    );
    expect(firstParams.parse_mode, testCase.name).toBe("HTML");
    if ("firstExpectNoReplyMarkup" in testCase && testCase.firstExpectNoReplyMarkup) {
      expect(firstParams, testCase.name).not.toHaveProperty("reply_markup");
    }
    if ("firstExpectReplyMarkup" in testCase && testCase.firstExpectReplyMarkup) {
      expect(firstParams.reply_markup, testCase.name).toEqual(testCase.firstExpectReplyMarkup);
    }

    if ("secondExpectReplyMarkup" in testCase && testCase.secondExpectReplyMarkup) {
      const secondParams = requireRecord(
        mockCall(botApi.editMessageText, 1, "second editMessageText call")[3],
        "second edit params",
      );
      expect(secondParams.reply_markup, testCase.name).toEqual(testCase.secondExpectReplyMarkup);
    }
  });

  it("treats 'message is not modified' as success", async () => {
    botApi.editMessageText.mockRejectedValueOnce(
      new Error(
        "400: Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message",
      ),
    );

    await expect(
      editMessageTelegram("123", 1, "hi", {
        token: "tok",
        cfg: {},
      }),
    ).resolves.toEqual({ ok: true, messageId: "1", chatId: "123" });
    expect(botApi.editMessageText).toHaveBeenCalledTimes(1);
  });

  it("uses editMessageCaption when requested for media captions", async () => {
    botApi.editMessageCaption.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await editMessageTelegram("123", 1, "Media **caption**", {
      token: "tok",
      cfg: {},
      editMode: "caption",
      buttons: [[{ text: "Open", url: "https://example.com" }]],
    });

    expect(botApi.editMessageText).not.toHaveBeenCalled();
    expect(botApi.editMessageCaption).toHaveBeenCalledTimes(1);
    const captionParams = requireRecord(
      firstMockCall(botApi.editMessageCaption, "editMessageCaption call")[2],
      "caption edit params",
    );
    expect(captionParams.caption).toBe("Media <b>caption</b>");
    expect(captionParams.parse_mode).toBe("HTML");
    expect(captionParams.reply_markup).toEqual({
      inline_keyboard: [[{ text: "Open", url: "https://example.com" }]],
    });
  });

  it("falls back to editMessageCaption when Telegram reports a media message has no text", async () => {
    botApi.editMessageText.mockRejectedValueOnce(
      new Error("400: Bad Request: there is no text in the message to edit"),
    );
    botApi.editMessageCaption.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await editMessageTelegram("123", 1, "New caption", {
      token: "tok",
      cfg: {},
      editMode: "auto",
    });

    expect(botApi.editMessageText).toHaveBeenCalledTimes(1);
    expect(botApi.editMessageCaption).toHaveBeenCalledTimes(1);
    const captionParams = requireRecord(
      firstMockCall(botApi.editMessageCaption, "fallback editMessageCaption call")[2],
      "fallback caption edit params",
    );
    expect(captionParams.caption).toBe("New caption");
    expect(captionParams.parse_mode).toBe("HTML");
  });

  it("retries editMessageTelegram on Telegram 5xx errors", async () => {
    botApi.editMessageText
      .mockRejectedValueOnce(Object.assign(new Error("502: Bad Gateway"), { error_code: 502 }))
      .mockResolvedValueOnce({ message_id: 1, chat: { id: "123" } });

    await expect(
      editMessageTelegram("123", 1, "hi", {
        token: "tok",
        cfg: {},
        retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
      }),
    ).resolves.toEqual({ ok: true, messageId: "1", chatId: "123" });

    expect(botApi.editMessageText).toHaveBeenCalledTimes(2);
  });

  it("edits text with raw rich markdown", async () => {
    botApi.editMessageText.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await editMessageTelegram("123", 1, "**edited**", {
      token: "tok",
      cfg: {},
    });

    expect(botRawApi.editMessageText).toHaveBeenCalledWith({
      chat_id: "123",
      message_id: 1,
      rich_message: { markdown: "**edited**" },
    });
  });

  it("edits complex text as raw rich markdown", async () => {
    botApi.editMessageText.mockResolvedValue({ message_id: 1, chat: { id: "123" } });
    const markdown = ["## Updated", "", "- **bold**", "- _italic_", "", "`code`"].join("\n");

    await editMessageTelegram("123", 1, markdown, {
      token: "tok",
      cfg: {},
    });

    expect(botRawApi.editMessageText).toHaveBeenCalledWith({
      chat_id: "123",
      message_id: 1,
      rich_message: { markdown },
    });
  });

  it("skips rich entity detection for rich text edits when link previews are disabled", async () => {
    botApi.editMessageText.mockResolvedValue({ message_id: 1, chat: { id: "123" } });

    await editMessageTelegram("123", 1, "https://example.com", {
      token: "tok",
      cfg: {},
      linkPreview: false,
    });

    expect(botRawApi.editMessageText).toHaveBeenCalledTimes(1);
    expect(botRawApi.editMessageText).toHaveBeenCalledWith({
      chat_id: "123",
      message_id: 1,
      rich_message: {
        markdown: "https://example.com",
        skip_entity_detection: true,
      },
    });
  });
});

describe("sendPollTelegram", () => {
  it("propagates gateway client scopes when resolving legacy poll targets", async () => {
    const api = {
      getChat: vi.fn(async () => ({ id: -100321 })),
      sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })),
    };

    await sendPollTelegram(
      "https://t.me/mychannel",
      { question: " Q ", options: [" A ", "B "] },
      {
        cfg: TELEGRAM_TEST_CFG,
        token: "t",
        api: api as unknown as Bot["api"],
        gatewayClientScopes: ["operator.admin"],
      },
    );

    expect(api.getChat).toHaveBeenCalledWith("@mychannel");
    expectPersistedTarget({
      rawTarget: "https://t.me/mychannel",
      resolvedChatId: "-100321",
      gatewayClientScopes: ["operator.admin"],
    });
  });

  it("maps durationSeconds to open_period", async () => {
    const api = {
      sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })),
    };

    const res = await sendPollTelegram(
      "123",
      { question: " Q ", options: [" A ", "B "], durationSeconds: 60 },
      { cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
    );

    expect(res).toEqual({ messageId: "123", chatId: "555", pollId: "p1" });
    expect(api.sendPoll).toHaveBeenCalledTimes(1);
    const sendPollMock = api.sendPoll as ReturnType<typeof vi.fn>;
    const sendPollCall = firstMockCall(sendPollMock, "send poll call");
    expect(sendPollCall[0]).toBe("123");
    expect(sendPollCall[1]).toBe("Q");
    expect(sendPollCall[2]).toEqual(["A", "B"]);
    expect(requireRecord(sendPollCall[3], "send poll params").open_period).toBe(60);
  });

  it("fails poll sends instead of retrying without message_thread_id", async () => {
    const api = {
      sendPoll: vi
        .fn()
        .mockRejectedValueOnce(new Error("400: Bad Request: message thread not found")),
    };

    await expect(
      sendPollTelegram(
        "-100123",
        { question: "Q", options: ["A", "B"] },
        {
          cfg: TELEGRAM_TEST_CFG,
          token: "t",
          api: api as unknown as Bot["api"],
          messageThreadId: 99,
        },
      ),
    ).rejects.toThrow("message thread not found");

    expect(api.sendPoll).toHaveBeenCalledTimes(1);
    expect(
      requireRecord(firstMockCall(api.sendPoll, "send poll call")[3], "send poll params")
        .message_thread_id,
    ).toBe(99);
  });

  it("rejects durationHours for Telegram polls", async () => {
    const api = { sendPoll: vi.fn() };

    await expect(
      sendPollTelegram(
        "123",
        { question: "Q", options: ["A", "B"], durationHours: 1 },
        { cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
      ),
    ).rejects.toThrow(/durationHours is not supported/i);

    expect(api.sendPoll).not.toHaveBeenCalled();
  });

  it("fails when poll send returns no message_id", async () => {
    const api = {
      sendPoll: vi.fn(async () => ({ chat: { id: 555 }, poll: { id: "p1" } })),
    };

    await expect(
      sendPollTelegram(
        "123",
        { question: "Q", options: ["A", "B"] },
        { cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
      ),
    ).rejects.toThrow(/returned no message_id/i);
  });
});

describe("createForumTopicTelegram", () => {
  const cases = [
    {
      name: "uses base chat id when target includes topic suffix",
      target: "telegram:group:-1001234567890:topic:271",
      title: "x",
      response: { message_thread_id: 272, name: "Build Updates" },
      expectedCall: ["-1001234567890", "x", undefined] as const,
      expectedResult: {
        topicId: 272,
        name: "Build Updates",
        chatId: "-1001234567890",
      },
    },
    {
      name: "forwards optional icon fields",
      target: "-1001234567890",
      title: "Roadmap",
      response: { message_thread_id: 300, name: "Roadmap" },
      options: {
        iconColor: 0x6fb9f0,
        iconCustomEmojiId: "  1234567890  ",
      },
      expectedCall: [
        "-1001234567890",
        "Roadmap",
        { icon_color: 0x6fb9f0, icon_custom_emoji_id: "1234567890" },
      ] as const,
      expectedResult: {
        topicId: 300,
        name: "Roadmap",
        chatId: "-1001234567890",
      },
    },
  ] as const;

  for (const testCase of cases) {
    it(testCase.name, async () => {
      const createForumTopic = vi.fn().mockResolvedValue(testCase.response);
      const api = { createForumTopic } as unknown as Bot["api"];

      const result = await createForumTopicTelegram(testCase.target, testCase.title, {
        cfg: TELEGRAM_TEST_CFG,
        token: "tok",
        api,
        ...("options" in testCase ? testCase.options : {}),
      });

      expect(createForumTopic).toHaveBeenCalledWith(...testCase.expectedCall);
      expect(result).toEqual(testCase.expectedResult);
    });
  }
});
