import { describe, test, expect, vi, beforeEach } from "vitest";
import {
  TelegramManager,
  escapeMarkdown,
  escapeInlineCode,
  normalizeBranchName,
  sortWorkspacesStarredFirst,
} from "./telegram-manager.js";
import type { TelegramConnectionConfig, TelegramPrInfo, TelegramWorkspaceInfo } from "./telegram-manager.js";
// TelegramWorkspaceInfo is used in the windowSlot validation tests at the bottom.

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function makeCredentialStore(secrets: Record<string, string> = {}) {
  return {
    hasSecret: vi.fn((ref: string) => ref in secrets),
    getSecret: vi.fn((ref: string) => secrets[ref] ?? null),
    setSecret: vi.fn(),
    deleteSecret: vi.fn(),
    listRefs: vi.fn(() => Object.keys(secrets)),
    isEncryptionAvailable: vi.fn(() => true),
  };
}

function makeConnection(overrides: Partial<TelegramConnectionConfig> = {}): TelegramConnectionConfig {
  return {
    id: "tg-1",
    label: "Test",
    botTokenRef: "cred:tg-1",
    chatId: "12345",
    enabled: true,
    pollSeconds: 5,
    forwardKinds: [],
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// escapeMarkdown
// ---------------------------------------------------------------------------

describe("escapeMarkdown", () => {
  test("escapes special MarkdownV2 characters", () => {
    expect(escapeMarkdown("Hello World!")).toBe("Hello World\\!");
    expect(escapeMarkdown("test.value")).toBe("test\\.value");
    expect(escapeMarkdown("a_b*c[d]e")).toBe("a\\_b\\*c\\[d\\]e");
    expect(escapeMarkdown("a-b")).toBe("a\\-b");
    expect(escapeMarkdown("(foo)")).toBe("\\(foo\\)");
  });

  test("does not double-escape already escaped chars", () => {
    // The function takes plain text — backslashes get escaped
    expect(escapeMarkdown("a\\b")).toBe("a\\\\b");
  });

  test("returns empty string for empty input", () => {
    expect(escapeMarkdown("")).toBe("");
  });

  test("handles text with no special chars unchanged", () => {
    expect(escapeMarkdown("hello world")).toBe("hello world");
  });
});

// ---------------------------------------------------------------------------
// escapeInlineCode
// ---------------------------------------------------------------------------

describe("escapeInlineCode", () => {
  test("escapes backslashes and backticks only", () => {
    expect(escapeInlineCode("path\\to\\file")).toBe("path\\\\to\\\\file");
    expect(escapeInlineCode("use `code` here")).toBe("use \\`code\\` here");
  });

  test("does not escape parentheses, dashes, dots, or other MarkdownV2 special chars", () => {
    expect(escapeInlineCode("/home/user/my-project (main)")).toBe("/home/user/my-project (main)");
    expect(escapeInlineCode("C:\\Users\\user\\project (test)")).toBe("C:\\\\Users\\\\user\\\\project (test)");
    expect(escapeInlineCode("src/foo.ts")).toBe("src/foo.ts");
    expect(escapeInlineCode("file_name.ext")).toBe("file_name.ext");
  });

  test("returns empty string for empty input", () => {
    expect(escapeInlineCode("")).toBe("");
  });

  test("handles text with no special chars unchanged", () => {
    expect(escapeInlineCode("hello world")).toBe("hello world");
  });
});

// ---------------------------------------------------------------------------
// normalizeBranchName — mobile keyboard friendly branch name normalization
// ---------------------------------------------------------------------------

describe("normalizeBranchName", () => {
  test("lowercases and replaces spaces with hyphens (typical mobile autocorrect output)", () => {
    expect(normalizeBranchName("Feature Auth Fix")).toBe("feature-auth-fix");
    expect(normalizeBranchName("API Rewrite")).toBe("api-rewrite");
  });

  test("preserves slashes for namespaced branches", () => {
    expect(normalizeBranchName("feature/auth-fix")).toBe("feature/auth-fix");
    expect(normalizeBranchName("Feature/Auth Fix")).toBe("feature/auth-fix");
  });

  test("trims leading and trailing whitespace", () => {
    expect(normalizeBranchName("  feature-x  ")).toBe("feature-x");
    expect(normalizeBranchName("\tfeature\n")).toBe("feature");
  });

  test("collapses multiple spaces into a single hyphen", () => {
    expect(normalizeBranchName("a   b    c")).toBe("a-b-c");
  });

  test("strips disallowed characters (smart quotes, emojis) but keeps Latin diacritics decomposed", () => {
    // Mobile autocorrect on iOS often turns hyphens into en-dashes — strip them
    expect(normalizeBranchName("feature—auth")).toBe("featureauth");
    // Latin diacritics decompose to their base letter (NFD + strip marks),
    // so Czech / French / German / Polish branch names stay readable instead
    // of getting half their letters dropped.
    expect(normalizeBranchName("česká-větev")).toBe("ceska-vetev");
    expect(normalizeBranchName("feature/Müller-naïve")).toBe("feature/muller-naive");
    expect(normalizeBranchName("PŘIDAT něco")).toBe("pridat-neco");
    // Emojis vanish
    expect(normalizeBranchName("feature 🚀 launch")).toBe("feature-launch");
  });

  test("collapses repeated separators", () => {
    expect(normalizeBranchName("a---b///c")).toBe("a-b/c");
    expect(normalizeBranchName("feature  /  auth")).toBe("feature/auth");
  });

  test("strips leading/trailing hyphens, slashes, and dots", () => {
    expect(normalizeBranchName("-feature-")).toBe("feature");
    expect(normalizeBranchName("/feature/")).toBe("feature");
    expect(normalizeBranchName(".feature.")).toBe("feature");
  });

  test("returns empty string for input that has no valid characters", () => {
    expect(normalizeBranchName("")).toBe("");
    expect(normalizeBranchName("   ")).toBe("");
    expect(normalizeBranchName("!!!")).toBe("");
    expect(normalizeBranchName("---")).toBe("");
  });

  test("preserves dots, underscores, and digits", () => {
    expect(normalizeBranchName("v1.2.3")).toBe("v1.2.3");
    expect(normalizeBranchName("fix_issue_42")).toBe("fix_issue_42");
  });
});

// ---------------------------------------------------------------------------
// getSnapshot
// ---------------------------------------------------------------------------

describe("TelegramManager.getSnapshot", () => {
  test("returns configured status when token exists", () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const snap = manager.getSnapshot();
    expect(snap.connections).toHaveLength(1);
    expect(snap.connections[0].status).toBe("configured");
    expect(snap.connections[0].id).toBe("tg-1");
  });

  test("returns missing-token status when credential is absent", () => {
    const cred = makeCredentialStore({}); // no secrets
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const snap = manager.getSnapshot();
    expect(snap.connections[0].status).toBe("missing-token");
  });

  test("returns empty connections when none configured", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([]);
    expect(manager.getSnapshot().connections).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// configure — filters disabled / incomplete connections
// ---------------------------------------------------------------------------

describe("TelegramManager.configure", () => {
  test("filters out disabled connections", () => {
    const cred = makeCredentialStore({ "cred:tg-1": "tok" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ id: "tg-1", enabled: true }), makeConnection({ id: "tg-2", enabled: false })]);
    expect(manager.getSnapshot().connections).toHaveLength(1);
    expect(manager.getSnapshot().connections[0].id).toBe("tg-1");
  });

  test("filters out connections with empty botTokenRef", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ botTokenRef: "" })]);
    expect(manager.getSnapshot().connections).toHaveLength(0);
  });

  test("filters out connections with empty chatId", () => {
    const cred = makeCredentialStore({ "cred:tg-1": "tok" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ chatId: "" })]);
    expect(manager.getSnapshot().connections).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// pollOffsets are per-connection
// ---------------------------------------------------------------------------

describe("pollOffsets per-connection", () => {
  test("each connection tracks its own offset independently", async () => {
    const cred = makeCredentialStore({
      "cred:tg-1": "token1",
      "cred:tg-2": "token2",
    });
    const manager = new TelegramManager({ credentialStore: cred });

    const polledConnIds: string[] = [];
    const polledOffsets: number[] = [];

    // Patch _pollConnection on the instance to record what offsets each
    // connection would use — this avoids the network call entirely.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._pollConnection = async (conn: TelegramConnectionConfig) => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const offset = (manager as any).pollOffsets.get(conn.id) ?? 0;
      polledConnIds.push(conn.id);
      polledOffsets.push(offset);
    };

    manager.configure([
      makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1", chatId: "111" }),
      makeConnection({ id: "tg-2", botTokenRef: "cred:tg-2", chatId: "222", pollSeconds: 10 }),
    ]);

    // Set running=true so _poll doesn't exit early, and stop the schedule timer
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).running = true;
    // Also patch _scheduleNextPoll to prevent it from actually scheduling
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._scheduleNextPoll = () => {};

    // Access private _poll via casting
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._poll();

    // Both connections should have been polled
    expect(polledConnIds).toHaveLength(2);
    expect(polledConnIds).toContain("tg-1");
    expect(polledConnIds).toContain("tg-2");

    // Both should start with offset 0 (independent per-connection state)
    expect(polledOffsets[0]).toBe(0);
    expect(polledOffsets[1]).toBe(0);
  });

  test("pollOffsets map advances independently per connection", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1" }),
      makeConnection({ id: "tg-2", botTokenRef: "cred:tg-2" }),
    ]);

    // Simulate advancing offset for tg-1 only
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const offsets = (manager as any).pollOffsets as Map<string, number>;
    offsets.set("tg-1", 100);

    expect(offsets.get("tg-1")).toBe(100);
    // tg-2 should still be at 0 (undefined → defaults to 0)
    expect(offsets.get("tg-2") ?? 0).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// _scheduleNextPoll uses correct interval
// ---------------------------------------------------------------------------

describe("_scheduleNextPoll", () => {
  test("uses minimum pollSeconds of all connections (not capped at 5s)", () => {
    vi.useFakeTimers();
    const cred = makeCredentialStore({ "cred:tg-1": "tok" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ pollSeconds: 30 })]);
    manager.start();

    // Should not fire before 30s
    vi.advanceTimersByTime(15000);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pollTimer).not.toBeNull();

    manager.stop();
    vi.useRealTimers();
  });

  test("defaults to 30s when no connections configured", () => {
    vi.useFakeTimers();
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([]);
    manager.start();

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pollTimer).not.toBeNull();

    manager.stop();
    vi.useRealTimers();
  });

  test("uses minimum pollSeconds across multiple connections", () => {
    vi.useFakeTimers();
    const cred = makeCredentialStore({ "cred:tg-1": "tok1", "cred:tg-2": "tok2" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1", pollSeconds: 60 }),
      makeConnection({ id: "tg-2", botTokenRef: "cred:tg-2", pollSeconds: 10 }),
    ]);

    // Patch _poll to be a no-op to avoid actual API calls
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._poll = vi.fn().mockResolvedValue(undefined);

    manager.start();

    // Should fire at 10s (minimum of 60 and 10)
    vi.advanceTimersByTime(10001);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any)._poll).toHaveBeenCalled();

    manager.stop();
    vi.useRealTimers();
  });
});

// ---------------------------------------------------------------------------
// Pending request state machine: task-description → confirm-action
// ---------------------------------------------------------------------------

