import {
  decideChatScope,
  buildDmSessionId,
  buildGroupSessionId,
  buildDmDispatchKey,
  buildGroupDispatchKey,
  buildDmClarifyKey,
  buildGroupClarifyKey,
  detectAtMention,
  stripLeadingMention,
  stripLeadingPrefix,
} from '../../../../src/modules/netaclaw/runtime/chat_scope.js';

describe('decideChatScope', () => {
  const botAccountId = 'bot-acc';

  it('classifies pure DM by from_user_id', () => {
    expect(decideChatScope({ from_user_id: 'alice', to_user_id: botAccountId }, botAccountId)).toEqual({
      kind: 'dm', chatId: 'alice', senderId: 'alice',
    });
  });

  it('classifies group chat when room_id present', () => {
    expect(decideChatScope({ from_user_id: 'alice', room_id: 'room-1@chatroom' }, botAccountId)).toEqual({
      kind: 'group', chatId: 'room-1@chatroom', senderId: 'alice',
    });
  });

  it('classifies group chat when chat_room_id alias present', () => {
    expect(decideChatScope({ from_user_id: 'bob', chat_room_id: 'room-2@chatroom' }, botAccountId)).toEqual({
      kind: 'group', chatId: 'room-2@chatroom', senderId: 'bob',
    });
  });

  it('returns null sender id when from_user_id missing', () => {
    expect(decideChatScope({}, botAccountId)).toBeNull();
  });

  it('returns null when sender equals bot account (echo message)', () => {
    expect(decideChatScope({ from_user_id: botAccountId }, botAccountId)).toBeNull();
  });
});

describe('session / dispatch / clarify key builders', () => {
  it('builds stable DM session id', () => {
    expect(buildDmSessionId(7, 'alice')).toBe('channel:7:weixin:alice');
  });

  it('builds stable group session id', () => {
    expect(buildGroupSessionId(7, 'room-1@chatroom')).toBe('channel:7:weixin:group:room-1@chatroom');
  });

  it('DM dispatch key matches session id (per-sender serial)', () => {
    expect(buildDmDispatchKey(7, 'alice')).toBe('channel:7:weixin:alice');
  });

  it('group dispatch key omits senderId for group-wide serial', () => {
    expect(buildGroupDispatchKey(7, 'room-1@chatroom')).toBe('channel:7:weixin:group:room-1@chatroom');
  });

  it('DM clarify key same as dispatch key', () => {
    expect(buildDmClarifyKey(7, 'alice')).toBe('channel:7:weixin:alice');
  });

  it('group clarify key includes senderId so only initiator can answer', () => {
    expect(buildGroupClarifyKey(7, 'room-1@chatroom', 'alice'))
      .toBe('channel:7:weixin:group:room-1@chatroom:alice');
  });
});

describe('detectAtMention', () => {
  const aliases = ['小神'];
  const accountIds = ['bot-acc'];

  it('hits via at_user_list protocol field (accountId match)', () => {
    expect(detectAtMention({ at_user_list: ['bot-acc', 'someone'] }, '', aliases, accountIds)).toBe(true);
  });

  it('hits via mention_list protocol field (accountId match)', () => {
    expect(detectAtMention({ mention_list: [{ user_id: 'bot-acc' }] }, '', aliases, accountIds)).toBe(true);
  });

  it('hits via text fallback with leading @alias + space', () => {
    expect(detectAtMention({}, '@小神 你好', aliases, accountIds)).toBe(true);
  });

  it('hits via text fallback with @alias + U+2005 quarter em space', () => {
    expect(detectAtMention({}, '@小神 你好', aliases, accountIds)).toBe(true);
  });

  it('hits via text fallback with @alias mid-sentence (after a space)', () => {
    expect(detectAtMention({}, '今天 @小神 帮看下', aliases, accountIds)).toBe(true);
  });

  it('misses when alias is a prefix of another token (@小神同学)', () => {
    expect(detectAtMention({}, '@小神同学 你好', aliases, accountIds)).toBe(false);
  });

  it('misses when alias appears inside group-name-like token (@小神聊天群)', () => {
    expect(detectAtMention({}, '@小神聊天群 大家好', aliases, accountIds)).toBe(false);
  });

  it('misses when @ is preceded by alphanumeric (email-like, P2-6)', () => {
    expect(detectAtMention({}, '我的邮箱 email@小神.com', aliases, accountIds)).toBe(false);
    expect(detectAtMention({}, 'foo@小神 帮我', aliases, accountIds)).toBe(false);
  });

  it('hits when alias is followed by Chinese punctuation', () => {
    expect(detectAtMention({}, '@小神，你好', aliases, accountIds)).toBe(true);
  });

  it('misses when neither protocol field nor text mentions bot', () => {
    expect(detectAtMention({}, '你好', aliases, accountIds)).toBe(false);
  });

  it('returns false when aliases empty and no protocol hit (botAlias not configured)', () => {
    expect(detectAtMention({}, '@anyone 你好', [], accountIds)).toBe(false);
  });
});

