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';

function buildSvc(): NetaClawAgentChannelService {
  const svc = new NetaClawAgentChannelService();
  (svc as any).logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() };
  (svc as any).weixinService = {
    sendText: jest.fn().mockResolvedValue(undefined),
    extractText: (items: any[]) => items?.[0]?.text_item?.text ?? items?.[0]?.text ?? '',
  };
  (svc as any).agentExecutor = {
    execute: jest.fn().mockResolvedValue({ content: 'hi', sessionId: 's', usage: undefined, toolCallCount: 0 }),
  };
  (svc as any).channelRepo = {
    update: jest.fn().mockResolvedValue(undefined),
    save: jest.fn().mockResolvedValue(undefined),
    findOneBy: jest.fn().mockImplementation(async ({ id }: any) => ({
      id, type: 'weixin-db', status: 1, loginStatus: 'connected', agentId: 1, agentName: 'a',
      credential: { wxid: 'wxid_x' }, config: {},
    })),
  };
  (svc as any).groupService = {
    findByKey: jest.fn().mockResolvedValue({
      id: 1, channelId: 5, roomId: '产品研发群', roomName: '产品研发群', status: 1,
      triggerMode: 'at_mention', boundAgentId: 1,
    }),
    touchActive: jest.fn().mockResolvedValue(undefined),
  };
  (svc as any).weixinDbService = {
    refreshWhitelist: jest.fn().mockResolvedValue(undefined),
    unbindChannel: jest.fn(),
    bindChannel: jest.fn().mockResolvedValue(undefined),
  };
  (svc as any).archiveSyncService = {
    deleteChannelArchive: jest.fn().mockResolvedValue(undefined),
  };
  (svc as any).desktopOpService = {
    abortByFilter: jest.fn(),
  };
  return svc;
}