describe("pending request state machine", () => {
  test("start-task button creates task-description pending request", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentMessages: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string, body: Record<string, unknown>) => {
      sentMessages.push({ method, body });
      return { ok: true, result: { message_id: 99 } };
    };

    const conn = makeConnection();
    const token = "token123";
    const chatId = "12345";

    // Seed alert context for the button-attached message — callback_data is
    // now just an action code; full context comes from contextByMessageId.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(42, {
      context: {
        alertId: "alert-1",
        workspaceId: "ws-1",
        panelId: "panel-1",
        kind: "completed",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // Simulate clicking "start-task" callback button (action code "s")
    const query = {
      id: "cq-1",
      from: { id: 1 },
      message: {
        message_id: 42,
        chat: { id: 12345 },
        text: "",
      },
      data: "s",
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, conn, token);

    // Should set pending task-description request
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending).toBeDefined();
    expect(pending.type).toBe("task-description");
    expect(pending.workspaceId).toBe("ws-1");
  });

  test("text reply after task-description creates confirm-action pending", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentMessages: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string, _body: Record<string, unknown>) => {
      sentMessages.push(method);
      return { ok: true, result: { message_id: 100 } };
    };

    const conn = makeConnection();
    const token = "token123";
    const chatId = "12345";

    // Seed task-description pending
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "task-description",
      workspaceId: "ws-1",
      panelId: "panel-1",
      createdAt: Date.now(),
    });

    const msg = {
      message_id: 50,
      chat: { id: 12345 },
      text: "Fix the login bug",
    };

    const emitted: unknown[] = [];
    manager.on("command", (cmd) => emitted.push(cmd));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, conn, token);

    // Should now have confirm-action pending (NOT emitted yet)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending).toBeDefined();
    expect(pending.type).toBe("confirm-action");
    expect(pending.pendingCmd?.type).toBe("start-task");
    expect(pending.pendingCmd?.taskDescription).toBe("Fix the login bug");
    // Should NOT have emitted command yet
    expect(emitted).toHaveLength(0);
  });

  test("confirm callback emits command and clears pending", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 101 } });

    const conn = makeConnection();
    const token = "token123";
    const chatId = "12345";

    const pendingCmd = {
      type: "start-task" as const,
      workspaceId: "ws-1",
      panelId: "panel-1",
      taskDescription: "Fix the login bug",
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "confirm-action",
      workspaceId: "ws-1",
      panelId: "panel-1",
      createdAt: Date.now(),
      pendingCmd,
    });

    const emitted: unknown[] = [];
    manager.on("command", (cmd) => emitted.push(cmd));

    const query = {
      id: "cq-2",
      from: { id: 1 },
      message: { message_id: 55, chat: { id: 12345 }, text: "" },
      data: "c",
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, conn, token);

    expect(emitted).toHaveLength(1);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((emitted[0] as any).type).toBe("start-task");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
  });

  test("cancel callback clears pending and sends cancelled message", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 102 } };
    };

    const conn = makeConnection();
    const token = "token123";
    const chatId = "12345";

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "confirm-action",
      workspaceId: "ws-1",
      panelId: "panel-1",
      createdAt: Date.now(),
      pendingCmd: { type: "start-task", workspaceId: "ws-1", panelId: "panel-1" },
    });

    const emitted: unknown[] = [];
    manager.on("command", (cmd) => emitted.push(cmd));

    const query = {
      id: "cq-3",
      from: { id: 1 },
      message: { message_id: 56, chat: { id: 12345 }, text: "" },
      data: "x",
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, conn, token);

    expect(emitted).toHaveLength(0);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
    expect(sentTexts.some((t) => t.includes("Cancelled"))).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// detectChats — auto-discovery of chat IDs from getUpdates
// ---------------------------------------------------------------------------

describe("detectChats", () => {
  test("returns single chat when bot has one recent conversation", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });

    const calls: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string) => {
      calls.push(method);
      if (method === "getMe") {
        return { ok: true, result: { username: "strIDEtermbot", first_name: "strIDEterm" } };
      }
      if (method === "getUpdates") {
        return {
          ok: true,
          result: [
            {
              update_id: 1,
              message: {
                message_id: 10,
                chat: { id: 8429137159, type: "private", first_name: "jarek" },
                text: "/start",
                from: { id: 8429137159, first_name: "jarek" },
              },
            },
          ],
        };
      }
      return { ok: false };
    };

    const result = await manager.detectChats({ botToken: "tok" });
    expect(result.botUsername).toBe("strIDEtermbot");
    expect(result.chats).toHaveLength(1);
    expect(result.chats[0].chatId).toBe("8429137159");
    expect(result.chats[0].title).toBe("jarek");
    expect(result.chats[0].type).toBe("private");
    expect(result.chats[0].lastText).toBe("/start");
    expect(calls).toEqual(["getMe", "getUpdates"]);
  });

  test("dedupes the same chat across multiple updates", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string) => {
      if (method === "getMe") return { ok: true, result: { username: "bot" } };
      if (method === "getUpdates") {
        return {
          ok: true,
          result: [
            {
              update_id: 1,
              message: { message_id: 1, chat: { id: 1, type: "private", first_name: "Alice" }, text: "hi" },
            },
            {
              update_id: 2,
              message: { message_id: 2, chat: { id: 1, type: "private", first_name: "Alice" }, text: "hello again" },
            },
          ],
        };
      }
      return { ok: false };
    };

    const result = await manager.detectChats({ botToken: "tok" });
    expect(result.chats).toHaveLength(1);
    // First message wins (the rest are dedup'd)
    expect(result.chats[0].lastText).toBe("hi");
  });

  test("returns multiple chats when bot has separate conversations", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string) => {
      if (method === "getMe") return { ok: true, result: { username: "bot" } };
      if (method === "getUpdates") {
        return {
          ok: true,
          result: [
            {
              update_id: 1,
              message: { message_id: 1, chat: { id: 1, type: "private", first_name: "Alice" }, text: "hi" },
            },
            {
              update_id: 2,
              message: { message_id: 2, chat: { id: -100123, type: "supergroup", title: "Team chat" }, text: "yo" },
            },
          ],
        };
      }
      return { ok: false };
    };

    const result = await manager.detectChats({ botToken: "tok" });
    expect(result.chats).toHaveLength(2);
    expect(result.chats.map((c) => c.chatId).sort()).toEqual(["-100123", "1"]);
  });

  test("returns empty list when bot has no recent messages", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string) => {
      if (method === "getMe") return { ok: true, result: { username: "bot" } };
      if (method === "getUpdates") return { ok: true, result: [] };
      return { ok: false };
    };

    const result = await manager.detectChats({ botToken: "tok" });
    expect(result.chats).toHaveLength(0);
    expect(result.botUsername).toBe("bot");
  });

  test("throws when bot token is invalid", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, method: string) => {
      if (method === "getMe") return { ok: false, description: "Unauthorized" };
      return { ok: false };
    };

    await expect(manager.detectChats({ botToken: "bad" })).rejects.toThrow(/Unauthorized/);
  });
});

// ---------------------------------------------------------------------------
// Forwarded-PR LRU: hasForwardedPr / markPrForwarded / forgetForwardedPr
// ---------------------------------------------------------------------------

describe("forwardedPrKeys LRU", () => {
  test("hasForwardedPr returns false until markPrForwarded is called", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    expect(manager.hasForwardedPr("pr-1")).toBe(false);
    manager.markPrForwarded("pr-1");
    expect(manager.hasForwardedPr("pr-1")).toBe(true);
  });

  test("forgetForwardedPr lets a PR be re-forwarded", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.markPrForwarded("pr-1");
    expect(manager.hasForwardedPr("pr-1")).toBe(true);
    manager.forgetForwardedPr("pr-1");
    expect(manager.hasForwardedPr("pr-1")).toBe(false);
  });

  test("LRU evicts the oldest entry when size cap is hit", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // The cap is internal (1000); reach it via a smaller stress to avoid slow tests.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const map = (manager as any).forwardedPrKeys as Map<string, number>;
    for (let i = 0; i < 1005; i++) {
      manager.markPrForwarded(`pr-${i}`);
    }
    expect(map.size).toBeLessThanOrEqual(1000);
    // The earliest insertions should have been dropped
    expect(manager.hasForwardedPr("pr-0")).toBe(false);
    expect(manager.hasForwardedPr("pr-1004")).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// callback_data shape: only short action codes (no JSON payload)
// ---------------------------------------------------------------------------

describe("callback_data is compact", () => {
  test("alert keyboard uses only single-character action codes", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const kb = (manager as any)._buildKeyboard({
      kind: "completed",
      workspaceId: "a".repeat(40),
      panelId: "b".repeat(40),
      title: "x",
    });
    expect(kb).not.toBeNull();
    for (const row of kb!) {
      for (const btn of row) {
        // Telegram's callback_data limit is 64 bytes; keep us well within.
        expect(btn.callback_data.length).toBeLessThanOrEqual(2);
      }
    }
  });

  test("PR alert keyboard fits within callback_data limit even with long workspace/prKey", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const kb = (manager as any)._buildKeyboard({
      kind: "review",
      workspaceId: "a".repeat(40),
      panelId: "b".repeat(40),
      prKey: "very-long-pr-key-".repeat(5),
      provider: "github",
      title: "x",
    });
    expect(kb).not.toBeNull();
    for (const row of kb!) {
      for (const btn of row) {
        expect(btn.callback_data.length).toBeLessThanOrEqual(2);
      }
    }
  });
});

// ---------------------------------------------------------------------------
// _buildAlertText — formatting of long technical detail (judge verdicts etc.)
// ---------------------------------------------------------------------------

describe("_buildAlertText detail formatting", () => {
  test("short one-line detail stays as italic for visual hierarchy", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "info",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Heads-up",
      detail: "prompt-returned",
    });
    expect(text).toContain("_prompt\\-returned_");
    expect(text).not.toContain("```");
  });

  test("long technical detail (judge verdict) renders as code block, not italic", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    const verdict =
      "Judge: All 1 requirements verified implemented: (1) Source file exists at src/foo/bar.ts:1 with helper (lines 3-11), 5-step process with concrete inputs and outputs (lines 13-33), and notes (lines 35-39); committed in f05334c. WORK_LOCK absent, TODO In Progress/Blocked empty, task moved to Done in TODO.md:14.";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "completed",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Task done",
      detail: verdict,
    });
    // Code block — preserves spacing and disables MarkdownV2 / auto-link
    // mangling of file paths and `(lines N-M)` patterns.
    expect(text).toContain("```");
    // The raw verdict text appears verbatim inside the fence (no escapes
    // applied, since code blocks treat content literally).
    expect(text).toContain("src/foo/bar.ts:1");
    // Italic detail wrapper must NOT have wrapped the verdict
    expect(text).not.toContain(`_${verdict}_`);
  });

  test("multi-line detail also renders as code block", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "info",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Output",
      detail: "line one\nline two\nline three",
    });
    expect(text).toContain("```");
    expect(text).toContain("line one");
    expect(text).toContain("line two");
  });

  test("backticks inside detail get escaped to keep the code fence intact", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    const longDetail = "user said `npm install` and then a much longer trailing string ".repeat(3);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "info",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Quoted output",
      detail: longDetail,
    });
    expect(text).toContain("```");
    expect(text).toContain("\\`npm install\\`");
  });

  test("includes profile / workspace / panel context lines so the screenshot is self-explanatory", () => {
    // With one global bot serving many profiles, the alert text is the only
    // way to tell at a glance "which profile / workspace / panel did this
    // come from" — important when forwarding the screenshot to a colleague.
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "completed",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Task done",
      workspaceProfileName: "MessageHub",
      workspaceName: "strideterm",
      panelTitle: "claude",
    });
    expect(text).toContain("MessageHub");
    expect(text).toContain("strideterm");
    expect(text).toContain("claude");
    // Profile line is its own row (compass), not crammed into the location row.
    expect(text.split("\n").some((line: string) => line.includes("🧭") && line.includes("MessageHub"))).toBe(true);
  });

  test("omits profile line when workspaceProfileName is missing (legacy callers)", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "completed",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Task done",
      workspaceName: "strideterm",
    });
    expect(text).not.toContain("🧭");
    expect(text).toContain("strideterm");
  });

  test("subagent_done renders with robot icon and minimal hint", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const text = (manager as any)._buildAlertText({
      kind: "subagent_done",
      workspaceId: "ws-1",
      panelId: "p-1",
      title: "Subagent done",
    });
    expect(text).toMatch(/🤖.*Subagent done/);
    expect(text).toContain("Sub\\-agent finished");
  });
});

// ---------------------------------------------------------------------------
// forwardAlert skips connections without matching forwardKinds
// ---------------------------------------------------------------------------

