import { beforeEach, describe, expect, it, vi } from 'vitest';

const hoisted = vi.hoisted(() => {
  const files = new Map<string, string>();

  const norm = (p: string): string => p.replace(/\\/g, '/');

  const stat = vi.fn(async (filePath: string) => {
    const data = files.get(norm(filePath));
    if (data === undefined) {
      const error = new Error('ENOENT') as NodeJS.ErrnoException;
      error.code = 'ENOENT';
      throw error;
    }
    return { isFile: () => true, size: Buffer.byteLength(data, 'utf8') };
  });

  const readFile = vi.fn(async (filePath: string) => {
    const data = files.get(norm(filePath));
    if (data === undefined) {
      const error = new Error('ENOENT') as NodeJS.ErrnoException;
      error.code = 'ENOENT';
      throw error;
    }
    return data;
  });

  const atomicWrite = vi.fn(async (filePath: string, data: string) => {
    files.set(norm(filePath), data);
  });

  return {
    files,
    stat,
    readFile,
    atomicWrite,
    appendSentMessage: vi.fn((teamName: string, message: Record<string, unknown>) => {
      const p = `/mock/teams/${teamName}/sentMessages.json`;
      const current = files.get(p);
      const rows = current ? (JSON.parse(current) as unknown[]) : [];
      rows.push(message);
      files.set(p, JSON.stringify(rows));
      return message;
    }),
    sendInboxMessage: vi.fn(
      (teamName: string, message: Record<string, unknown>) => {
        const member =
          typeof message.member === 'string'
            ? message.member
            : typeof message.to === 'string'
              ? message.to
              : 'unknown';
        const p = `/mock/teams/${teamName}/inboxes/${member}.json`;
        const current = files.get(p);
        const rows = current ? (JSON.parse(current) as unknown[]) : [];
        rows.push(message);
        files.set(p, JSON.stringify(rows));
        return { deliveredToInbox: true, messageId: 'mock-id', message };
      }
    ),
  };
});

vi.mock('fs', async (importOriginal) => {
  const actual = await importOriginal<typeof import('fs')>();
  return {
    ...actual,
    promises: { ...actual.promises, stat: hoisted.stat, readFile: hoisted.readFile },
  };
});

vi.mock('../../../../src/main/services/team/atomicWrite', () => ({
  atomicWriteAsync: hoisted.atomicWrite,
}));

vi.mock('../../../../src/main/services/team/fileLock', () => ({
  withFileLock: async (_filePath: string, fn: () => Promise<unknown>) => await fn(),
  withFileLockSync: (_filePath: string, fn: () => unknown) => fn(),
}));

vi.mock('../../../../src/main/services/team/inboxLock', () => ({
  withInboxLock: async (_filePath: string, fn: () => Promise<unknown>) => await fn(),
}));

vi.mock('../../../../src/main/utils/pathDecoder', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../../../src/main/utils/pathDecoder')>();
  return { ...actual, getTeamsBasePath: () => '/mock/teams' };
});

vi.mock('../../../../src/main/utils/fsRead', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../../../src/main/utils/fsRead')>();
  return {
    ...actual,
    readFileUtf8WithTimeout: hoisted.readFile,
  };
});

vi.mock('agent-teams-controller', () => ({
  AGENT_TEAMS_TEAMMATE_OPERATIONAL_TOOL_NAMES: [] as readonly string[],
  AGENT_TEAMS_NAMESPACED_LEAD_BOOTSTRAP_TOOL_NAMES: [] as readonly string[],
  AGENT_TEAMS_NAMESPACED_TEAMMATE_OPERATIONAL_TOOL_NAMES: [] as readonly string[],
  createController: ({ teamName }: { teamName: string }) => ({
    messages: {
      appendSentMessage: (message: Record<string, unknown>) =>
        hoisted.appendSentMessage(teamName, message),
      sendMessage: (message: Record<string, unknown>) =>
        hoisted.sendInboxMessage(teamName, message),
    },
  }),
  protocols: {
    buildActionModeProtocolText: (delegate: string) =>
      `ACTION MODE PROTOCOL (mock, delegate: ${delegate})`,
    buildProcessProtocolText: (teamName: string) =>
      `BACKGROUND PROCESS REGISTRATION (mock for ${teamName})`,
  },
}));

import { ConfigManager } from '../../../../src/main/services/infrastructure/ConfigManager';
import {
  clearAutoResumeService,
  getAutoResumeService,
  initializeAutoResumeService,
} from '../../../../src/main/services/team/AutoResumeService';
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';

import type { TeamChangeEvent } from '@shared/types/team';

function seedConfig(teamName: string): void {
  hoisted.files.set(
    `/mock/teams/${teamName}/config.json`,
    JSON.stringify({
      name: 'My Team',
      members: [{ name: 'team-lead', agentType: 'team-lead' }],
    })
  );
}

