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 buildChannelService(opts: {
  agentExecutorImpl?: (params: any) => Promise<any>;
  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 ?? ''),
  } 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 = {
    upsertOnInbound: jest.fn().mockResolvedValue({ created: false }),
    findByKey: jest.fn().mockResolvedValue(null),
    touchActive: jest.fn().mockResolvedValue(undefined),
  } as any;
  return service;
}

function buildChannelEntity(): any {
  return {
    id: 7,
    type: 'weixin',
    status: 1,
    loginStatus: 'connected',
    agentId: 1,
    agentName: 'a',
    credential: { accountId: 'bot', token: 't', baseUrl: 'http', userId: 'bot' },
  };
}

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

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

describe('NetaClawAgentChannelService.buildRiskConfirmPrompt', () => {
  it('formats risk confirm prompt with numbered options and reason', () => {
    const text = NetaClawAgentChannelService.buildRiskConfirmPrompt({
      toolName: 'bash',
      toolLabel: 'Bash',
      riskReason: '包含 rm -rf 命令',
    });
    expect(text).toContain('Bash');
    expect(text).toContain('包含 rm -rf 命令');
    expect(text).toContain('1. 确认执行');
    expect(text).toContain('2. 不执行');
  });

  it('falls back to toolName when toolLabel missing', () => {
    const text = NetaClawAgentChannelService.buildRiskConfirmPrompt({
      toolName: 'memory_save',
      riskReason: '原因',
    });
    expect(text).toContain('memory_save');
  });
});

describe('NetaClawAgentChannelService.parseRiskConfirmReply', () => {
  it('treats "1" as confirmed', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('1')).toBe(true);
  });

  it('treats "2" as declined', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('2')).toBe(false);
  });

  it('treats Chinese affirmative as confirmed', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('确认')).toBe(true);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('确认执行')).toBe(true);
  });

  it('treats Chinese cancel as declined', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('取消')).toBe(false);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('不执行')).toBe(false);
  });

  it('treats "yes"/"y" as confirmed', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('yes')).toBe(true);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('Y')).toBe(true);
  });

  it('treats arbitrary text and empty input as declined', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('随便')).toBe(false);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('')).toBe(false);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('  ')).toBe(false);
  });

  it('trims surrounding whitespace before matching', () => {
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('  1  ')).toBe(true);
    expect(NetaClawAgentChannelService.parseRiskConfirmReply('  确认  ')).toBe(true);
  });
});

describe('NetaClawAgentChannelService.routeInboundMessage', () => {
  it('resolves pendingClarify reply synchronously and does not dispatch a new agent run', async () => {
    const service = buildChannelService();
    const channel = buildChannelEntity();
    const state = buildState();

    const clarifyKey = `channel:${channel.id}:weixin:user-A`;
    const resolved: string[] = [];
    (service as any).pendingClarify.set(clarifyKey, {
      resolve: (answer: string) => resolved.push(answer),
      choices: ['确认执行', '不执行'],
    });

    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'm1',
      item_list: [{ text: '2' }],
    });

    expect(resolved).toEqual(['不执行']);
    expect((service as any).pendingClarify.has(clarifyKey)).toBe(false);
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('does not block runLoop when handler is slow (the death-lock fix)', async () => {
    let releaseHandler: () => void = () => undefined;
    const handlerStart = jest.fn();

    const service = buildChannelService({
      agentExecutorImpl: async () => {
        handlerStart();
        await new Promise<void>(resolve => {
          releaseHandler = resolve;
        });
        return { content: 'done', sessionId: 's', usage: undefined, toolCallCount: 0 };
      },
    });
    const channel = buildChannelEntity();
    const state = buildState();
    const clarifyKey = `channel:${channel.id}:weixin:user-A`;

    // 第一条消息：触发 agent，handler 内部因等 risk reply 卡住
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'm1',
      item_list: [{ text: '请删除 3.xlsx' }],
    });

    // 模拟 agent 进入 risk confirm 等待
    await flushPromises();
    expect(handlerStart).toHaveBeenCalledTimes(1);

    let pendingResolved: string | undefined;
    (service as any).pendingClarify.set(clarifyKey, {
      resolve: (answer: string) => {
        pendingResolved = answer;
      },
      choices: undefined,
    });

    // 第二条消息：用户的 "取消"。在死锁修复前，runLoop 在 await m1，根本拉不到 m2。
    // 修复后，routeInboundMessage 应当同步 resolve 这条 pending。
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'm2',
      item_list: [{ text: '取消' }],
    });

    expect(pendingResolved).toBe('取消');
    // 后续 release，handler 才结束 —— 但即便永不 release 也不影响下条消息
    releaseHandler();
    await flushPromises();
  });

  it('serializes messages from the same sender', async () => {
    const order: string[] = [];
    const service = buildChannelService({
      agentExecutorImpl: async (params: any) => {
        order.push(`enter:${params.message}`);
        await new Promise(resolve => setTimeout(resolve, 10));
        order.push(`exit:${params.message}`);
        return { content: 'ok', sessionId: 's', usage: undefined, toolCallCount: 0 };
      },
    });
    const channel = buildChannelEntity();
    const state = buildState();

    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'm1',
      item_list: [{ text: 'first' }],
    });
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'm2',
      item_list: [{ text: 'second' }],
    });

    // 等待全部 promise 完成
    await new Promise(resolve => setTimeout(resolve, 80));

    // 同一 sender 必须严格交替，enter 与 exit 不能交错
    expect(order).toEqual([
      'enter:first', 'exit:first',
      'enter:second', 'exit:second',
    ]);
  });

  it('processes different senders in parallel', async () => {
    const inFlight: number[] = [];
    let peak = 0;
    const service = buildChannelService({
      agentExecutorImpl: async () => {
        inFlight.push(1);
        peak = Math.max(peak, inFlight.length);
        await new Promise(resolve => setTimeout(resolve, 20));
        inFlight.pop();
        return { content: 'ok', sessionId: 's', usage: undefined, toolCallCount: 0 };
      },
    });
    const channel = buildChannelEntity();
    const state = buildState();

    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'mA',
      item_list: [{ text: 'hi' }],
    });
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-B',
      message_id: 'mB',
      item_list: [{ text: 'hi' }],
    });
    await new Promise(resolve => setTimeout(resolve, 80));
    expect(peak).toBe(2);
  });

  it('skips group chat messages', () => {
    const service = buildChannelService();
    const channel = buildChannelEntity();
    const state = buildState();
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      room_id: 'group-1',
      message_id: 'm1',
      item_list: [{ text: '群消息' }],
    });
    // Plan C Task 9: ClawBot (type=weixin) 渠道 group 消息走 drop_group_on_clawbot 日志
    expect(service.logger.debug).toHaveBeenCalledWith(
      expect.stringContaining('drop_group_on_clawbot'),
      expect.anything(), expect.anything(),
    );
    expect(service.agentExecutor.execute).not.toHaveBeenCalled();
  });

  it('deduplicates by message_id', async () => {
    const service = buildChannelService();
    const channel = buildChannelEntity();
    const state = buildState();
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'dup',
      item_list: [{ text: 'first' }],
    });
    (service as any).routeInboundMessage(channel, state, {
      from_user_id: 'user-A',
      message_id: 'dup',
      item_list: [{ text: 'first-again' }],
    });
    await new Promise(resolve => setTimeout(resolve, 30));
    expect(service.agentExecutor.execute).toHaveBeenCalledTimes(1);
  });
});