describe("forwardAlert", () => {
  test("skips connections whose forwardKinds does not include the alert kind", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "tok1", "cred:tg-2": "tok2" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1", forwardKinds: ["completed"] }),
      makeConnection({ id: "tg-2", botTokenRef: "cred:tg-2", forwardKinds: ["error"] }),
    ]);

    const calledWith: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendAlertToConnection = async (conn: TelegramConnectionConfig) => {
      calledWith.push(conn.id);
    };

    await manager.forwardAlert({
      workspaceId: "ws-1",
      panelId: "p-1",
      kind: "completed",
      title: "Done",
    });

    // Only tg-1 accepts "completed"
    expect(calledWith).toEqual(["tg-1"]);
  });

  test("forwards to all connections when forwardKinds is empty", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "tok1", "cred:tg-2": "tok2" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1", forwardKinds: [] }),
      makeConnection({ id: "tg-2", botTokenRef: "cred:tg-2", forwardKinds: [] }),
    ]);

    const calledWith: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendAlertToConnection = async (conn: TelegramConnectionConfig) => {
      calledWith.push(conn.id);
    };

    await manager.forwardAlert({
      workspaceId: "ws-1",
      panelId: "p-1",
      kind: "info",
      title: "Hello",
    });

    expect(calledWith).toEqual(["tg-1", "tg-2"]);
  });

  test("skips connection when no connections configured", async () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([]);

    // Should not throw or call anything
    await expect(
      manager.forwardAlert({ workspaceId: "ws-1", panelId: "p-1", kind: "completed", title: "Done" }),
    ).resolves.toBeUndefined();
  });

  test("filters connections by workspaceProfileId so chat A never receives chat B's alerts", async () => {
    // Two telegram connections, one per profile. A workspace in profile-b
    // fires an alert. Without the fix, both connections receive it —
    // including chat A, whose user has no idea what profile-b is.
    const cred = makeCredentialStore({ "cred:tg-default": "tok-default", "cred:tg-b": "tok-b" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-default", botTokenRef: "cred:tg-default", forwardKinds: [], profileId: "default" }),
      makeConnection({ id: "tg-b", botTokenRef: "cred:tg-b", forwardKinds: [], profileId: "profile-b" }),
    ]);

    const calledWith: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendAlertToConnection = async (conn: TelegramConnectionConfig) => {
      calledWith.push(conn.id);
    };

    await manager.forwardAlert({
      workspaceId: "ws-1",
      panelId: "p-1",
      kind: "info",
      title: "Hello from profile-b",
      workspaceProfileId: "profile-b",
    });

    expect(calledWith).toEqual(["tg-b"]);
  });

  test("global connections (profileId empty) receive every alert regardless of workspaceProfileId", async () => {
    // Post-2.2.15: empty profileId means "global" — the connection serves
    // every profile in the install. This is the recommended default for
    // single-bot users, who want one chat to surface alerts from every
    // profile (the profile name now ships in the message body).
    const cred = makeCredentialStore({ "cred:tg-global": "tok-g", "cred:tg-b": "tok-b" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-global", botTokenRef: "cred:tg-global", forwardKinds: [], profileId: "" }),
      makeConnection({ id: "tg-b", botTokenRef: "cred:tg-b", forwardKinds: [], profileId: "profile-b" }),
    ]);

    const calledWith: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendAlertToConnection = async (conn: TelegramConnectionConfig) => {
      calledWith.push(conn.id);
    };

    await manager.forwardAlert({
      workspaceId: "ws-a",
      panelId: "p-1",
      kind: "completed",
      title: "Hello from default",
      workspaceProfileId: "default",
    });
    await manager.forwardAlert({
      workspaceId: "ws-b",
      panelId: "p-2",
      kind: "completed",
      title: "Hello from profile-b",
      workspaceProfileId: "profile-b",
    });

    // The global connection receives both. The profile-b-bound one only
    // sees its own profile's alert.
    expect(calledWith.filter((id) => id === "tg-global")).toHaveLength(2);
    expect(calledWith.filter((id) => id === "tg-b")).toEqual(["tg-b"]);
  });

  test("getSnapshot reports needsProfileBinding=false in all cases (post-2.2.15 global default)", () => {
    // Pre-2.2.15 a multi-profile install with an unbound connection raised
    // a "Pick a profile" warning. Now empty profileId is an intentional
    // configuration (global delivery), so the flag is always false. The
    // field is left in the snapshot shape for renderer backwards compat.
    const cred = makeCredentialStore({ "cred:tg-1": "tok1", "cred:tg-2": "tok2" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setProfilesGetter(() => [
      { id: "default", name: "Default" },
      { id: "profile-b", name: "Profile B" },
    ]);
    manager.configure([
      makeConnection({ id: "tg-bound", botTokenRef: "cred:tg-1", profileId: "profile-b" }),
      makeConnection({ id: "tg-unbound", botTokenRef: "cred:tg-2", profileId: "" }),
    ]);

    const snapshot = manager.getSnapshot();
    for (const c of snapshot.connections) {
      expect(c.needsProfileBinding).toBe(false);
    }
  });

  test("legacy alerts with no workspaceProfileId fan out to every connection (backwards compat)", async () => {
    // Older callers may not pass workspaceProfileId yet. To avoid silently
    // dropping their alerts, the filter is skipped when the payload doesn't
    // include the field at all.
    const cred = makeCredentialStore({ "cred:tg-default": "tok-default", "cred:tg-b": "tok-b" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([
      makeConnection({ id: "tg-default", botTokenRef: "cred:tg-default", forwardKinds: [], profileId: "default" }),
      makeConnection({ id: "tg-b", botTokenRef: "cred:tg-b", forwardKinds: [], profileId: "profile-b" }),
    ]);

    const calledWith: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendAlertToConnection = async (conn: TelegramConnectionConfig) => {
      calledWith.push(conn.id);
    };

    await manager.forwardAlert({
      workspaceId: "ws-1",
      panelId: "p-1",
      kind: "info",
      title: "Legacy alert",
      // no workspaceProfileId — caller hasn't migrated
    });

    expect(calledWith.sort()).toEqual(["tg-b", "tg-default"]);
  });
});

// ---------------------------------------------------------------------------
// setWorkspacesGetter — /status and /workspaces commands
// ---------------------------------------------------------------------------

function makeWorkspace(overrides: Partial<TelegramWorkspaceInfo> = {}): TelegramWorkspaceInfo {
  return {
    id: "ws-1",
    name: "myproject",
    cwd: "/home/user/myproject",
    kind: "workspace",
    panels: [{ id: "panel-1", title: "bash" }],
    task: null,
    ...overrides,
  };
}

describe("setWorkspacesGetter", () => {
  test("/status lists task workspaces with state and cwd", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "ws-task",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "running", description: "Fix the authentication bug" },
      }),
      makeWorkspace({ id: "ws-plain", kind: "workspace", task: null }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 200 } };
    };

    const msg = { message_id: 1, chat: { id: 12345 }, text: "/status" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentTexts.length).toBeGreaterThan(0);
    const text = sentTexts[0];
    expect(text).toContain("fix\\-auth");
    expect(text).toContain("running");
    // Paths inside backtick spans use escapeInlineCode — dashes are NOT escaped there
    expect(text).toContain("/projects/fix-auth");
  });

  test("/status reports no tasks when none running", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [makeWorkspace({ kind: "workspace", task: null })]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 201 } };
    };

    const msg = { message_id: 2, chat: { id: 12345 }, text: "/status" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentTexts[0]).toContain("No task agents are running");
  });

  test("/status is scoped to the connection profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "personal-task",
        name: "personal-task",
        kind: "task",
        cwd: "/projects/personal",
        profileId: "personal",
        task: { state: "running", description: "Personal" },
      }),
      makeWorkspace({
        id: "work-task",
        name: "work-task",
        kind: "task",
        cwd: "/projects/work",
        profileId: "work",
        task: { state: "running", description: "Work" },
      }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 2031 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 21, chat: { id: 12345 }, text: "/status" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    expect(sentTexts[0]).toContain("work\\-task");
    expect(sentTexts[0]).not.toContain("personal\\-task");
  });

  test("/workspaces puts starred entries first and prefixes them with a star", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-1", name: "zeta", cwd: "/p/zeta" }),
      makeWorkspace({ id: "ws-2", name: "beta", cwd: "/p/beta", starred: true }),
      makeWorkspace({ id: "ws-3", name: "alpha", cwd: "/p/alpha" }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 600 } };
    };

    const msg = { message_id: 99, chat: { id: 12345 }, text: "/workspaces" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    const text = sentTexts[0] || "";
    const betaIdx = text.indexOf("beta");
    const alphaIdx = text.indexOf("alpha");
    const zetaIdx = text.indexOf("zeta");
    // beta is starred so it appears before alpha and zeta even though it
    // comes later alphabetically among starred items (only one starred here).
    expect(betaIdx).toBeGreaterThan(0);
    expect(betaIdx).toBeLessThan(alphaIdx);
    expect(alphaIdx).toBeLessThan(zetaIdx);
    expect(text).toContain("⭐");
  });

  test("sortWorkspacesStarredFirst: starred ⭐ first, then alphabetical", () => {
    const sorted = sortWorkspacesStarredFirst([
      { name: "zeta" },
      { name: "alpha" },
      { name: "Mike", starred: true },
      { name: "Bob", starred: true },
    ] as TelegramWorkspaceInfo[]);
    expect(sorted.map((s) => s.name)).toEqual(["Bob", "Mike", "alpha", "zeta"]);
  });

  test("/workspaces lists all workspaces with cwd", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/projects/alpha", kind: "workspace" }),
      makeWorkspace({
        id: "ws-2",
        name: "beta",
        cwd: "/projects/beta",
        kind: "task",
        task: { state: "running", description: "do stuff" },
      }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 202 } };
    };

    const msg = { message_id: 3, chat: { id: 12345 }, text: "/workspaces" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    const text = sentTexts[0];
    expect(text).toContain("alpha");
    expect(text).toContain("beta");
    expect(text).toContain("/projects/alpha");
    expect(text).toContain("/projects/beta");
  });

  test("/workspaces is scoped to the connection profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "personal-ws", name: "personal", kind: "manual", cwd: "/p/personal", profileId: "personal" }),
      makeWorkspace({ id: "work-ws", name: "work", kind: "manual", cwd: "/p/work", profileId: "work" }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 2032 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 31, chat: { id: 12345 }, text: "/workspaces" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    expect(sentTexts[0]).toContain("work");
    expect(sentTexts[0]).not.toContain("personal");
  });

  test("/task prompts workspace selection and stores pending", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/projects/alpha", kind: "workspace" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 203 } });

    const msg = { message_id: 4, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending).toBeDefined();
    expect(pending.type).toBe("workspace-selection");
    expect(pending.workspaceChoices).toHaveLength(1);
    expect(pending.workspaceChoices[0].id).toBe("ws-1");
  });

  test("workspace-selection: number reply transitions to worktree-mode-selection", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const ws = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/projects/alpha", kind: "workspace" });
    manager.setWorkspacesGetter(() => [ws]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 204 } });

    // Seed workspace-selection pending
    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "workspace-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws],
    });

    const msg = { message_id: 5, chat: { id: 12345 }, text: "1" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // After workspace pick, the user is asked HOW the task should run
    // (directly / new worktree / existing). Description is collected later.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending).toBeDefined();
    expect(pending.type).toBe("worktree-mode-selection");
    expect(pending.workspaceId).toBe("ws-1");
    expect(pending.draftTask?.parentWorkspaceId).toBe("ws-1");
    expect(pending.draftTask?.parentCwd).toBe("/projects/alpha");
  });

  test("/task is rate-limited when fired twice in quick succession", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/projects/alpha" })]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 300 } };
    };

    const conn = makeConnection();
    const msg1 = { message_id: 10, chat: { id: 12345 }, text: "/task" };
    const msg2 = { message_id: 11, chat: { id: 12345 }, text: "/task" };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg1, conn, "token123");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg2, conn, "token123");

    // Second call should produce a rate-limit response
    const rateLimitedHit = sentTexts.some((t) => t.includes("Wait") && t.includes("before starting another"));
    expect(rateLimitedHit).toBe(true);
  });

  test("workspace-selection: invalid number sends error and clears pending", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_token: string, _method: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 205 } };
    };

    const ws = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/projects/alpha" });
    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "workspace-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws],
    });

    const msg = { message_id: 6, chat: { id: 12345 }, text: "99" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentTexts[0]).toContain("Invalid choice");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// /task candidates filter — only true top-level workspaces are valid parents
