jest.mock('uuid', () => ({
  v4: jest.fn(() => 'mock-uuid-v4'),
  v7: jest.fn(() => 'mock-uuid-v7'),
}));

import { NetaClawAgentChannelService } from '../../../../src/modules/netaclaw/service/agent_channel.js';

const BOT_ACC = 'bot-acc';

function buildService(opts: {
  agentExecutorImpl?: (params: any) => Promise<any>;
  groupFindByKey?: jest.Mock;
  groupTouchActive?: jest.Mock;
  extractText?: (items: any[]) => string;
} = {}): NetaClawAgentChannelService {
  const service = new NetaClawAgentChannelService();
  service.logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() } as any;
  service.weixinService = {
    sendText: jest.fn().mockResolvedValue(undefined),
    extractText: opts.extractText ?? ((items: any[]) => items?.[0]?.text_item?.text ?? items?.[0]?.text ?? ''),
  } as any;
  service.agentExecutor = {
    execute: jest.fn().mockImplementation(
      opts.agentExecutorImpl
        ?? (async () => ({ content: 'ok', sessionId: 's', usage: undefined, toolCallCount: 0 }))
    ),
  } as any;
  service.channelRepo = {
    update: jest.fn().mockResolvedValue(undefined),
    save: jest.fn().mockResolvedValue(undefined),
    findOneBy: jest.fn().mockResolvedValue(null),
  } as any;
  service.groupService = {
    findByKey: opts.groupFindByKey ?? jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 0, triggerMode: 'at_mention', triggerPrefix: null, boundAgentId: null,
    }),
    touchActive: opts.groupTouchActive ?? jest.fn().mockResolvedValue(undefined),
  } as any;
  // weixin-db 渠道群 path 会调 weixinDbService.replyToGroup(当前尚未实现, throw 被吞掉)
  (service as any).weixinDbService = {
    replyToGroup: jest.fn().mockRejectedValue(new Error('[weixin-db] replyToGroup not implemented yet')),
    refreshWhitelist: jest.fn().mockResolvedValue(undefined),
    unbindChannel: jest.fn(),
    bindChannel: jest.fn().mockResolvedValue(undefined),
  };
  return service;
}

function channel(override: Record<string, unknown> = {}): any {
  return {
    id: 7, type: 'weixin-db', status: 1, loginStatus: 'connected',
    agentId: 1, agentName: 'a',
    credential: { accountId: BOT_ACC, wxid: 'wxid_bot', nickname: '小神' },
    config: { group: { botAlias: '小神' } },
    ...override,
  };
}

function buildState() {
  return { stopped: false, syncBuf: '', contextTokens: {}, seenMessageIds: {} };
}

function flushQueues() {
  return new Promise(resolve => setTimeout(resolve, 30));
}