function seedLeadInbox(teamName: string, messages: unknown[]): void {
  hoisted.files.set(`/mock/teams/${teamName}/inboxes/team-lead.json`, JSON.stringify(messages));
}

interface RunLike {
  runId: string;
  teamName: string;
  progress: {
    runId: string;
    teamName: string;
    state: 'spawning' | 'running';
    message: string;
    startedAt: string;
    updatedAt: string;
    error?: string;
  };
  provisioningComplete: boolean;
  detectedSessionId?: string | null;
  leadMsgSeq: number;
  pendingToolCalls: { name: string; preview: string }[];
  activeToolCalls: Map<string, unknown>;
  pendingDirectCrossTeamSendRefresh: boolean;
  lastLeadTextEmitMs: number;
  leadRelayCapture: null;
  silentUserDmForward:
    | null
    | { target: string; startedAt: string; mode: 'user_dm' | 'member_inbox_relay' };
  suppressPostCompactReminderOutput?: boolean;
  child: Record<string, unknown> | null;
  processKilled: boolean;
  cancelRequested: boolean;
  provisioningOutputParts: string[];
  provisioningOutputIndexByMessageId: Map<string, number>;
  request: { members: { name: string; role?: string }[] };
  activeCrossTeamReplyHints?: Array<{ toTeam: string; conversationId: string }>;
  pendingInboxRelayCandidates?: unknown[];
  liveLeadTextBuffer?: unknown;
  memberSpawnStatuses: Map<string, unknown>;
  pendingApprovals: Map<string, unknown>;
}

/**
 * Attach a run to the service internals. `provisioningComplete` defaults to false
 * (pre-ready) to test the early message pipeline.
 */
function attachRun(
  service: TeamProvisioningService,
  teamName: string,
  opts?: { provisioningComplete?: boolean; runId?: string; detectedSessionId?: string | null }
): RunLike {
  const runId = opts?.runId ?? 'run-1';
  const now = new Date().toISOString();
  const provisioningComplete = opts?.provisioningComplete ?? false;
  const run: RunLike = {
    runId,
    teamName,
    progress: {
      runId,
      teamName,
      state: provisioningComplete ? 'running' : 'spawning',
      message: provisioningComplete ? 'Running' : 'Starting',
      startedAt: now,
      updatedAt: now,
    },
    provisioningComplete,
    detectedSessionId: opts?.detectedSessionId ?? null,
    leadMsgSeq: 0,
    pendingToolCalls: [],
    activeToolCalls: new Map(),
    pendingDirectCrossTeamSendRefresh: false,
    lastLeadTextEmitMs: 0,
    leadRelayCapture: null,
    silentUserDmForward: null,
    pendingInboxRelayCandidates: [],
    child: { stdin: { writable: true, write: vi.fn(), end: vi.fn() } },
    processKilled: false,
    cancelRequested: false,
    provisioningOutputParts: [],
    provisioningOutputIndexByMessageId: new Map(),
    request: { members: [{ name: 'team-lead', role: 'Team Lead' }] },
    activeCrossTeamReplyHints: [],
    liveLeadTextBuffer: null,
    memberSpawnStatuses: new Map(),
    pendingApprovals: new Map(),
  };

  (service as unknown as { aliveRunByTeam: Map<string, string> }).aliveRunByTeam.set(
    teamName,
    runId
  );
  (service as unknown as { runs: Map<string, unknown> }).runs.set(runId, run);

  return run;
}

function callHandleStreamJsonMessage(
  service: TeamProvisioningService,
  run: RunLike,
  msg: Record<string, unknown>
): void {
  (service as unknown as { handleStreamJsonMessage: (r: unknown, m: unknown) => void })
    .handleStreamJsonMessage(run, msg);
}