// ---------------------------------------------------------------------------

describe("/task candidate filter", () => {
  test("excludes review/quickfix/task children and provider inboxes", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "root-1", name: "main-repo", kind: "manual", cwd: "/projects/main" }),
      makeWorkspace({ id: "root-2", name: "other-repo", kind: "manual", cwd: "/projects/other" }),
      // Children — must be excluded
      makeWorkspace({
        id: "task-child",
        name: "main-repo / task-1",
        kind: "task",
        cwd: "/projects/main",
        parentWorkspaceId: "root-1",
        task: { state: "running", description: "x" },
      }),
      makeWorkspace({
        id: "review-child",
        name: "main-repo / pr-42",
        kind: "manual",
        cwd: "/projects/main",
        parentWorkspaceId: "root-1",
      }),
      // Provider inboxes / docker — must be excluded
      makeWorkspace({ id: "azure-inbox", name: "Azure", kind: "azure", cwd: "" }),
      makeWorkspace({ id: "github-inbox", name: "GitHub", kind: "github", cwd: "" }),
      makeWorkspace({ id: "docker-inbox", name: "Docker", kind: "docker", cwd: "" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 400 } });

    const msg = { message_id: 100, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["root-1", "root-2"]);
  });

  test("excludes workspaces without cwd", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "with-cwd", name: "real", kind: "manual", cwd: "/projects/real" }),
      makeWorkspace({ id: "no-cwd", name: "empty", kind: "manual", cwd: "" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 401 } });

    const msg = { message_id: 101, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["with-cwd"]);
  });

  test("excludes worktree children (any non-empty parentWorkspaceId acts as the 'is child' marker)", async () => {
    // Worktree children look top-level — empty parentWorkspaceId on the
    // state model — but the runtime workspaces-getter sets a synthetic
    // parentWorkspaceId marker (e.g. "__worktree__") for them so the filter
    // catches both real children and worktree children with one rule.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "root-1", name: "main", kind: "manual", cwd: "/projects/main" }),
      makeWorkspace({
        id: "worktree-child",
        name: "main / feature-x",
        kind: "manual",
        cwd: "/projects/main/.strideterm/tree/feature-x",
        parentWorkspaceId: "__worktree__",
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 402 } });

    const msg = { message_id: 102, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["root-1"]);
  });

  test("active profile filter: only workspaces in the current profile are candidates", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "active-1", name: "active-only", kind: "manual", cwd: "/p/a", profileId: "work" }),
      makeWorkspace({ id: "other-1", name: "other-profile", kind: "manual", cwd: "/p/b", profileId: "personal" }),
      makeWorkspace({ id: "default-1", name: "no-profile", kind: "manual", cwd: "/p/c" }),
    ]);
    manager.setActiveProfileGetter(() => "work");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 403 } });

    const msg = { message_id: 103, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["active-1"]);
  });

  test("connection profile filter overrides the global active profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "personal-ws", name: "personal", kind: "manual", cwd: "/p/personal", profileId: "personal" }),
      makeWorkspace({ id: "work-ws", name: "work", kind: "manual", cwd: "/p/work", profileId: "work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 405 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 105, chat: { id: 12345 }, text: "/task" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("workspace-selection");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["work-ws"]);
  });

  test("unbound connection prompts for a profile when multiple desktop profiles are open", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setWindowSlotsGetter(() => [
      { id: "win-personal", profileId: "personal" },
      { id: "win-work", profileId: "work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "personal-ws", name: "personal", kind: "manual", cwd: "/p/personal", profileId: "personal" }),
      makeWorkspace({ id: "work-ws", name: "work", kind: "manual", cwd: "/p/work", profileId: "work" }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 406 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 106, chat: { id: 12345 }, text: "/task" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("profile-selection");
    expect(sentTexts.some((text) => text.includes("Pick a profile"))).toBe(true);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 107, chat: { id: 12345 }, text: "2" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("workspace-selection");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["work-ws"]);
  });

  test("default profile: workspaces with no explicit profileId are treated as 'default'", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", kind: "manual", cwd: "/p/a" })]);
    // No active profile getter set — falls back to "default"
    // and workspaces without profileId also default to "default"

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 404 } });

    const msg = { message_id: 104, chat: { id: 12345 }, text: "/task" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["ws-1"]);
  });
});

// ---------------------------------------------------------------------------
// Worktree mode selection — direct / new worktree / existing worktree
// ---------------------------------------------------------------------------

describe("worktree mode selection (post /task pick)", () => {
  test("after picking workspace, bot offers 'directly / new worktree / existing'", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const parent = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/alpha", kind: "manual" });
    const wt = makeWorkspace({
      id: "ws-1-wt",
      name: "alpha / feature-x",
      cwd: "/p/alpha/.strideterm/tree/feature-x",
      kind: "manual",
      notes: "Worktree of alpha",
      parentWorkspaceId: "__worktree__",
    });
    manager.setWorkspacesGetter(() => [parent, wt]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 800 } };
    };

    // Seed workspace-selection and reply with "1"
    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "workspace-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [parent],
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 200, chat: { id: 12345 }, text: "1" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("worktree-mode-selection");
    expect(pending?.worktreeChoices).toHaveLength(1);
    expect(pending?.worktreeChoices?.[0]?.id).toBe("ws-1-wt");

    // The mode menu message must include the three primary callbacks
    const lastWithKeyboard = [...sentBodies].reverse().find((b) => b.reply_markup);
    const markup = lastWithKeyboard!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks).toContain("m:n");
    expect(callbacks).toContain("m:d");
    expect(callbacks).toContain("m:e"); // existing-worktree button only when worktrees exist
  });

  test("existing-worktree picker excludes worktrees that belong to a different profile", async () => {
    // Two profiles each have a workspace named "alpha". The Telegram bot
    // is talking to profile-b. /task → /existing must not surface
    // profile-default's alpha worktrees in profile-b's picker — picking
    // one would set targetCwd to a path that profile-b doesn't own.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "profile-b" })]);

    const parentDefault = makeWorkspace({
      id: "ws-default",
      name: "alpha",
      cwd: "/p/alpha",
      profileId: "default",
      kind: "manual",
    });
    const wtDefault = makeWorkspace({
      id: "ws-default-wt",
      name: "alpha / feature-default",
      cwd: "/p/alpha/.strideterm/tree/feature-default",
      profileId: "default",
      kind: "manual",
      notes: "Worktree of alpha",
      parentWorkspaceId: "__worktree__",
    });
    const parentB = makeWorkspace({
      id: "ws-b",
      name: "alpha",
      cwd: "/p/alpha",
      profileId: "profile-b",
      kind: "manual",
    });
    const wtB = makeWorkspace({
      id: "ws-b-wt",
      name: "alpha / feature-b",
      cwd: "/p/alpha/.strideterm/tree/feature-b",
      profileId: "profile-b",
      kind: "manual",
      notes: "Worktree of alpha",
      parentWorkspaceId: "__worktree__",
    });
    manager.setWorkspacesGetter(() => [parentDefault, wtDefault, parentB, wtB]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const result = (manager as any)._findExistingWorktrees(parentB) as Array<{ id: string }>;
    expect(result.map((r) => r.id).sort()).toEqual(["ws-b-wt"]);
  });

  test("clicking 'directly' (m:d) goes to task-description with useWorktree=false", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/alpha", kind: "manual" })]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 801 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-mode-selection",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: false,
      },
    });

    const query = {
      id: "cq-d",
      from: { id: 1 },
      message: { message_id: 80, chat: { id: 12345 }, text: "" },
      data: "m:d",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("task-description");
    expect(pending?.draftTask?.useWorktree).toBe(false);
  });

  test("clicking 'new worktree' (m:n) transitions to worktree-branch-input", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 802 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-mode-selection",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: false,
      },
    });

    const query = {
      id: "cq-n",
      from: { id: 1 },
      message: { message_id: 81, chat: { id: 12345 }, text: "" },
      data: "m:n",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("worktree-branch-input");
    expect(pending?.draftTask?.useWorktree).toBe(true);
  });

  test("worktree-branch-input: valid branch → task-description with branch in draftTask", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 803 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-branch-input",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: true,
      },
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 90, chat: { id: 12345 }, text: "feature/auth-fix" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("task-description");
    expect(pending?.draftTask?.useWorktree).toBe(true);
    expect(pending?.draftTask?.worktreeBranch).toBe("feature/auth-fix");
  });

  test("worktree-branch-input: spaces and capitals get normalized (mobile keyboard friendly)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 804 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-branch-input",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: true,
      },
    });

    // Typical mobile autocorrect output: capitalized, with spaces
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 91, chat: { id: 12345 }, text: "Bad Branch With Spaces" },
      makeConnection(),
      "token123",
    );

    // Should normalize to a valid branch and proceed to task-description
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("task-description");
    expect(pending?.draftTask?.worktreeBranch).toBe("bad-branch-with-spaces");
  });

  test("worktree-branch-input: input with no valid chars still gets rejected and re-asks", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 8041 } };
    };

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-branch-input",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: true,
      },
    });

    // Pure punctuation/emoji — nothing valid survives normalization
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 911, chat: { id: 12345 }, text: "!!! 🚀 ???" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("worktree-branch-input");
    expect(sentTexts.some((t) => t.includes("Invalid branch name"))).toBe(true);
  });

  test("description after worktree-branch flows into start-task with useWorktree=true", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/alpha", kind: "manual" })]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 805 } });

    const chatId = "12345";
    // Seed task-description with worktree draft (as if user already typed branch)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "task-description",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: true,
        worktreeBranch: "feature/x",
      },
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 92, chat: { id: 12345 }, text: "Build feature X" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("confirm-action");
    expect(pending?.pendingCmd?.type).toBe("start-task");
    expect(pending?.pendingCmd?.useWorktree).toBe(true);
    expect(pending?.pendingCmd?.worktreeBranch).toBe("feature/x");
    expect(pending?.pendingCmd?.taskDescription).toBe("Build feature X");
  });

  test("clicking 'existing worktree' (m:e) lists worktree options as buttons", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 806 } };
    };

    const chatId = "12345";
    const wt1 = makeWorkspace({
      id: "wt-1",
      name: "alpha / feat-x",
      cwd: "/p/alpha/.strideterm/tree/feat-x",
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-mode-selection",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: false,
      },
      worktreeChoices: [wt1],
    });

    const query = {
      id: "cq-e",
      from: { id: 1 },
      message: { message_id: 82, chat: { id: 12345 }, text: "" },
      data: "m:e",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("worktree-existing-pick");

    const lastWithKb = [...sentBodies].reverse().find((b) => b.reply_markup);
    const markup = lastWithKb!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks).toContain("m:x:0");
  });

  test("clicking 'Get file' (t:f:<wsId>) opens file-path-input pending", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "completed", description: "Fix login" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 900 } });

    const query = {
      id: "cq-getfile",
      from: { id: 1 },
      message: { message_id: 90, chat: { id: 12345 }, text: "" },
      data: "t:f:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("file-path-input");
    expect(pending?.workspaceId).toBe("task-1");
  });

  test("file-path-input: typing a path transitions to file-mode-selection (not direct emit)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 901 } };
    };

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "file-path-input",
      workspaceId: "task-1",
      panelId: "",
      createdAt: Date.now(),
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 95, chat: { id: 12345 }, text: "notes.md" },
      makeConnection(),
      "token123",
    );

    // No command yet — user still has to pick delivery mode
    expect(emitted).toHaveLength(0);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("file-mode-selection");
    expect(pending?.pendingFilePath).toBe("notes.md");
    // Mode menu was sent with both buttons
    const lastWithKb = [...sentBodies].reverse().find((b) => b.reply_markup);
    const markup = lastWithKb!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks).toContain("fm:a");
    expect(callbacks).toContain("fm:d");
  });

  test("file-mode 'auto' (fm:a) emits send-task-file with mode=auto", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 9011 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "file-mode-selection",
      workspaceId: "task-1",
      panelId: "",
      createdAt: Date.now(),
      pendingFilePath: "notes.md",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 100, chat: { id: 12345 }, text: "" }, data: "fm:a" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("send-task-file");
    expect(emitted[0].fileMode).toBe("auto");
    expect(emitted[0].filePath).toBe("notes.md");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
  });

  test("file-mode 'document' (fm:d) emits send-task-file with mode=document", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 9012 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "file-mode-selection",
      workspaceId: "task-1",
      panelId: "",
      createdAt: Date.now(),
      pendingFilePath: "notes.md",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 101, chat: { id: 12345 }, text: "" }, data: "fm:d" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("send-task-file");
    expect(emitted[0].fileMode).toBe("document");
  });

  test("file-path-input: whitespace-only text is dropped by upstream guard, no command emitted", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 902 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "file-path-input",
      workspaceId: "task-1",
      panelId: "",
      createdAt: Date.now(),
    });

    const emitted: unknown[] = [];
    manager.on("command", (cmd) => emitted.push(cmd));

    // _handleMessage trims and early-returns on empty text — never reaches
    // the file-path-input handler. Pending stays alive so the user's next
    // (real) reply still works.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 96, chat: { id: 12345 }, text: "   " },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(0);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("file-path-input");
  });

  test("picking existing worktree (m:x:<idx>) sets targetCwd in draftTask", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 807 } });

    const chatId = "12345";
    const wt = makeWorkspace({
      id: "wt-1",
      name: "alpha / feat-x",
      cwd: "/p/alpha/.strideterm/tree/feat-x",
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "worktree-existing-pick",
      workspaceId: "ws-1",
      panelId: "",
      createdAt: Date.now(),
      draftTask: {
        parentWorkspaceId: "ws-1",
        parentName: "alpha",
        parentCwd: "/p/alpha",
        useWorktree: false,
      },
      worktreeChoices: [wt],
    });

    const query = {
      id: "cq-x",
      from: { id: 1 },
      message: { message_id: 83, chat: { id: 12345 }, text: "" },
      data: "m:x:0",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("task-description");
    expect(pending?.draftTask?.targetCwd).toBe("/p/alpha/.strideterm/tree/feat-x");
    expect(pending?.draftTask?.useWorktree).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// Confirmed start-task carries chatId so the runtime can reply via Telegram
// ---------------------------------------------------------------------------

describe("emitted commands carry chatId", () => {
  test("confirm callback merges chatId onto pendingCmd before emitting", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 500 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "confirm-action",
      workspaceId: "ws-1",
      panelId: "p-1",
      createdAt: Date.now(),
      pendingCmd: {
        type: "start-task",
        workspaceId: "ws-1",
        panelId: "p-1",
        taskDescription: "do the thing",
      },
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const query = {
      id: "cq-confirm",
      from: { id: 1 },
      message: { message_id: 60, chat: { id: 12345 }, text: "" },
      data: "c",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].chatId).toBe(chatId);
    expect(emitted[0].type).toBe("start-task");
  });
});