describe('NetaClawAgentChannelService.page', () => {
  it('enriches weixin-db channel with wxid + wechatVersion, leaves non-db channel untouched', async () => {
    const service = new NetaClawAgentChannelService();
    service.logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() } as any;

    const dbChannel: any = {
      id: 11,
      type: 'weixin-db',
      credential: {
        wxid: 'wxid_x',
        wechatVersion: '4.1.8.107',
      },
    };
    const clawChannel: any = {
      id: 12,
      type: 'weixin',
      credential: { accountId: 'bot' },
    };

    service.channelRepo = {
      findAndCount: jest.fn().mockResolvedValue([[dbChannel, clawChannel], 2]),
    } as any;
    service.groupService = {
      statsByChannels: jest.fn().mockResolvedValue(
        new Map<number, { total: number; enabled: number }>([
          [11, { total: 3, enabled: 2 }],
          [12, { total: 0, enabled: 0 }],
        ])
      ),
    } as any;

    const res = await service.page({ page: 1, size: 20 });

    expect(res.pagination).toEqual({ page: 1, size: 20, total: 2 });
    expect(res.list).toHaveLength(2);

    const dbItem: any = res.list[0];
    expect(dbItem).toMatchObject({
      id: 11,
      type: 'weixin-db',
      groupTotal: 3,
      groupEnabled: 2,
      wechatVersion: '4.1.8.107',
      wxid: 'wxid_x',
    });
    // 不再有 bridgeOnline / profileName / nickname 字段
    expect(dbItem.bridgeOnline).toBeUndefined();

    const clawItem: any = res.list[1];
    expect(clawItem).toMatchObject({
      id: 12,
      type: 'weixin',
      groupTotal: 0,
      groupEnabled: 0,
    });
    // 非 db 渠道不应被注入 db 字段
    expect(clawItem).not.toHaveProperty('wechatVersion');
    expect(clawItem).not.toHaveProperty('wxid');
  });

  it('weixin-db with null credential → wechatVersion/wxid default to null', async () => {
    const service = new NetaClawAgentChannelService();
    service.logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() } as any;

    const dbChannel: any = {
      id: 21,
      type: 'weixin-db',
      credential: null,
    };
    service.channelRepo = {
      findAndCount: jest.fn().mockResolvedValue([[dbChannel], 1]),
    } as any;
    service.groupService = {
      statsByChannels: jest.fn().mockResolvedValue(new Map()),
    } as any;

    const res = await service.page({});
    const item: any = res.list[0];
    expect(item.wechatVersion).toBeNull();
    expect(item.wxid).toBeNull();
    expect(item.groupTotal).toBe(0);
    expect(item.groupEnabled).toBe(0);
  });
});