describe('agent_channel weixin-db routing', () => {
  it('drops DM when weixin-db channel receives no room_id', async () => {
    const svc = buildSvc();
    const channel = { id: 5, type: 'weixin-db', credential: { wxid: 'x' } } as any;
    const state = { stopped: false, syncBuf: '', contextTokens: {}, seenMessageIds: {} };
    const pseudo = {
      from_user_id: 'wxid_alice',
      message_id: 'm1',
      item_list: [{ type: 1, text_item: { text: 'hi' } }],
      // no room_id
    };
    (svc as any).routeInboundMessage(channel, state, pseudo);
    await new Promise(r => setTimeout(r, 20));
    expect((svc as any).agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('routes group message via reply agent (no auto-send)', async () => {
    const svc = buildSvc();
    const channel = {
      id: 5, type: 'weixin-db', status: 1, loginStatus: 'connected',
      agentId: 1, agentName: 'agent-a', credential: { wxid: 'wxid_x' }, config: {},
    } as any;
    const state = { stopped: false, syncBuf: '', contextTokens: {}, seenMessageIds: {} };
    const pseudo = {
      from_user_id: 'wxid_alice',
      room_id: '5:room:gA@chatroom',
      room_name: '产品研发群',
      message_id: 's1',
      item_list: [{ type: 1, text_item: { text: 'hello agent' } }],
      sender_remark: 'Alice备注',
      sender_nick_name: 'Alice昵称',
      sender_display_name: 'Alice备注',
      __weixin_db: true,
    };
    // 用 all 模式绕开 @bot 校验
    (svc as any).groupService.findByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 5, roomId: '产品研发群', roomName: '产品研发群', status: 1,
      triggerMode: 'all', boundAgentId: 1,
    });
    (svc as any).routeInboundMessage(channel, state, pseudo);
    await new Promise(r => setTimeout(r, 50));
    expect((svc as any).agentExecutor.execute).toHaveBeenCalled();
    // v4: replyToGroup 已删除，reply agent 通过 delegate_task 完成发送
    // agent 执行器收到 chatScope=group
    const execArg = (svc as any).agentExecutor.execute.mock.calls[0][0];
    expect(execArg.chatScope).toBe('group');
    expect(execArg.bizContext.senderId).toBe('wxid_alice');
    expect(execArg.bizContext.senderRemark).toBe('Alice备注');
    expect(execArg.bizContext.senderNickName).toBe('Alice昵称');
    expect(execArg.bizContext.senderDisplayName).toBe('Alice备注');
  });

  it('drops weixin-db watermarked messages sent by desktop tool', async () => {
    const svc = buildSvc();
    const channel = {
      id: 5, type: 'weixin-db', status: 1, loginStatus: 'connected',
      agentId: 1, agentName: 'agent-a', credential: { wxid: 'wxid_x' }, config: {},
    } as any;
    const state = { stopped: false, syncBuf: '', contextTokens: {}, seenMessageIds: {} };
    const pseudo = {
      from_user_id: '2',
      room_id: '5:room:gA@chatroom',
      room_name: '产品研发群',
      message_id: 's-ai',
      item_list: [{ type: 1, text_item: { text: '测试消息 —AI' } }],
      sender_wxid: 'shin_seed',
      sender_nick_name: 'if',
      sender_display_name: 'if',
      __weixin_db: true,
    };
    (svc as any).groupService.findByKey = jest.fn().mockResolvedValue({
      id: 1, channelId: 5, roomId: '产品研发群', roomName: '产品研发群', status: 1,
      triggerMode: 'all', boundAgentId: 1,
    });
    (svc as any).routeInboundMessage(channel, state, pseudo);
    await new Promise(r => setTimeout(r, 30));
    expect((svc as any).agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('drops group message when no whitelist record (findByKey null)', async () => {
    const svc = buildSvc();
    (svc as any).groupService.findByKey = jest.fn().mockResolvedValue(null);
    const channel = {
      id: 5, type: 'weixin-db', status: 1, loginStatus: 'connected',
      agentId: 1, agentName: 'a', credential: { wxid: 'wxid_x' }, config: {},
    } as any;
    const state = { stopped: false, syncBuf: '', contextTokens: {}, seenMessageIds: {} };
    const pseudo = {
      from_user_id: 'wxid_alice',
      room_id: '5:room:未知群',
      room_name: '未知群',
      message_id: 's2',
      item_list: [{ type: 1, text_item: { text: 'hi' } }],
    };
    (svc as any).routeInboundMessage(channel, state, pseudo);
    await new Promise(r => setTimeout(r, 30));
    expect((svc as any).agentExecutor.execute).not.toHaveBeenCalled();
    // v4: replyToGroup 已删除，此处只验证 agentExecutor 未被调用
  });

  it('delete() unbinds channel via weixinDbService', async () => {
    const svc = buildSvc();
    (svc as any).groupService.cascadeDeleteByChannel = jest.fn().mockResolvedValue(undefined);
    (svc as any).channelRepo.delete = jest.fn().mockResolvedValue(undefined);
    await (svc as any).delete([7, 9]);
    expect((svc as any).weixinDbService.unbindChannel).toHaveBeenCalledWith(7);
    expect((svc as any).weixinDbService.unbindChannel).toHaveBeenCalledWith(9);
    expect((svc as any).archiveSyncService.deleteChannelArchive).toHaveBeenCalledWith(7);
    expect((svc as any).archiveSyncService.deleteChannelArchive).toHaveBeenCalledWith(9);
    expect((svc as any).channelRepo.delete).toHaveBeenCalledWith([7, 9]);
  });

  it('toggleGroupAndNotify refreshes whitelist for weixin-db channel', async () => {
    const svc = buildSvc();
    (svc as any).groupService.findById = jest.fn().mockResolvedValue({
      id: 1, channelId: 5, roomName: '产品研发群',
    });
    (svc as any).groupService.toggle = jest.fn().mockResolvedValue(undefined);
    await svc.toggleGroupAndNotify(1, 1);
    expect((svc as any).weixinDbService.refreshWhitelist).toHaveBeenCalledWith(5);
  });

  it('toggleGroupAndNotify skips refresh for non weixin-db channel', async () => {
    const svc = buildSvc();
    (svc as any).groupService.findById = jest.fn().mockResolvedValue({
      id: 1, channelId: 5, roomName: '产品研发群',
    });
    (svc as any).groupService.toggle = jest.fn().mockResolvedValue(undefined);
    // info 返回 ClawBot 渠道
    (svc as any).channelRepo.findOneBy = jest.fn().mockResolvedValue({
      id: 5, type: 'weixin',
    });
    await svc.toggleGroupAndNotify(1, 0);
    expect((svc as any).weixinDbService.refreshWhitelist).not.toHaveBeenCalled();
  });
});