// ---------------------------------------------------------------------------
// Interactive /status — task list with inline action buttons
// ---------------------------------------------------------------------------

describe("interactive /status with inline buttons", () => {
  test("/status sends inline keyboard with one button per task", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "running", description: "Fix login" },
      }),
      makeWorkspace({
        id: "task-2",
        name: "add-tests",
        kind: "task",
        cwd: "/projects/add-tests",
        task: { state: "paused", description: "Tests" },
      }),
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 600 } };
    };

    const msg = { message_id: 200, chat: { id: 12345 }, text: "/status" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies.length).toBeGreaterThan(0);
    const last = sentBodies[sentBodies.length - 1];
    const markup = last.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    expect(markup.inline_keyboard).toHaveLength(2);
    expect(markup.inline_keyboard[0][0].callback_data).toBe("t:m:task-1");
    expect(markup.inline_keyboard[1][0].callback_data).toBe("t:m:task-2");
  });

  test("clicking task action 'menu' (t:m:<wsId>) sends per-task action menu", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "paused", description: "Fix login" },
      }),
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 601 } };
    };

    const query = {
      id: "cq-menu",
      from: { id: 1 },
      message: { message_id: 61, chat: { id: 12345 }, text: "" },
      data: "t:m:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // Find the menu message (skip the answerCallbackQuery acknowledgement)
    const menuBody = sentBodies.find((b) => b.reply_markup);
    expect(menuBody).toBeDefined();
    const markup = menuBody!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const allCallbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    // Paused state should expose Resume / Edit / Edit+Start / Reset / Back
    expect(allCallbacks).toContain("t:r:task-1");
    expect(allCallbacks).toContain("t:e:task-1");
    expect(allCallbacks).toContain("t:g:task-1");
    expect(allCallbacks).toContain("t:x:task-1");
    expect(allCallbacks).toContain("t:b:");
  });

  test("clicking pause (t:p:<wsId>) emits pause-task command immediately", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "running", description: "Fix login" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 602 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const query = {
      id: "cq-pause",
      from: { id: 1 },
      message: { message_id: 62, chat: { id: 12345 }, text: "" },
      data: "t:p:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("pause-task");
    expect(emitted[0].workspaceId).toBe("task-1");
    expect(emitted[0].chatId).toBe("12345");
  });

  test("clicking reset (t:x:<wsId>) requires confirmation, does not emit immediately", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "paused", description: "Fix login" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 603 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const query = {
      id: "cq-reset",
      from: { id: 1 },
      message: { message_id: 63, chat: { id: 12345 }, text: "" },
      data: "t:x:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    expect(emitted).toHaveLength(0);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending).toBeDefined();
    expect(pending.type).toBe("confirm-action");
    expect(pending.pendingCmd.type).toBe("reset-task");
  });

  test("clicking edit+continue (t:c:<wsId>) sets task-edit-description pending with followUp=resume", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "running", description: "old description" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 604 } });

    const query = {
      id: "cq-edit",
      from: { id: 1 },
      message: { message_id: 64, chat: { id: 12345 }, text: "" },
      data: "t:c:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("task-edit-description");
    expect(pending?.workspaceId).toBe("task-1");
    expect(pending?.followUp).toBe("resume");
  });

  test("edit+continue from running state pre-pauses the task", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "running", description: "old description" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 604.1 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const query = {
      id: "cq-editc",
      from: { id: 1 },
      message: { message_id: 65, chat: { id: 12345 }, text: "" },
      data: "t:c:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    // The pre-pause is what makes the eventual `resume` after the description
    // edit actually do something: resumeTask only succeeds from paused state.
    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("pause-task");
    expect(emitted[0].workspaceId).toBe("task-1");
  });

  test("edit+continue from already-paused state does NOT emit a redundant pause", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/projects/fix-auth",
        task: { state: "paused", description: "old description" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 604.2 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const query = {
      id: "cq-editc",
      from: { id: 1 },
      message: { message_id: 66, chat: { id: 12345 }, text: "" },
      data: "t:c:task-1",
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(query, makeConnection(), "token123");

    expect(emitted).toHaveLength(0);
  });

  test("text reply during task-edit-description emits update-task-description with followUp", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 605 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "task-edit-description",
      workspaceId: "task-1",
      panelId: "panel-1",
      createdAt: Date.now(),
      followUp: "start",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const msg = { message_id: 70, chat: { id: 12345 }, text: "new improved task brief" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("update-task-description");
    expect(emitted[0].workspaceId).toBe("task-1");
    expect(emitted[0].taskDescription).toBe("new improved task brief");
    expect(emitted[0].followUp).toBe("start");
    expect(emitted[0].chatId).toBe(chatId);
    // Pending must be cleared after dispatch so a follow-up message doesn't
    // accidentally re-trigger the edit flow.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// sendFile — format-aware delivery (image / inline code / document)
// ---------------------------------------------------------------------------

describe("sendFile format detection", () => {
  // We don't actually hit the network — _uploadFile/_sendText are stubbed
  // to capture which path was taken for which file extension.
  const setupCapture = (manager: TelegramManager) => {
    const calls: Array<{ method: string; bodyText?: string; filename?: string; field?: string }> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._uploadFile = async (
      _token: string,
      method: string,
      _chat: string,
      field: string,
      _buf: Buffer,
      filename: string,
    ) => {
      calls.push({ method, filename, field });
    };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendText = async (_token: string, _chat: string, text: string) => {
      calls.push({ method: "sendMessage", bodyText: text });
    };
    return calls;
  };

  test("image extension → sendPhoto", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    const calls = setupCapture(manager);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._readFileForTest = true; // marker, not used
    // Stub fs to avoid hitting disk
    const fakeFs = { stat: async () => ({ isFile: () => true, size: 1234 }), readFile: async () => Buffer.from([0]) };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const origImport = (manager as any)._import;
    const sendFileSpy = manager.sendFile.bind(manager);
    // Patch dynamic import resolution — easiest: pre-load fs with a Proxy via globalThis is overkill;
    // instead, exploit the fact that node's import("node:fs/promises") returns a real module —
    // we'll just check that calling sendFile on a non-existent path triggers the read-fail branch
    // and exits cleanly. The format-detection branches are exercised by the unit tests below
    // that go through the manager via direct method call patching.
    void fakeFs;
    void origImport;
    void sendFileSpy;
    void calls;

    // Direct test: call sendFile for a path that won't exist; expect a graceful warning text.
    await manager.sendFile({
      chatId: "12345",
      absolutePath: "/definitely/does/not/exist/x.png",
      relPath: "x.png",
    });
    // sendText called with an error message
    expect(
      calls.some(
        (c) =>
          (c.method === "sendMessage" && (c.bodyText || "").includes("Cannot read file")) ||
          (c.bodyText || "").includes("Path does not exist"),
      ),
    ).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// /menu — interactive hub for mobile users
// ---------------------------------------------------------------------------

describe("/menu hub", () => {
  test("/menu sends inline keyboard with all top-level actions", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" }),
      makeWorkspace({
        id: "task-1",
        name: "running-task",
        kind: "task",
        cwd: "/p/t",
        task: { state: "running", description: "x" },
      }),
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2000 } };
    };

    const msg = { message_id: 400, chat: { id: 12345 }, text: "/menu" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    const markup = sentBodies[0].reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks).toContain("mn:status");
    expect(callbacks).toContain("mn:task");
    expect(callbacks).toContain("mn:workspaces");
    expect(callbacks).toContain("mn:screenshot");
    expect(callbacks).toContain("mn:help");

    // Header must reflect live state (1 running task) so the user can see
    // at-a-glance what's going on without clicking anywhere.
    const text = sentBodies[0].text as string;
    expect(text).toContain("Running: *1*");
  });

  test("plain 'menu' (without slash) works — mobile keyboards make / hard to type", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => []);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2010 } };
    };

    // No slash, mixed case, surrounding whitespace — should all be tolerated
    for (const variant of ["menu", "Menu", "MENU", " menu ", "/MENU"]) {
      sentBodies.length = 0;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await (manager as any)._handleMessage(
        { message_id: 410, chat: { id: 12345 }, text: variant },
        makeConnection(),
        "token123",
      );
      expect(sentBodies.some((b) => (b.reply_markup as { inline_keyboard?: unknown })?.inline_keyboard)).toBe(true);
    }
  });

  test("plain command words (without slash) reach all top-level handlers", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" })]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2011 } };
    };

    // Each command word triggers its handler. We just verify a message
    // was sent (the handler-specific assertions live in their own tests).
    for (const word of ["status", "workspaces", "task", "screenshot", "help"]) {
      const before = sentBodies.length;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await (manager as any)._handleMessage(
        { message_id: 411, chat: { id: 12345 }, text: word },
        makeConnection(),
        "token123",
      );
      expect(sentBodies.length).toBeGreaterThan(before);
    }
  });

  test("/start aliases /menu (Telegram bot convention for first interaction)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => []);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2001 } };
    };

    const msg = { message_id: 401, chat: { id: 12345 }, text: "/start" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    expect(sentBodies[0].reply_markup).toBeDefined();
  });

  test("/menu summary is global and does not resolve a profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "personal-task",
        name: "personal",
        kind: "task",
        cwd: "/p/personal",
        profileId: "personal",
        task: { state: "running", description: "Personal" },
      }),
      makeWorkspace({
        id: "work-task",
        name: "work",
        kind: "task",
        cwd: "/p/work",
        profileId: "work",
        task: { state: "running", description: "Work" },
      }),
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2001.1 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 402, chat: { id: 12345 }, text: "/menu" },
      makeConnection(),
      "token123",
    );

    const text = String(sentBodies[0].text || "");
    expect(text).toContain("Profiles: *2*");
    expect(text).toContain("Running: *2*");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.get("12345")).toBeUndefined();
  });

  test("clicking mn:status dispatches to /status handler (lists task agents)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "alpha",
        kind: "task",
        cwd: "/p/a",
        task: { state: "running", description: "x" },
      }),
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 2002 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 120, chat: { id: 12345 }, text: "" }, data: "mn:status" },
      makeConnection(),
      "token123",
    );

    // Status sends the task list keyboard — buttons with t:m:<wsId>
    const lastWithKb = [...sentBodies].reverse().find((b) => b.reply_markup);
    const markup = lastWithKb!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks.some((c) => c.startsWith("t:m:"))).toBe(true);
  });

  test("clicking mn:task dispatches to /task handler (workspace-selection pending)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" })]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 2003 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 121, chat: { id: 12345 }, text: "" }, data: "mn:task" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("workspace-selection");
  });

  test("clicking mn:screenshot dispatches to /screenshot handler", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" })]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 2004 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 122, chat: { id: 12345 }, text: "" }, data: "mn:screenshot" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("screenshot-mode-selection");
  });

  test("unbound multi-profile menu button asks for profile at action time", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setWindowSlotsGetter(() => [
      { id: "win-personal", profileId: "personal" },
      { id: "win-work", profileId: "work" },
    ]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-work",
        name: "work task",
        kind: "task",
        cwd: "/p/work",
        profileId: "work",
        task: { state: "idle", description: "check work" },
      }),
    ]);

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      if (body.text) sentTexts.push(body.text as string);
      return { ok: true, result: { message_id: 2005 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 123, chat: { id: 12345 }, text: "" }, data: "mn:status" },
      makeConnection(),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("profile-selection");
    expect(sentTexts.some((text) => text.includes("Pick a profile"))).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// /screenshot flow — capture current or pick another workspace