describe('routeInboundMessage group path', () => {
  it('whitelisted group with status=0 never dispatches to agent', async () => {
    const service = buildService();
    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice',
      room_id: 'r1@chatroom',
      message_id: 'm1',
      item_list: [{ text_item: { text: '@小神 你好' } }],
    });
    await flushQueues();
    expect(service.groupService.findByKey).toHaveBeenCalledWith(7, 'r1@chatroom');
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('enabled + at_mention triggers agent only when @bot, cleaned text passed through', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'at_mention', triggerPrefix: null, boundAgentId: 1,
    });
    const service = buildService({ groupFindByKey });

    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm1',
      item_list: [{ text_item: { text: '你好' } }],
    });
    await flushQueues();
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();

    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm2',
      item_list: [{ text_item: { text: '@小神 帮我查天气' } }],
    });
    await flushQueues();
    expect(service.agentExecutor.execute).toHaveBeenCalledTimes(1);
    const call = (service.agentExecutor.execute as jest.Mock).mock.calls[0][0];
    expect(call.chatScope).toBe('group');
    expect(call.sessionId).toBe('channel:7:weixin:group:r1@chatroom');
    expect(call.message).toBe('帮我查天气');
    expect(call.userId).toBe('alice');
    expect(call.onRiskConfirmRequest).toBeUndefined();
  });

  it('triggerMode=all accepts any message', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'all', triggerPrefix: null, boundAgentId: 1,
    });
    const service = buildService({ groupFindByKey });
    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm1',
      item_list: [{ text_item: { text: '随便聊' } }],
    });
    await flushQueues();
    const call = (service.agentExecutor.execute as jest.Mock).mock.calls[0][0];
    expect(call.message).toBe('随便聊');
  });

  it('same group messages from multiple senders strictly serialize (group-wide queue)', async () => {
    const order: string[] = [];
    let resolve1: () => void = () => {};
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'all', triggerPrefix: null, boundAgentId: 1,
    });
    const service = buildService({
      groupFindByKey,
      agentExecutorImpl: async (params: any) => {
        order.push(`enter:${params.message}`);
        if (params.message.startsWith('first')) await new Promise<void>(r => { resolve1 = r; });
        order.push(`exit:${params.message}`);
        return { content: 'ok', sessionId: 's', usage: undefined, toolCallCount: 0 };
      },
    });

    const state = buildState();
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm1',
      item_list: [{ text_item: { text: 'first from alice' } }],
    });
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'bob', room_id: 'r1@chatroom', message_id: 'm2',
      item_list: [{ text_item: { text: 'second from bob' } }],
    });

    await new Promise(r => setTimeout(r, 20));
    expect(order).toEqual(['enter:first from alice']);
    resolve1();
    await flushQueues();
    expect(order).toEqual([
      'enter:first from alice', 'exit:first from alice',
      'enter:second from bob', 'exit:second from bob',
    ]);
  });

  it('different rooms process in parallel', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'rX', roomName: 'rX',
      status: 1, triggerMode: 'all', triggerPrefix: null, boundAgentId: 1,
    });
    let peak = 0;
    const inflight: number[] = [];
    const service = buildService({
      groupFindByKey,
      agentExecutorImpl: async () => {
        inflight.push(1);
        peak = Math.max(peak, inflight.length);
        await new Promise(r => setTimeout(r, 15));
        inflight.pop();
        return { content: 'ok', sessionId: 's', usage: undefined, toolCallCount: 0 };
      },
    });
    const state = buildState();
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'alice', room_id: 'rA@chatroom', message_id: 'm1',
      item_list: [{ text_item: { text: 'msg1' } }],
    });
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'alice', room_id: 'rB@chatroom', message_id: 'm2',
      item_list: [{ text_item: { text: 'msg2' } }],
    });
    await flushQueues();
    expect(peak).toBe(2);
  });

  it('group pendingClarify reply short-circuits: initiator reply is not dispatched as new message', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'at_mention', triggerPrefix: null, boundAgentId: null,
    });
    const service = buildService({ groupFindByKey });

    let resolved: string | undefined;
    (service as any).pendingClarify.set(
      'channel:7:weixin:group:r1@chatroom:alice',
      { resolve: (answer: string) => { resolved = answer; } },
    );

    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm-reply',
      item_list: [{ text_item: { text: '1' } }],
    });
    await flushQueues();

    expect(resolved).toBe('1');
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('non-initiator reply of "1" is NOT consumed as clarify, falls back to trigger gate', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'at_mention', triggerPrefix: null, boundAgentId: null,
    });
    const service = buildService({ groupFindByKey });

    let resolved: string | undefined;
    (service as any).pendingClarify.set(
      'channel:7:weixin:group:r1@chatroom:alice',
      { resolve: (answer: string) => { resolved = answer; } },
    );

    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'bob', room_id: 'r1@chatroom', message_id: 'm-bob',
      item_list: [{ text_item: { text: '1' } }],
    });
    await flushQueues();

    expect(resolved).toBeUndefined();
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('skips echo messages from bot itself', async () => {
    const service = buildService();
    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: BOT_ACC, room_id: 'r1@chatroom', message_id: 'mecho',
      item_list: [{ text_item: { text: 'anything' } }],
    });
    await flushQueues();
    expect(service.groupService.findByKey).not.toHaveBeenCalled();
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('deduplicates by message_id', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'all', triggerPrefix: null, boundAgentId: 1,
    });
    const service = buildService({ groupFindByKey });
    const state = buildState();
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'dup',
      item_list: [{ text_item: { text: '你好' } }],
    });
    (service as any).routeInboundMessage(channel(), state, {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'dup',
      item_list: [{ text_item: { text: '你好再来' } }],
    });
    await flushQueues();
    expect(service.agentExecutor.execute).toHaveBeenCalledTimes(1);
  });

  it('weixin-db group without boundAgentId does not fall back to channel default agent', async () => {
    const groupFindByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 7, roomId: 'r1@chatroom', roomName: 'r1@chatroom',
      status: 1, triggerMode: 'all', triggerPrefix: null, boundAgentId: null,
    });
    const service = buildService({ groupFindByKey });
    (service as any).routeInboundMessage(channel(), buildState(), {
      from_user_id: 'alice', room_id: 'r1@chatroom', message_id: 'm-no-agent',
      item_list: [{ text_item: { text: '随便聊' } }],
    });
    await flushQueues();
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });
});