describe('TeamProvisioningService pre-ready live messages', () => {
  beforeEach(() => {
    hoisted.files.clear();
    hoisted.appendSentMessage.mockClear();
    hoisted.sendInboxMessage.mockClear();
  });

  it('pre-ready assistant text is added to liveLeadProcessMessages', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Команда создана. Запускаю всех тиммейтов параллельно.' }],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].text).toBe('Команда создана. Запускаю всех тиммейтов параллельно.');
    expect(live[0].source).toBe('lead_process');
    expect(live[0].messageId).toMatch(/^lead-turn-run-1-1$/);

    // Also still in provisioningOutputParts for the banner
    expect(run.provisioningOutputParts).toHaveLength(1);
  });

  it('coalesces Codex synthetic chunks through stream handling without dropping spaces', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    const emitSyntheticText = (text: string, timestamp: string): void => {
      callHandleStreamJsonMessage(service, run, {
        type: 'assistant',
        timestamp,
        message: {
          id: `msg-${timestamp}`,
          model: '<synthetic>',
          type: 'message',
          content: [{ type: 'text', text }],
        },
      });
    };

    emitSyntheticText('Пр', '2026-04-17T12:00:00.000Z');
    emitSyntheticText('ин', '2026-04-17T12:00:00.010Z');
    emitSyntheticText('ял', '2026-04-17T12:00:00.020Z');
    emitSyntheticText(':', '2026-04-17T12:00:00.030Z');
    emitSyntheticText(' раз', '2026-04-17T12:00:00.040Z');
    emitSyntheticText('лож', '2026-04-17T12:00:00.050Z');
    emitSyntheticText('у', '2026-04-17T12:00:00.060Z');

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].text).toBe('Принял: разложу');
    expect(live[0].messageId).toBe('lead-turn-run-1-1');
  });

  it('attaches leadSessionId to a live message when the same assistant payload carries session_id', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      session_id: 'sess-123',
      content: [{ type: 'text', text: 'Команда создана. Запускаю всех тиммейтов параллельно.' }],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].leadSessionId).toBe('sess-123');
  });

  it('makes leadSessionId visible to synchronous lead-message listeners in the same turn', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const seenSessionIds: Array<string | undefined> = [];
    service.setTeamChangeEmitter((event) => {
      if (event.type === 'lead-message') {
        seenSessionIds.push(service.getLiveLeadProcessMessages('my-team')[0]?.leadSessionId);
      }
    });
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      session_id: 'sess-sync',
      content: [{ type: 'text', text: 'Команда создана. Запускаю всех тиммейтов параллельно.' }],
    });

    expect(seenSessionIds).toEqual(['sess-sync']);
  });

  it('retrofits leadSessionId onto earlier live messages after session detection', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Команда создана. Запускаю всех тиммейтов параллельно.' }],
    });
    expect(service.getLiveLeadProcessMessages('my-team')[0]?.leadSessionId).toBeUndefined();

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      session_id: 'sess-456',
      content: [],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].leadSessionId).toBe('sess-456');
  });

  it('emits lead-message event type (not inbox)', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Launching teammates now.' }],
    });

    expect(emitter).toHaveBeenCalledTimes(1);
    expect(emitter).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'lead-message', teamName: 'my-team' })
    );
  });

  it('coalesces rapid emissions via LEAD_TEXT_EMIT_THROTTLE_MS', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    // First message: should emit
    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Message 1' }],
    });
    expect(emitter).toHaveBeenCalledTimes(1);

    // Second message immediately after: should be coalesced (not emitted again)
    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Message 2' }],
    });
    expect(emitter).toHaveBeenCalledTimes(1); // Still 1

    // Messages are still cached though
    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(2);
  });

  it('early live messages carry toolCalls and toolSummary', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    // First: tool_use message (no text)
    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'TeamCreate',
          input: { team_name: 'super-team', description: 'test' },
        },
      ],
    });

    // Then: text message — should pick up pending tool calls
    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Team created successfully.' }],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].toolCalls).toBeDefined();
    expect(live[0].toolCalls).toHaveLength(1);
    expect(live[0].toolCalls![0].name).toBe('TeamCreate');
    expect(live[0].toolSummary).toBeDefined();
  });

  it('provisioning-time SendMessage(to:user) is captured and persisted', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: false });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'user',
            content: 'All teammates online!',
            summary: 'Team ready',
          },
        },
      ],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].to).toBe('user');
    expect(live[0].text).toBe('All teammates online!');
    expect(live[0].source).toBe('lead_process');

    // Also persisted to sentMessages.json
    expect(hoisted.appendSentMessage).toHaveBeenCalledTimes(1);
  });

  it('suppresses duplicate assistant thought text when SendMessage targets a teammate', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        { type: 'text', text: 'Forwarding the clarification request now.' },
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'team-lead',
            content: 'Need clarification on #abcd1234',
            summary: 'Clarification request',
          },
        },
      ],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].to).toBe('team-lead');
    expect(live[0].text).toBe('Need clarification on #abcd1234');
    expect(live[0].source).toBe('lead_process');
    // Non-user recipient → delivered to inbox, not sentMessages
    expect(hoisted.sendInboxMessage).toHaveBeenCalledTimes(1);
    expect(hoisted.appendSentMessage).not.toHaveBeenCalled();
  });

  it('suppresses duplicate assistant thought text when SendMessage targets user', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        { type: 'text', text: 'Task completed. Sending the summary now.' },
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'user',
            content: 'Task completed successfully.',
            summary: 'Done',
          },
        },
      ],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].to).toBe('user');
    expect(live[0].text).toBe('Task completed successfully.');
    expect(live[0].source).toBe('lead_process');
    expect(hoisted.appendSentMessage).toHaveBeenCalledTimes(1);
  });

  it('suppresses duplicate assistant thought text when Agent Teams message_send is already visible', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        { type: 'text', text: 'Sending this through the Agent Teams MCP tool now.' },
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__message_send',
          input: {
            teamName: 'my-team',
            to: 'user',
            text: 'Task completed through MCP.',
            from: 'team-lead',
            summary: 'Done',
          },
        },
      ],
    });

    // The MCP controller owns persistence for agent-teams_message_send. The stream
    // capture path must not show the assistant narration as a second "thought".
    expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(0);
    expect(hoisted.appendSentMessage).not.toHaveBeenCalled();
  });

  it('keeps assistant thought text when Agent Teams message_send payload is incomplete', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        { type: 'text', text: 'I need to retry this because the tool input is incomplete.' },
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__message_send',
          input: {
            teamName: 'my-team',
            to: 'user',
            from: 'team-lead',
            summary: 'Incomplete',
          },
        },
      ],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].text).toBe('I need to retry this because the tool input is incomplete.');
    expect(live[0].source).toBe('lead_process');
  });

  it('post-ready path also uses the unified helper', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Assigning tasks now.' }],
    });

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].source).toBe('lead_process');

    // Post-ready also emits lead-message
    expect(emitter).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'lead-message', teamName: 'my-team' })
    );
  });

  it('suppresses bare transcript speaker placeholders from lead thought output', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [{ type: 'text', text: 'Human: ' }],
    });

    expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(0);
    expect(emitter).not.toHaveBeenCalled();
  });

  it('SendMessage(to:teammate) creates inbox row and emits inbox detail for recipient', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'alice',
            content: 'Please check the migration.',
            summary: 'Migration check',
          },
        },
      ],
    });

    // Delivered to recipient inbox, not sentMessages
    expect(hoisted.sendInboxMessage).toHaveBeenCalledTimes(1);
    expect(hoisted.sendInboxMessage).toHaveBeenCalledWith(
      'my-team',
      expect.objectContaining({ member: 'alice' })
    );
    expect(hoisted.appendSentMessage).not.toHaveBeenCalled();

    // Emits inbox event for the specific recipient
    expect(emitter).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'inbox',
        teamName: 'my-team',
        detail: 'inboxes/alice.json',
      })
    );
  });

  it('SendMessage(to:user) still persists to sentMessages.json', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'user',
            content: 'Task completed!',
            summary: 'Done',
          },
        },
      ],
    });

    expect(hoisted.appendSentMessage).toHaveBeenCalledTimes(1);
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();

    expect(emitter).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'inbox',
        teamName: 'my-team',
        detail: 'sentMessages.json',
      })
    );
  });

  it('canonicalizes capitalized SendMessage user recipients before persistence', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'User',
            content: 'Task completed!',
            summary: 'Done',
          },
        },
      ],
    });

    expect(hoisted.appendSentMessage).toHaveBeenCalledWith(
      'my-team',
      expect.objectContaining({ to: 'user' })
    );
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
  });

  it('upgrades qualified SendMessage recipients into cross-team sends', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-1' }));
    service.setTeamChangeEmitter(emitter);
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });
    run.activeCrossTeamReplyHints = [{ toTeam: 'team-best', conversationId: 'conv-123' }];

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'team-best.user',
            content: 'Привет!',
            summary: 'Ответ',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        fromTeam: 'my-team',
        fromMember: 'team-lead',
        toTeam: 'team-best',
        text: 'Привет!',
        conversationId: 'conv-123',
        replyToConversationId: 'conv-123',
      })
    );

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].from).toBe('team-lead');
    expect(live[0].source).toBe('cross_team_sent');
    expect(live[0].to).toBe('team-best.user');
    expect(live[0].text).toBe('Привет!');
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
    expect(hoisted.appendSentMessage).not.toHaveBeenCalled();
  });

  it('ignores stale cross-team send completions from an older run after a new run starts', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');

    let resolveSend: ((value: { deliveredToInbox: boolean; messageId: string }) => void) | null =
      null;
    const crossTeamSender = vi.fn(
      () =>
        new Promise<{ deliveredToInbox: boolean; messageId: string }>((resolve) => {
          resolveSend = resolve;
        })
    );
    service.setCrossTeamSender(crossTeamSender);

    const oldRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-old',
      detectedSessionId: 'sess-old',
    });
    oldRun.activeCrossTeamReplyHints = [{ toTeam: 'team-best', conversationId: 'conv-old' }];

    callHandleStreamJsonMessage(service, oldRun, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'team-best.user',
            content: 'Old run cross-team reply.',
            summary: 'Old run reply',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    const newRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-new',
      detectedSessionId: 'sess-new',
    });
    service.pushLiveLeadProcessMessage('my-team', {
      from: 'team-lead',
      text: 'Current run is active.',
      timestamp: '2026-04-17T12:00:10.000Z',
      read: true,
      source: 'lead_process',
      messageId: 'lead-turn-run-new-1',
      leadSessionId: 'sess-new',
    });

    expect(resolveSend).not.toBeNull();
    const finishSend = resolveSend as unknown as ((
      value: { deliveredToInbox: boolean; messageId: string }
    ) => void);
    finishSend({ deliveredToInbox: true, messageId: 'cross-stale-old-run' });
    await Promise.resolve();
    await Promise.resolve();

    expect(service.getLiveLeadProcessMessages('my-team')).toEqual([
      expect.objectContaining({
        text: 'Current run is active.',
        messageId: 'lead-turn-run-new-1',
        leadSessionId: 'sess-new',
      }),
    ]);

    (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(oldRun);
    (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(newRun);
  });

  it('upgrades pseudo cross-team recipients into cross-team sends', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-2' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'cross-team:team-best',
            content: 'Привет команде!',
            summary: 'Приветствие',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        fromTeam: 'my-team',
        fromMember: 'team-lead',
        toTeam: 'team-best',
        text: 'Привет команде!',
      })
    );

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].from).toBe('team-lead');
    expect(live[0].source).toBe('cross_team_sent');
    expect(live[0].to).toBe('cross-team:team-best');
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
  });

  it('upgrades MCP message_send pseudo recipients into cross-team sends', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-mcp-1' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });
    run.activeCrossTeamReplyHints = [{ toTeam: 'team-best', conversationId: 'conv-mcp-1' }];
    const taskRefs = [{ taskId: 'task-1', displayId: 'abcd1234', teamName: 'my-team' }];

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__message_send',
          input: {
            teamName: 'my-team',
            to: 'cross-team:team-best',
            text: 'Ответ через MCP.',
            from: 'team-lead',
            summary: 'MCP reply',
            taskRefs,
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        fromTeam: 'my-team',
        fromMember: 'team-lead',
        toTeam: 'team-best',
        text: 'Ответ через MCP.',
        conversationId: 'conv-mcp-1',
        replyToConversationId: 'conv-mcp-1',
        taskRefs,
      })
    );

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].from).toBe('team-lead');
    expect(live[0].source).toBe('cross_team_sent');
    expect(live[0].to).toBe('cross-team:team-best');
    expect(live[0].taskRefs).toEqual(taskRefs);
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
  });

  it('refreshes sentMessages history after direct MCP cross_team_send succeeds', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    service.setTeamChangeEmitter(emitter);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__cross_team_send',
          input: {
            teamName: 'my-team',
            toTeam: 'team-best',
            text: 'Прямой вызов MCP.',
            summary: 'Direct MCP send',
          },
        },
      ],
    });

    expect(run.pendingDirectCrossTeamSendRefresh).toBe(true);

    callHandleStreamJsonMessage(service, run, {
      type: 'result',
      subtype: 'success',
    });

    expect(run.pendingDirectCrossTeamSendRefresh).toBe(false);
    expect(emitter).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'inbox',
        teamName: 'my-team',
        detail: 'sentMessages.json',
      })
    );
  });

  it('marks native cross-team teammate-message deliveries as read and restores reply hints', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    seedLeadInbox('my-team', [
      {
        from: 'other-team.team-lead',
        to: 'team-lead',
        text: '<cross-team from="other-team.team-lead" depth="0" conversationId="conv-native-1" replyToConversationId="conv-native-1" />\nНативная доставка.',
        timestamp: '2026-02-23T10:01:00.000Z',
        read: false,
        source: 'cross_team',
        messageId: 'm-native-cross-team-1',
        conversationId: 'conv-native-1',
        replyToConversationId: 'conv-native-1',
      },
    ]);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'user',
      message: {
        role: 'user',
        content:
          '<teammate-message teammate_id="other-team.team-lead" color="purple" summary="Cross-team reply"><cross-team from="other-team.team-lead" depth="0" conversationId="conv-native-1" replyToConversationId="conv-native-1" />\nНативная доставка.</teammate-message>',
      },
    });

    await vi.waitFor(() => {
      const updatedInbox = JSON.parse(
        hoisted.files.get('/mock/teams/my-team/inboxes/team-lead.json') ?? '[]'
      ) as Array<{ read?: boolean }>;
      expect(updatedInbox[0]?.read).toBe(true);
    });

    expect(run.activeCrossTeamReplyHints).toEqual([
      { toTeam: 'other-team', conversationId: 'conv-native-1' },
    ]);
  });

  it('suppresses native duplicate cross-team teammate-message after recent relay delivery', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const content =
      '<cross-team from="other-team.team-lead" depth="0" conversationId="conv-native-dup" replyToConversationId="conv-native-dup" />\nПовторная доставка.';
    seedLeadInbox('my-team', [
      {
        from: 'other-team.team-lead',
        to: 'team-lead',
        text: content,
        timestamp: '2026-03-10T21:43:00.000Z',
        read: false,
        source: 'cross_team',
        messageId: 'm-native-cross-team-dup',
        conversationId: 'conv-native-dup',
        replyToConversationId: 'conv-native-dup',
      },
    ]);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    (
      service as unknown as {
        rememberRecentCrossTeamLeadDeliveryMessageIds: (
          teamName: string,
          messageIds: string[]
        ) => void;
      }
    ).rememberRecentCrossTeamLeadDeliveryMessageIds('my-team', ['m-native-cross-team-dup']);

    callHandleStreamJsonMessage(service, run, {
      type: 'user',
      message: {
        role: 'user',
        content: `<teammate-message teammate_id="other-team.team-lead" color="purple" summary="Cross-team reply">${content}</teammate-message>`,
      },
    });

    await vi.waitFor(() => {
      const updatedInbox = JSON.parse(
        hoisted.files.get('/mock/teams/my-team/inboxes/team-lead.json') ?? '[]'
      ) as Array<{ read?: boolean }>;
      expect(updatedInbox[0]?.read).toBe(true);
    });

    expect(run.activeCrossTeamReplyHints).toEqual([]);
  });

  it('rescues mistaken cross_team_send recipients into actual cross-team replies', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-mcp-tool-1' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });
    run.activeCrossTeamReplyHints = [{ toTeam: 'team-best', conversationId: 'conv-tool-1' }];

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__message_send',
          input: {
            teamName: 'my-team',
            to: 'cross_team_send',
            text: 'Исправленный ответ.',
            from: 'team-lead',
            summary: 'Ответ через tool recipient mistake',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        fromTeam: 'my-team',
        fromMember: 'team-lead',
        toTeam: 'team-best',
        text: 'Исправленный ответ.',
        conversationId: 'conv-tool-1',
        replyToConversationId: 'conv-tool-1',
      })
    );

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].from).toBe('team-lead');
    expect(live[0].source).toBe('cross_team_sent');
    expect(live[0].to).toBe('team-best.team-lead');
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
  });

  it('rescues cross_team::team pseudo recipients into actual cross-team replies', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-colon-1' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'mcp__agent-teams__message_send',
          input: {
            teamName: 'my-team',
            to: 'cross_team::team-best',
            text: 'Ответ через fallback pseudo recipient.',
            summary: 'Fallback pseudo reply',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        fromTeam: 'my-team',
        fromMember: 'team-lead',
        toTeam: 'team-best',
        text: 'Ответ через fallback pseudo recipient.',
      })
    );

    const live = service.getLiveLeadProcessMessages('my-team');
    expect(live).toHaveLength(1);
    expect(live[0].source).toBe('cross_team_sent');
    expect(live[0].to).toBe('team-best.team-lead');
    expect(hoisted.sendInboxMessage).not.toHaveBeenCalled();
  });

  it('strips canonical cross-team tag from outbound cross-team content', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-legacy' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });
    run.activeCrossTeamReplyHints = [{ toTeam: 'team-best', conversationId: 'conv-legacy' }];

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'team-best.user',
            content:
              '<cross-team from="my-team.team-lead" depth="0" conversationId="conv-legacy" replyToConversationId="conv-legacy" />\nПривет!',
            summary: 'Ответ',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(crossTeamSender).toHaveBeenCalledWith(
      expect.objectContaining({
        text: 'Привет!',
        conversationId: 'conv-legacy',
        replyToConversationId: 'conv-legacy',
      })
    );
  });

  it('does not push a duplicate live row when cross-team fallback deduplicates', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const emitter = vi.fn<(event: TeamChangeEvent) => void>();
    const crossTeamSender = vi.fn(async () => ({
      deliveredToInbox: true,
      messageId: 'existing-cross-1',
      deduplicated: true,
    }));
    service.setTeamChangeEmitter(emitter);
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'team-best.user',
            content: 'Повтор без нового live row',
            summary: 'Повтор',
          },
        },
      ],
    });

    await vi.waitFor(() => {
      expect(crossTeamSender).toHaveBeenCalledTimes(1);
    });

    expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(0);
    expect(emitter).not.toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'lead-message',
        teamName: 'my-team',
        detail: 'cross-team-send',
      })
    );
  });

  it('does not upgrade dotted local teammate names into cross-team sends', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const crossTeamSender = vi.fn(async () => ({ deliveredToInbox: true, messageId: 'cross-1' }));
    service.setCrossTeamSender(crossTeamSender);
    const run = attachRun(service, 'my-team', { provisioningComplete: true });
    run.request.members.push({ name: 'ops.bot', role: 'Specialist' });

    callHandleStreamJsonMessage(service, run, {
      type: 'assistant',
      content: [
        {
          type: 'tool_use',
          name: 'SendMessage',
          input: {
            type: 'message',
            recipient: 'ops.bot',
            content: 'Please verify the rollout.',
            summary: 'Verify rollout',
          },
        },
      ],
    });

    expect(crossTeamSender).not.toHaveBeenCalled();
    expect(hoisted.sendInboxMessage).toHaveBeenCalledTimes(1);
    expect(hoisted.sendInboxMessage).toHaveBeenCalledWith(
      'my-team',
      expect.objectContaining({ member: 'ops.bot' })
    );
  });
});