// ---------------------------------------------------------------------------

describe("/screenshot flow", () => {
  test("/screenshot opens mode selection menu (Current / Pick)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" })]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 1000 } };
    };

    const msg = { message_id: 300, chat: { id: 12345 }, text: "/screenshot" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("screenshot-mode-selection");
    expect(pending?.workspaceChoices).toHaveLength(1);

    const lastWithKb = [...sentBodies].reverse().find((b) => b.reply_markup);
    const markup = lastWithKb!.reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    const callbacks = markup.inline_keyboard.flatMap((row) => row.map((b) => b.callback_data));
    expect(callbacks).toContain("ss:c");
    expect(callbacks).toContain("ss:w");
  });

  test("/screenshot picker is scoped to the connection profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setActiveProfileGetter(() => "personal");
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "personal-ws", name: "personal", cwd: "/p/personal", profileId: "personal" }),
      makeWorkspace({ id: "work-ws", name: "work", cwd: "/p/work", profileId: "work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1000.1 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 301, chat: { id: 12345 }, text: "/screenshot" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("screenshot-mode-selection");
    expect(pending?.workspaceChoices?.map((w: TelegramWorkspaceInfo) => w.id)).toEqual(["work-ws"]);
  });

  test("clicking Current (ss:c) emits screenshot-current immediately", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1001 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-mode-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [],
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 110, chat: { id: 12345 }, text: "" }, data: "ss:c" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].chatId).toBe(chatId);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any).pendingRequests.has(chatId)).toBe(false);
  });

  test("clicking Vybrat (ss:w) transitions to numbered workspace pick", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1002 } });

    const chatId = "12345";
    const ws1 = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" });
    const ws2 = makeWorkspace({ id: "ws-2", name: "beta", cwd: "/p/b" });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-mode-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws1, ws2],
    });

    const emitted: unknown[] = [];
    manager.on("command", (cmd) => emitted.push(cmd));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 111, chat: { id: 12345 }, text: "" }, data: "ss:w" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(0);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("screenshot-workspace-pick");
    expect(pending?.workspaceChoices).toHaveLength(2);
  });

  test("typing number 2 during screenshot-workspace-pick emits screenshot-workspace for chosen ws", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1003 } });

    const chatId = "12345";
    const ws1 = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" });
    const ws2 = makeWorkspace({ id: "ws-2", name: "beta", cwd: "/p/b" });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-workspace-pick",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws1, ws2],
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 305, chat: { id: 12345 }, text: "2" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-workspace");
    expect(emitted[0].workspaceId).toBe("ws-2");
    expect(emitted[0].chatId).toBe(chatId);
  });

  test("per-task menu screenshot button (t:s:<wsId>) emits screenshot-workspace", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/p/fix",
        task: { state: "running", description: "x" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1004 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 112, chat: { id: 12345 }, text: "" }, data: "t:s:task-1" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-workspace");
    expect(emitted[0].workspaceId).toBe("task-1");
  });

  test("/screenshot 1 emits screenshot-current with windowId resolved from first slot", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const slot1 = { id: "win-uuid-1", profileId: "profile-personal" };
    const slot2 = { id: "win-uuid-2", profileId: "profile-work" };
    manager.setWindowSlotsGetter(() => [slot1, slot2]);
    manager.setWorkspacesGetter(() => []);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1005 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const msg = { message_id: 301, chat: { id: 12345 }, text: "/screenshot 1" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].windowId).toBe("win-uuid-1");
  });

  test("/screenshot 2 emits screenshot-current with windowId resolved from second slot", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const slot1 = { id: "win-uuid-1", profileId: "profile-personal" };
    const slot2 = { id: "win-uuid-2", profileId: "profile-work" };
    manager.setWindowSlotsGetter(() => [slot1, slot2]);
    manager.setWorkspacesGetter(() => []);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1006 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    const msg = { message_id: 302, chat: { id: 12345 }, text: "/screenshot 2" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].windowId).toBe("win-uuid-2");
  });

  test("/screenshot ws-name resolves window via workspace profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const slot1 = { id: "win-uuid-1", profileId: "profile-personal" };
    const slot2 = { id: "win-uuid-2", profileId: "profile-work" };
    manager.setWindowSlotsGetter(() => [slot1, slot2]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-personal", name: "personal-project", profileId: "profile-personal" }),
      makeWorkspace({ id: "ws-work", name: "work-project", profileId: "profile-work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1007 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // /screenshot work-project → workspace in profile-work → slot2 (win-uuid-2)
    const msg = { message_id: 303, chat: { id: 12345 }, text: "/screenshot work-project" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].windowId).toBe("win-uuid-2");
  });

  test("/screenshot with out-of-range index falls back to primary (no windowId set)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWindowSlotsGetter(() => [{ id: "win-uuid-1", profileId: "profile-personal" }]);
    manager.setWorkspacesGetter(() => []);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1008 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // /screenshot 5 → only 1 slot, out of range → resolvedWindowId stays undefined
    const msg = { message_id: 304, chat: { id: 12345 }, text: "/screenshot 5" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].windowId).toBeUndefined();
  });

  // -------------------------------------------------------------------------
  // profile-routing emit contract — the user reported that picking a profile
  // in /screenshot then hitting "Current workspace" captured the wrong window.
  // Root cause was that emit sites dropped the profile scope; these tests
  // pin the contract so the regression cannot come back silently.
  // -------------------------------------------------------------------------

  test("/screenshot stores activeProfileId in pending so ss:c can scope the capture later", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);

    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setWindowSlotsGetter(() => [
      { id: "win-personal", profileId: "personal" },
      { id: "win-work", profileId: "work" },
    ]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "work-ws", name: "work", cwd: "/p/work", profileId: "work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1100 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 400, chat: { id: 12345 }, text: "/screenshot" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("screenshot-mode-selection");
    expect(pending?.activeProfileId).toBe("work");
  });

  test("ss:c emits screenshot-current with profileId from pending.activeProfileId", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1101 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-mode-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [],
      activeProfileId: "profile-work",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 401, chat: { id: 12345 }, text: "" }, data: "ss:c" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    // The critical assertion — runtime relies on this to resolve windowId.
    expect(emitted[0].profileId).toBe("profile-work");
  });

  test("ss:c emits profileId=undefined when pending has no activeProfileId (single-profile install)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1102 } });

    const chatId = "12345";
    // Legacy pending state without activeProfileId — runtime falls back to
    // primary window, which is correct for single-profile installs.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-mode-selection",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [],
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 402, chat: { id: 12345 }, text: "" }, data: "ss:c" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].profileId).toBeUndefined();
  });

  test("numbered pick during screenshot-workspace-pick emits screenshot-workspace with profileId from chosen workspace", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1103 } });

    const chatId = "12345";
    const ws1 = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a", profileId: "profile-work" });
    const ws2 = makeWorkspace({ id: "ws-2", name: "beta", cwd: "/p/b", profileId: "profile-work" });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-workspace-pick",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws1, ws2],
      activeProfileId: "profile-work",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 403, chat: { id: 12345 }, text: "2" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-workspace");
    expect(emitted[0].workspaceId).toBe("ws-2");
    expect(emitted[0].profileId).toBe("profile-work");
  });

  test("numbered pick falls back to pending.activeProfileId when chosen workspace has no profileId field", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1104 } });

    const chatId = "12345";
    const ws1 = makeWorkspace({ id: "ws-1", name: "alpha", cwd: "/p/a" }); // no profileId → "default" → fallback
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "screenshot-workspace-pick",
      workspaceId: "",
      panelId: "",
      createdAt: Date.now(),
      workspaceChoices: [ws1],
      activeProfileId: "profile-work",
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 404, chat: { id: 12345 }, text: "1" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    // chosen.profileId is empty → "default" precedence kicks in first.
    // Workspaces in screenshot pickers always come from a profile-filtered
    // list, so in practice they carry profileId. The fallback chain still
    // resolves: chosen.profileId || pending.activeProfileId || "default".
    expect(emitted[0].profileId).toBe("profile-work");
  });

  test("per-task screenshot button (t:s:<wsId>) emits with profileId from the task's workspace", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-1",
        name: "fix-auth",
        kind: "task",
        cwd: "/p/fix",
        profileId: "profile-work",
        task: { state: "running", description: "x" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1105 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      {
        id: "cq",
        from: { id: 1 },
        message: { message_id: 405, chat: { id: 12345 }, text: "" },
        data: "t:s:task-1",
      },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-workspace");
    expect(emitted[0].workspaceId).toBe("task-1");
    expect(emitted[0].profileId).toBe("profile-work");
  });

  test("per-task screenshot button emits profileId='default' when workspace has no explicit profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // No profileId on the workspace → should fall back to "default" so runtime
    // resolves to whichever slot is the default bucket (single-profile installs
    // and legacy state both rely on this).
    manager.setWorkspacesGetter(() => [
      makeWorkspace({
        id: "task-2",
        name: "no-profile-task",
        kind: "task",
        cwd: "/p/np",
        task: { state: "running", description: "x" },
      }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1106 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      {
        id: "cq",
        from: { id: 1 },
        message: { message_id: 406, chat: { id: 12345 }, text: "" },
        data: "t:s:task-2",
      },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].profileId).toBe("default");
  });

  test("/screenshot <ws-name> direct path keeps explicit windowId (does NOT need profileId)", async () => {
    // Regression guard: the direct /screenshot N | <name> path was correct
    // before this fix, and the resolver gives windowId precedence over
    // profileId. Make sure we didn't accidentally break it.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWindowSlotsGetter(() => [
      { id: "win-personal", profileId: "profile-personal" },
      { id: "win-work", profileId: "profile-work" },
    ]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-work", name: "work-project", profileId: "profile-work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 1107 } });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 407, chat: { id: 12345 }, text: "/screenshot work-project" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("screenshot-current");
    expect(emitted[0].windowId).toBe("win-work");
    // profileId is not set on the direct path — runtime uses windowId directly.
    expect(emitted[0].profileId).toBeUndefined();
  });
});