describe('stripLeadingMention', () => {
  it('removes leading @alias and trailing whitespace', () => {
    expect(stripLeadingMention('@小神 你好', ['小神'])).toBe('你好');
  });

  it('removes leading @alias with U+2005', () => {
    expect(stripLeadingMention('@小神 你好', ['小神'])).toBe('你好');
  });

  it('preserves mid-string @alias', () => {
    expect(stripLeadingMention('hello @小神 世界', ['小神'])).toBe('hello @小神 世界');
  });

  it('returns original when nothing to strip', () => {
    expect(stripLeadingMention('你好', ['小神'])).toBe('你好');
  });

  it('is no-op with empty aliases', () => {
    expect(stripLeadingMention('@小神 你好', [])).toBe('@小神 你好');
  });
});

describe('stripLeadingPrefix', () => {
  it('removes leading prefix and whitespace', () => {
    expect(stripLeadingPrefix('/bot help', '/bot')).toBe('help');
  });

  it('is no-op when prefix does not match', () => {
    expect(stripLeadingPrefix('/other help', '/bot')).toBe('/other help');
  });

  it('returns original with empty prefix', () => {
    expect(stripLeadingPrefix('hello', '')).toBe('hello');
  });
});

import { decideGroupAcceptance } from '../../../../src/modules/netaclaw/runtime/chat_scope.js';

describe('decideGroupAcceptance', () => {
  const group = (overrides: Record<string, unknown> = {}) => ({
    id: 1, channelId: 7, roomId: 'room-1@chatroom', roomName: null,
    status: 1, triggerMode: 'at_mention', triggerPrefix: null,
    ...overrides,
  });

  const aliases = ['小神'];
  const accountIds = ['bot-acc'];

  it('rejects when group.status === 0', () => {
    const result = decideGroupAcceptance(group({ status: 0 }), {}, '@小神 你好', aliases, accountIds);
    expect(result.accept).toBe(false);
    expect(result.reason).toBe('group_disabled');
  });

  it('triggerMode=all accepts any text and keeps it as-is', () => {
    const result = decideGroupAcceptance(group({ triggerMode: 'all' }), {}, '随便什么消息', aliases, accountIds);
    expect(result.accept).toBe(true);
    expect(result.cleanedText).toBe('随便什么消息');
  });

  it('triggerMode=prefix accepts when text starts with prefix and strips it', () => {
    const result = decideGroupAcceptance(
      group({ triggerMode: 'prefix', triggerPrefix: '/bot' }),
      {}, '/bot help', aliases, accountIds,
    );
    expect(result.accept).toBe(true);
    expect(result.cleanedText).toBe('help');
  });

  it('triggerMode=prefix rejects when text does not start with prefix', () => {
    const result = decideGroupAcceptance(
      group({ triggerMode: 'prefix', triggerPrefix: '/bot' }),
      {}, 'hello', aliases, accountIds,
    );
    expect(result.accept).toBe(false);
    expect(result.reason).toBe('prefix_mismatch');
  });

  it('triggerMode=prefix rejects with reason when triggerPrefix empty', () => {
    const result = decideGroupAcceptance(
      group({ triggerMode: 'prefix', triggerPrefix: null }),
      {}, '/bot help', aliases, accountIds,
    );
    expect(result.accept).toBe(false);
    expect(result.reason).toBe('prefix_not_configured');
  });

  it('triggerMode=at_mention accepts when @bot and strips leading mention', () => {
    const result = decideGroupAcceptance(
      group(),
      {}, '@小神 帮我查天气', aliases, accountIds,
    );
    expect(result.accept).toBe(true);
    expect(result.cleanedText).toBe('帮我查天气');
  });

  it('triggerMode=at_mention rejects when no @bot', () => {
    const result = decideGroupAcceptance(group(), {}, '你好', aliases, accountIds);
    expect(result.accept).toBe(false);
    expect(result.reason).toBe('not_mentioned');
  });

  it('triggerMode=at_mention accepts via protocol at_user_list even when text has no @', () => {
    const result = decideGroupAcceptance(
      group(),
      { at_user_list: ['bot-acc'] },
      '帮我查天气', aliases, accountIds,
    );
    expect(result.accept).toBe(true);
    expect(result.cleanedText).toBe('帮我查天气');
  });
});