describe('TeamProvisioningService auto-resume cleanup', () => {
  beforeEach(() => {
    hoisted.files.clear();
    hoisted.appendSentMessage.mockClear();
    hoisted.sendInboxMessage.mockClear();
    clearAutoResumeService();
    vi.useFakeTimers();
  });

  afterEach(() => {
    clearAutoResumeService();
    vi.useRealTimers();
  });

  it('cancels pending auto-resume timers when a run is cleaned up', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', { provisioningComplete: true });

    const autoResumeProvisioning = {
      getCurrentRunId: vi.fn(() => 'run-1' as string | null),
      isTeamAlive: vi.fn(() => true),
      sendMessageToTeam: vi.fn(async () => undefined),
    };
    initializeAutoResumeService(autoResumeProvisioning);

    const configManager = ConfigManager.getInstance();
    const actualConfig = configManager.getConfig();
    const getConfigSpy = vi.spyOn(configManager, 'getConfig').mockImplementation(
      () =>
        ({
          ...actualConfig,
          notifications: {
            ...actualConfig.notifications,
            autoResumeOnRateLimit: true,
          },
        }) as never
    );

    try {
      getAutoResumeService().handleRateLimitMessage(
        'my-team',
        "You've hit your limit. Resets in 5 minutes.",
        new Date('2026-04-17T12:00:00.000Z')
      );

      (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(run);

      await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 30 * 1000 + 100);
      expect(autoResumeProvisioning.sendMessageToTeam).not.toHaveBeenCalled();
    } finally {
      getConfigSpy.mockRestore();
    }
  });

  it('does not let stale cleanup from an older run cancel the current run state', async () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const oldRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-old',
      detectedSessionId: 'sess-old',
    });
    const newRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-new',
      detectedSessionId: 'sess-new',
    });

    const autoResumeProvisioning = {
      getCurrentRunId: vi.fn(() => 'run-1' as string | null),
      isTeamAlive: vi.fn(() => true),
      sendMessageToTeam: vi.fn(async () => undefined),
    };
    initializeAutoResumeService(autoResumeProvisioning);

    const configManager = ConfigManager.getInstance();
    const actualConfig = configManager.getConfig();
    const getConfigSpy = vi.spyOn(configManager, 'getConfig').mockImplementation(
      () =>
        ({
          ...actualConfig,
          notifications: {
            ...actualConfig.notifications,
            autoResumeOnRateLimit: true,
          },
        }) as never
    );

    try {
      getAutoResumeService().handleRateLimitMessage(
        'my-team',
        "You've hit your limit. Resets in 5 minutes.",
        new Date('2026-04-17T12:00:00.000Z')
      );

      service.pushLiveLeadProcessMessage('my-team', {
        from: 'team-lead',
        text: 'Current run is active.',
        timestamp: '2026-04-17T12:00:01.000Z',
        read: true,
        source: 'lead_process',
        messageId: 'live-new-run',
      });
      expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(1);

      (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(oldRun);

      expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(1);

      await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 30 * 1000 + 100);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledTimes(1);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledWith(
        'my-team',
        expect.stringContaining('rate limit has reset')
      );
    } finally {
      getConfigSpy.mockRestore();
      (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(newRun);
    }
  });

  it('removes stale live lead messages from an older run while preserving the current run', () => {
    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const oldRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-old',
      detectedSessionId: 'sess-old',
    });

    service.pushLiveLeadProcessMessage('my-team', {
      from: 'team-lead',
      text: "You've hit your limit. Resets in 5 minutes.",
      timestamp: '2026-04-17T12:00:00.000Z',
      read: true,
      source: 'lead_process',
      messageId: 'lead-turn-run-old-1',
      leadSessionId: 'sess-old',
    });

    const newRun = attachRun(service, 'my-team', {
      provisioningComplete: true,
      runId: 'run-new',
      detectedSessionId: 'sess-new',
    });

    service.pushLiveLeadProcessMessage('my-team', {
      from: 'team-lead',
      text: 'Current run is active.',
      timestamp: '2026-04-17T12:00:10.000Z',
      read: true,
      source: 'lead_process',
      messageId: 'lead-turn-run-new-1',
      leadSessionId: 'sess-new',
    });

    expect(service.getLiveLeadProcessMessages('my-team')).toHaveLength(2);

    (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(oldRun);

    expect(service.getLiveLeadProcessMessages('my-team')).toEqual([
      expect.objectContaining({
        text: 'Current run is active.',
        messageId: 'lead-turn-run-new-1',
        leadSessionId: 'sess-new',
      }),
    ]);

    (service as unknown as { cleanupRun: (runLike: unknown) => void }).cleanupRun(newRun);
  });

  it('preserves the canonical assistant timestamp for live rate-limit messages', async () => {
    vi.setSystemTime(new Date('2026-04-17T12:00:20.000Z'));

    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', {
      provisioningComplete: true,
      detectedSessionId: 'sess-live',
    });

    const autoResumeProvisioning = {
      getCurrentRunId: vi.fn(() => 'run-1' as string | null),
      isTeamAlive: vi.fn(() => true),
      sendMessageToTeam: vi.fn(async () => undefined),
    };
    initializeAutoResumeService(autoResumeProvisioning);

    const configManager = ConfigManager.getInstance();
    const actualConfig = configManager.getConfig();
    const getConfigSpy = vi.spyOn(configManager, 'getConfig').mockImplementation(
      () =>
        ({
          ...actualConfig,
          notifications: {
            ...actualConfig.notifications,
            autoResumeOnRateLimit: true,
          },
        }) as never
    );

    try {
      callHandleStreamJsonMessage(service, run, {
        type: 'assistant',
        timestamp: '2026-04-17T12:00:00.000Z',
        content: [{ type: 'text', text: "You've hit your limit. Resets in 5 minutes." }],
      });

      const live = service.getLiveLeadProcessMessages('my-team');
      expect(live).toHaveLength(1);
      expect(live[0].timestamp).toBe('2026-04-17T12:00:00.000Z');

      getAutoResumeService().handleRateLimitMessage(
        'my-team',
        live[0].text,
        new Date('2026-04-17T12:00:20.000Z'),
        new Date(live[0].timestamp)
      );

      await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 9 * 1000);
      expect(autoResumeProvisioning.sendMessageToTeam).not.toHaveBeenCalled();

      await vi.advanceTimersByTimeAsync(1500);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledTimes(1);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledWith(
        'my-team',
        expect.stringContaining('rate limit has reset')
      );
    } finally {
      getConfigSpy.mockRestore();
    }
  });

  it('schedules auto-resume from api_retry model_cooldown payloads during provisioning', async () => {
    vi.setSystemTime(new Date('2026-04-17T12:00:00.000Z'));

    const service = new TeamProvisioningService();
    seedConfig('my-team');
    const run = attachRun(service, 'my-team', {
      provisioningComplete: false,
      detectedSessionId: 'sess-live',
    });
    (run as unknown as { progress: Record<string, unknown> }).progress = {
      state: 'starting',
      updatedAt: '2026-04-17T12:00:00.000Z',
    };
    const onProgress = vi.fn();
    (run as unknown as { onProgress: (progress: unknown) => void }).onProgress = onProgress;

    const autoResumeProvisioning = {
      getCurrentRunId: vi.fn(() => 'run-1' as string | null),
      isTeamAlive: vi.fn(() => true),
      sendMessageToTeam: vi.fn(async () => undefined),
    };
    initializeAutoResumeService(autoResumeProvisioning);

    const configManager = ConfigManager.getInstance();
    const actualConfig = configManager.getConfig();
    const getConfigSpy = vi.spyOn(configManager, 'getConfig').mockImplementation(
      () =>
        ({
          ...actualConfig,
          notifications: {
            ...actualConfig.notifications,
            autoResumeOnRateLimit: true,
          },
        }) as never
    );

    try {
      callHandleStreamJsonMessage(service, run, {
        type: 'system',
        subtype: 'api_retry',
        timestamp: '2026-04-17T12:00:00.000Z',
        attempt: 1,
        max_retries: 10,
        error_status: 429,
        error: 'model_cooldown',
        error_message:
          '429 {"error":{"code":"model_cooldown","message":"All credentials for model claude-opus-4-6 are cooling down via provider claude","model":"claude-opus-4-6","provider":"claude","reset_seconds":41,"reset_time":"40s"}}',
        retry_delay_ms: 41_000,
      });

      expect(onProgress).toHaveBeenCalled();

      await vi.advanceTimersByTimeAsync(41 * 1000 + 29 * 1000);
      expect(autoResumeProvisioning.sendMessageToTeam).not.toHaveBeenCalled();

      await vi.advanceTimersByTimeAsync(1500);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledTimes(1);
      expect(autoResumeProvisioning.sendMessageToTeam).toHaveBeenCalledWith(
        'my-team',
        expect.stringContaining('rate limit has reset')
      );
    } finally {
      getConfigSpy.mockRestore();
    }
  });
});