// ---------------------------------------------------------------------------
// open-pr-review profile-routing emit contract
// Same bug class as the screenshot routing — runtime needs profileId to open
// the PR review in the right window slot, so emit sites must always supply it.
// ---------------------------------------------------------------------------

describe("open-pr-review profile routing", () => {
  test("'review' text reply on a PR notification stores pendingCmd with profileId from the alert's workspace", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-work-inbox", name: "Work inbox", profileId: "profile-work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 500 } });

    const conn = makeConnection();
    const chatId = "12345";

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(60, {
      context: {
        alertId: "alert-pr-1",
        workspaceId: "ws-work-inbox",
        panelId: "panel-1",
        kind: "review",
        prKey: "pr-42",
        provider: "github",
        connectionId: "gh-conn-1",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      {
        message_id: 501,
        chat: { id: 12345 },
        text: "review",
        reply_to_message: { message_id: 60 },
      },
      conn,
      "token123",
    );

    // Confirmation flow stashes the cmd; profileId is preserved end-to-end.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("confirm-action");
    expect(pending?.pendingCmd?.type).toBe("open-pr-review");
    expect(pending?.pendingCmd?.profileId).toBe("profile-work");
  });

  test("clicking 'open PR' button on notification stores pendingCmd with profileId from the alert's workspace", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-personal-inbox", name: "Personal inbox", profileId: "profile-personal" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 502 } });

    const conn = makeConnection();
    const chatId = "12345";

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(70, {
      context: {
        alertId: "alert-pr-2",
        workspaceId: "ws-personal-inbox",
        panelId: "panel-1",
        kind: "review",
        prKey: "pr-77",
        provider: "azure-devops",
        connectionId: "az-conn-1",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 70, chat: { id: 12345 }, text: "" }, data: "o" },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get(chatId);
    expect(pending?.type).toBe("confirm-action");
    expect(pending?.pendingCmd?.type).toBe("open-pr-review");
    expect(pending?.pendingCmd?.prKey).toBe("pr-77");
    expect(pending?.pendingCmd?.profileId).toBe("profile-personal");
  });

  test("confirming 'c' a stored open-pr-review pendingCmd emits the command with profileId intact", async () => {
    // End-to-end guard: confirm-action -> emit must preserve every field on
    // pendingCmd, including the new profileId. Spreading `...pending.pendingCmd`
    // already does this, but pin the behavior so a future re-build of cmdToEmit
    // can't silently drop the field.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 503 } });

    const chatId = "12345";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).pendingRequests.set(chatId, {
      type: "confirm-action",
      workspaceId: "ws-work-inbox",
      panelId: "panel-1",
      createdAt: Date.now(),
      pendingCmd: {
        type: "open-pr-review",
        workspaceId: "ws-work-inbox",
        panelId: "panel-1",
        prKey: "pr-42",
        provider: "github",
        connectionId: "gh-conn-1",
        profileId: "profile-work",
      },
    });

    const emitted: Array<Record<string, unknown>> = [];
    manager.on("command", (cmd) => emitted.push(cmd as Record<string, unknown>));

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 80, chat: { id: 12345 }, text: "" }, data: "c" },
      makeConnection(),
      "token123",
    );

    expect(emitted).toHaveLength(1);
    expect(emitted[0].type).toBe("open-pr-review");
    expect(emitted[0].profileId).toBe("profile-work");
    expect(emitted[0].prKey).toBe("pr-42");
  });

  test("'review' text reply on PR with workspace missing the profileId field defaults to 'default'", async () => {
    // Legacy workspaces that predate per-profile organisation have no
    // profileId field. The emit must still scope to *something* so runtime
    // can look up the default slot; falling back to "default" matches the
    // convention used elsewhere (slot.profileId || "default").
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "legacy-ws", name: "legacy" }), // no profileId
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 504 } });

    const conn = makeConnection();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(90, {
      context: {
        alertId: "alert-pr-3",
        workspaceId: "legacy-ws",
        panelId: "panel-1",
        kind: "review",
        prKey: "pr-99",
        provider: "github",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      {
        message_id: 505,
        chat: { id: 12345 },
        text: "review",
        reply_to_message: { message_id: 90 },
      },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.pendingCmd?.profileId).toBe("default");
  });
});

// ---------------------------------------------------------------------------
// Auto-review profile routing
// Pre-2.2.15 auto-review silently dropped profileId, so the task workspace
// always landed in "default" even when the PR originated elsewhere. The
// stored ctx.profileId (captured at forwardAlert time) is now the source of
// truth for routing the resulting start-task command.
// ---------------------------------------------------------------------------

describe("auto-review profile routing", () => {
  test("'auto-review' button inherits profileId from the alert's stored ctx", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-personal-inbox", name: "Personal inbox", profileId: "profile-personal" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 600 } });

    const conn = makeConnection();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(120, {
      context: {
        alertId: "alert-ar-1",
        workspaceId: "ws-personal-inbox",
        panelId: "panel-1",
        kind: "review",
        prKey: "pr-99",
        provider: "github",
        connectionId: "gh-conn-1",
        title: "Fix the auth bug",
        profileId: "profile-personal",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 120, chat: { id: 12345 }, text: "" }, data: "ar" },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("confirm-action");
    expect(pending?.pendingCmd?.type).toBe("start-task");
    expect(pending?.pendingCmd?.profileId).toBe("profile-personal");
    expect(pending?.pendingCmd?.prKey).toBe("pr-99");
  });

  test("'auto-review' falls back to workspace.profileId when ctx.profileId is missing (legacy alert)", async () => {
    // Older alerts cached before 2.2.15 don't have ctx.profileId. We still
    // want auto-review to route correctly, so the workspace's profile is
    // the documented fallback.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "ws-work-inbox", name: "Work inbox", profileId: "profile-work" }),
    ]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 601 } });

    const conn = makeConnection();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(121, {
      context: {
        alertId: "alert-ar-2",
        workspaceId: "ws-work-inbox",
        panelId: "panel-1",
        kind: "review",
        prKey: "pr-101",
        provider: "azure-devops",
        connectionId: "az-conn-1",
        // No profileId — legacy ctx
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 121, chat: { id: 12345 }, text: "" }, data: "ar" },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.pendingCmd?.profileId).toBe("profile-work");
  });

  test("forwardAlert stores workspaceProfileId in the context map so callbacks can read it back", async () => {
    // The bridge between forwardAlert (writes ctx) and _handleCallbackQuery
    // (reads ctx) is the contextByMessageId map. Pin the contract so a
    // future refactor of either side can't silently drop profileId.
    const cred = makeCredentialStore({ "cred:tg-1": "tok1" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ id: "tg-1", botTokenRef: "cred:tg-1", profileId: "" })]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendMessage = async () => ({ ok: true, result: { message_id: 999 } });

    await manager.forwardAlert({
      workspaceId: "ws-q",
      panelId: "p-1",
      kind: "review",
      title: "PR Review: foo",
      prKey: "pr-1",
      provider: "github",
      workspaceProfileId: "profile-team",
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const entry = (manager as any).contextByMessageId.get(999);
    expect(entry?.context?.profileId).toBe("profile-team");
  });

  test("'new task' button (s) stores pending task-description with profileId from ctx", async () => {
    // The "🚀 New Task" button on a completed-alert opens a description
    // dialog. The pending request must remember the originating profile so
    // the eventual start-task command routes the right window — otherwise
    // the global bot's "reply with description" round-trip would drop it.
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 700 } });

    const conn = makeConnection();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any).contextByMessageId.set(150, {
      context: {
        alertId: "alert-completed",
        workspaceId: "ws-build",
        panelId: "panel-1",
        kind: "completed",
        profileId: "profile-team",
      },
      connectionId: conn.id,
      at: Date.now(),
    });

    // Click "🚀 New Task"
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 150, chat: { id: 12345 }, text: "" }, data: "s" },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("task-description");
    expect(pending?.profileId).toBe("profile-team");

    // Now type the description — that should produce a confirm-action whose
    // pendingCmd carries profileId all the way through.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 151, chat: { id: 12345 }, text: "rebuild docs" },
      conn,
      "token123",
    );

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const confirm = (manager as any).pendingRequests.get("12345");
    expect(confirm?.type).toBe("confirm-action");
    expect(confirm?.pendingCmd?.profileId).toBe("profile-team");
    expect(confirm?.pendingCmd?.taskDescription).toBe("rebuild docs");
  });
});

// ---------------------------------------------------------------------------
// /prs — profile-scoped PR selection
// ---------------------------------------------------------------------------

describe("/prs command", () => {
  test("filters PRs by TelegramPrInfo.profileId, not by fallback workspace profile", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setProfilesGetter(() => [
      { id: "default", name: "Default" },
      { id: "work", name: "Work" },
    ]);
    manager.setWorkspacesGetter(() => [
      makeWorkspace({ id: "azure-default", name: "Azure Default", kind: "azure", profileId: "default" }),
    ]);
    manager.setPrInfosGetter(() => [
      {
        prKey: "ado:1",
        provider: "azure-devops",
        connectionId: "ado-work",
        profileId: "work",
        workspaceId: "azure-default",
        title: "Work PR without review workspace",
        hasAttention: true,
      },
      {
        prKey: "ado:2",
        provider: "azure-devops",
        connectionId: "ado-default",
        profileId: "default",
        workspaceId: "azure-default",
        title: "Default PR",
        hasAttention: true,
      },
    ]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 720 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handlePrsCommand("12345", "token123", makeConnection(), "work");

    const text = String(sentBodies.at(-1)?.text || "");
    expect(text).toContain("Work PR without review workspace");
    expect(text).not.toContain("Default PR");
  });

  test("PR action menu preserves profileId for Open Review and Auto-review callbacks", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    const pr: TelegramPrInfo = {
      prKey: "gh:77",
      provider: "github",
      connectionId: "gh-work",
      profileId: "work",
      workspaceId: "github-work",
      title: "Route me in Work",
      hasAttention: true,
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async () => ({ ok: true, result: { message_id: 721 } });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._presentPrActionsMenu("token123", "12345", pr);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const entry = (manager as any).contextByMessageId.get(721);
    expect(entry?.context?.profileId).toBe("work");
  });
});

// ---------------------------------------------------------------------------
// promptStartAfterCreate — runtime → manager reverse channel
// ---------------------------------------------------------------------------

describe("promptStartAfterCreate", () => {
  test("stores confirm-action pending for start-existing-task and sends confirm/cancel keyboard", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 700 } };
    };

    await manager.promptStartAfterCreate({
      chatId: "12345",
      workspaceId: "new-task-ws",
      description: "do the thing",
      parentName: "main-repo",
      cwd: "/projects/main",
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const pending = (manager as any).pendingRequests.get("12345");
    expect(pending?.type).toBe("confirm-action");
    expect(pending?.pendingCmd?.type).toBe("start-existing-task");
    expect(pending?.pendingCmd?.workspaceId).toBe("new-task-ws");

    expect(sentBodies).toHaveLength(1);
    const markup = sentBodies[0].reply_markup as { inline_keyboard: Array<Array<{ callback_data: string }>> };
    expect(markup.inline_keyboard[0][0].callback_data).toBe("c");
    expect(markup.inline_keyboard[0][1].callback_data).toBe("x");
  });

  test("noop when chatId has no matching connection", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 701 } };
    };

    await manager.promptStartAfterCreate({
      chatId: "99999",
      workspaceId: "x",
      description: "y",
      parentName: "z",
      cwd: "/q",
    });

    expect(sentBodies).toHaveLength(0);
  });
});

// ---------------------------------------------------------------------------
// /tunnel — surface remote URL (LAN / Cloudflare) on the user's phone
// ---------------------------------------------------------------------------

describe("/tunnel command", () => {
  test("/tunnel reports Cloudflare public URL with copy + open buttons when connected", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://192.168.1.20:7333/?token=abc", "http://10.0.0.5:7333/?token=abc"],
      cloudflareUrl: "https://blah-blah.trycloudflare.com",
      remoteToken: "abc",
      cloudflareStatus: "connected",
      tunnelMode: "cloudflare",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8000 } };
    };

    const msg = { message_id: 800, chat: { id: 12345 }, text: "/tunnel" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    const body = sentBodies[0];
    const text = body.text as string;
    // Cloudflare URL is shown with appended ?token= so it works without
    // re-pasting the auth secret on the phone.
    expect(text).toContain("blah-blah.trycloudflare.com");
    expect(text).toContain("token=abc");
    // LAN URLs are listed underneath as fallbacks.
    expect(text).toContain("192.168.1.20:7333");
    expect(text).toContain("10.0.0.5:7333");

    // Inline keyboard contains an "Open public" button + per-LAN open buttons.
    const markup = body.reply_markup as { inline_keyboard: Array<Array<{ text: string; url: string }>> };
    const urls = markup.inline_keyboard.flatMap((row) => row.map((b) => b.url));
    expect(urls.some((u) => u.includes("trycloudflare.com"))).toBe(true);
    expect(urls.some((u) => u.includes("192.168.1.20"))).toBe(true);
  });

  test("/tunnel falls back to LAN URLs when Cloudflare is not connected", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://192.168.1.20:7333/?token=abc"],
      cloudflareUrl: "",
      remoteToken: "abc",
      cloudflareStatus: "idle",
      tunnelMode: "lan-only",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8001 } };
    };

    const msg = { message_id: 801, chat: { id: 12345 }, text: "/tunnel" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    const text = sentBodies[0].text as string;
    expect(text).toContain("192.168.1.20:7333");
    expect(text).not.toContain("trycloudflare");
  });

  test("/tunnel appends the bound open desktop profile context to URLs", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection({ profileId: "work" })]);
    manager.setProfilesGetter(() => [{ id: "work", name: "Work" }]);
    manager.setWindowSlotsGetter(() => [{ id: "win-work", profileId: "work" }]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://192.168.1.20:7333/?token=abc"],
      cloudflareUrl: "https://blah-blah.trycloudflare.com",
      remoteToken: "abc",
      cloudflareStatus: "connected",
      tunnelMode: "cloudflare",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8006 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 806, chat: { id: 12345 }, text: "/tunnel" },
      makeConnection({ profileId: "work" }),
      "token123",
    );

    const text = sentBodies[0].text as string;
    expect(text).toContain("profileId=work");
    const markup = sentBodies[0].reply_markup as { inline_keyboard: Array<Array<{ url: string }>> };
    expect(
      markup.inline_keyboard
        .flatMap((row) => row.map((button) => button.url))
        .every((url) => url.includes("profileId=work")),
    ).toBe(true);
  });

  test("/tunnel prompts unbound chats when multiple desktop profiles are open", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setProfilesGetter(() => [
      { id: "personal", name: "Personal" },
      { id: "work", name: "Work" },
    ]);
    manager.setWindowSlotsGetter(() => [
      { id: "win-personal", profileId: "personal" },
      { id: "win-work", profileId: "work" },
    ]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://192.168.1.20:7333/?token=abc"],
      cloudflareUrl: "",
      remoteToken: "abc",
      cloudflareStatus: "idle",
      tunnelMode: "lan-only",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8007 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(
      { message_id: 807, chat: { id: 12345 }, text: "/tunnel" },
      makeConnection(),
      "token123",
    );

    expect(sentBodies).toHaveLength(1);
    const text = sentBodies[0].text as string;
    expect(text).toContain("Pick a profile");
    expect(text).toContain("Personal");
    expect(text).toContain("Window 1");
    expect(text).toContain("Work");
    expect(text).toContain("Window 2");
    expect(text).not.toContain("192.168.1.20");
  });

  test("/tunnel says remote access is off when nothing is configured", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: false,
      lanUrls: [],
      cloudflareUrl: "",
      remoteToken: "",
      cloudflareStatus: "idle",
      tunnelMode: "off",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8002 } };
    };

    const msg = { message_id: 802, chat: { id: 12345 }, text: "/tunnel" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    const text = sentBodies[0].text as string;
    expect(text.toLowerCase()).toContain("remote access is");
    expect(text.toLowerCase()).toContain("off");
  });

  test("plain 'tunnel' alias and '/url' alias both reach handler", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://10.0.0.1:7333/?token=t"],
      cloudflareUrl: "",
      remoteToken: "t",
      cloudflareStatus: "idle",
      tunnelMode: "lan-only",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8003 } };
    };

    for (const variant of ["tunnel", "/tunnel", "url", "/URL", " /tunnel "]) {
      sentBodies.length = 0;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await (manager as any)._handleMessage(
        { message_id: 810, chat: { id: 12345 }, text: variant },
        makeConnection(),
        "token123",
      );
      expect(sentBodies.length).toBe(1);
      expect((sentBodies[0].text as string).toLowerCase()).toContain("strideterm tunnel");
    }
  });

  test("clicking mn:tunnel in main menu dispatches to /tunnel handler", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);
    manager.setTunnelInfoGetter(() => ({
      remoteEnabled: true,
      lanUrls: ["http://10.0.0.1:7333/?token=t"],
      cloudflareUrl: "",
      remoteToken: "t",
      cloudflareStatus: "idle",
      tunnelMode: "lan-only",
    }));

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8004 } };
    };

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleCallbackQuery(
      { id: "cq", from: { id: 1 }, message: { message_id: 130, chat: { id: 12345 }, text: "" }, data: "mn:tunnel" },
      makeConnection(),
      "token123",
    );

    const text = sentBodies[sentBodies.length - 1].text as string;
    expect(text.toLowerCase()).toContain("strideterm tunnel");
  });

  test("/tunnel without getter set yields a clear warning instead of crashing", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.configure([makeConnection()]);

    const sentBodies: Array<Record<string, unknown>> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = async (_t: string, _m: string, body: Record<string, unknown>) => {
      sentBodies.push(body);
      return { ok: true, result: { message_id: 8005 } };
    };

    const msg = { message_id: 803, chat: { id: 12345 }, text: "/tunnel" };
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMessage(msg, makeConnection(), "token123");

    expect(sentBodies).toHaveLength(1);
    const text = sentBodies[0].text as string;
    expect(text.toLowerCase()).toContain("not available");
  });
});

// ---------------------------------------------------------------------------
// Telegram dispatch — windowSlot validation (PR 3 audit)
// ---------------------------------------------------------------------------

describe("TelegramManager windowSlot validation for /task", () => {
  function makeWorkspace(id: string, profileId: string): TelegramWorkspaceInfo {
    return {
      id,
      name: `Workspace ${id}`,
      profileId,
      cwd: `/tmp/${id}`,
      kind: "terminal",
      parentWorkspaceId: "",
      panels: [],
      starred: false,
    };
  }

  test("profile choices come only from open desktop windowSlots, not persisted profiles", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setProfilesGetter(() => [
      { id: "p1", name: "P1" },
      { id: "p2", name: "P2" },
    ]);
    manager.setWindowSlotsGetter(() => []);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any)._profileChoices()).toEqual([]);
  });

  test("explicit profile choices are rejected when the profile is no longer open on desktop", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setProfilesGetter(() => [
      { id: "p1", name: "P1" },
      { id: "p2", name: "P2" },
    ]);
    manager.setWindowSlotsGetter(() => [{ id: "w2", profileId: "p2" }]);

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any)._resolveConnectionProfileId(makeConnection(), "p1")).toBeNull();
  });

  test("proceeds when profile p1 is in windowSlot W2 (targets W2)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setProfilesGetter(() => [
      { id: "p1", name: "P1" },
      { id: "p2", name: "P2" },
    ]);
    // p1 is in W2 (second slot), p2 is in W1 (first slot)
    manager.setWindowSlotsGetter(() => [
      { id: "w1", profileId: "p2" },
      { id: "w2", profileId: "p1" },
    ]);
    manager.setWorkspacesGetter(() => [makeWorkspace("ws1", "p1")]);

    const conn = makeConnection({ profileId: "p1" });
    const commands: unknown[] = [];
    manager.on("command", (cmd) => commands.push(cmd));

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendText = vi.fn(async (_token: string, _chatId: string, text: string) => {
      sentTexts.push(text);
      return { ok: true, result: { message_id: 1 } };
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleTaskCommand("12345", "token123", conn);

    // Should NOT reject with "not open" error — p1 is in W2
    const rejectedByWindow = sentTexts.some((t) => t.toLowerCase().includes("not open"));
    expect(rejectedByWindow).toBe(false);
  });

  test("rejects with error when profile has no windowSlot (no-op + error result)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setProfilesGetter(() => [
      { id: "p1", name: "P1" },
      { id: "p2", name: "P2" },
    ]);
    // Only p2 has a window slot — p1 does not
    manager.setWindowSlotsGetter(() => [{ id: "w1", profileId: "p2" }]);
    manager.setWorkspacesGetter(() => [makeWorkspace("ws1", "p1")]);

    const conn = makeConnection({ profileId: "p1" });
    const commands: unknown[] = [];
    manager.on("command", (cmd) => commands.push(cmd));

    const sentTexts: string[] = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._sendText = vi.fn(async (_token: string, _chatId: string, text: string) => {
      sentTexts.push(text);
      return { ok: true, result: { message_id: 1 } };
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleTaskCommand("12345", "token123", conn);

    // Should reject with "not open" error
    const rejectedByWindow = sentTexts.some((t) => t.toLowerCase().includes("not open"));
    expect(rejectedByWindow).toBe(true);
    // No command event should have been emitted
    expect(commands).toHaveLength(0);
  });

  test("_windowIdForProfile returns slot id when profile is present", () => {
    const cred = makeCredentialStore({});
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setWindowSlotsGetter(() => [
      { id: "w1", profileId: "p2" },
      { id: "w2", profileId: "p1" },
    ]);
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any)._windowIdForProfile("p1")).toBe("w2");
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((manager as any)._windowIdForProfile("p99")).toBeUndefined();
  });
});

// ---------------------------------------------------------------------------
// _handleMenuCommand — /menu MarkdownV2 escape
// ---------------------------------------------------------------------------

describe("_handleMenuCommand — MarkdownV2 escape", () => {
  test("/menu sends message successfully (no unescaped MarkdownV2 hyphens crash the send)", async () => {
    const cred = makeCredentialStore({ "cred:tg-1": "token123" });
    const manager = new TelegramManager({ credentialStore: cred });
    manager.setWorkspacesGetter(() => []);
    manager.setProfilesGetter(() => []);
    manager.setWindowSlotsGetter(() => []);

    const apiCalls: Array<{ method: string; params: Record<string, unknown> }> = [];
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    (manager as any)._apiCall = vi.fn(async (_token: string, method: string, params: Record<string, unknown>) => {
      apiCalls.push({ method, params });
      return { ok: true, result: { message_id: 1 } };
    });

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await (manager as any)._handleMenuCommand("12345", "token123");

    // The api call must have been attempted (not swallowed by a .catch due to parse error)
    expect(apiCalls).toHaveLength(1);
    expect(apiCalls[0].method).toBe("sendMessage");

    // The text must not contain unescaped hyphens inside italic spans (_..._).
    // A hyphen immediately preceded by an unescaped letter inside an italic
    // span would cause Telegram MarkdownV2 to reject the whole message.
    const text = apiCalls[0].params.text as string;
    expect(text).toContain("Profile\\-specific");
    expect(text).not.toMatch(/_[^_]*[^\\]-[^_]*_/);
  });
});
