import {
  describe,
  expect,
  it
} from 'vitest';
import {
  CUSTOM_EMOJI_MAX_SIZE_BYTES,
  CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES,
  canInlineCustomEmojiTransfer,
  assembleCustomEmojiDataUrl,
  buildCustomEmojiMessageToken,
  buildCustomEmojiTextAlias,
  extractCustomEmojiIds,
  getCustomEmojiIdFromMarkdownAlt,
  getCustomEmojiLabelFromMarkdownAlt,
  isCustomEmojiOnlyMessage,
  isSingleUnicodeEmojiOnlyMessage,
  replaceCustomEmojiMessageTokens,
  replaceCustomEmojiTokensForPreview,
  splitCustomEmojiDataUrl,
  replaceCustomEmojiTextAliases,
  CUSTOM_EMOJI_ID_ATTRIBUTE,
  CUSTOM_EMOJI_CONTEXT_MENU_TARGET,
  CUSTOM_EMOJI_LIBRARY_CONTEXT_MENU_TARGET,
  filterCustomEmojisForPicker,
  filterUnicodeEmojiPickerEntries,
  resolveCustomEmojiContextMenuTarget,
  selectEmojiShortcutEntries,
  splitTextIntoEmojiSegments,
  UNICODE_EMOJI_PICKER_ENTRIES,
  validateCustomEmojiFile
} from './custom-emoji.rules';
import { CustomEmoji } from '../../../shared-kernel';

function customEmoji(id: string, name: string): CustomEmoji {
  return {
    id,
    name,
    creatorUserId: 'user-1',
    dataUrl: `data:image/webp;base64,${id}`,
    hash: `${id}-hash`,
    mime: 'image/webp',
    size: 128,
    createdAt: 100,
    updatedAt: 100
  };
}

describe('custom emoji rules', () => {
  it('rejects image uploads larger than 1 MB', () => {
    const result = validateCustomEmojiFile({
      name: 'large.webp',
      size: CUSTOM_EMOJI_MAX_SIZE_BYTES + 1,
      type: 'image/webp'
    });

    expect(result.valid).toBe(false);
    expect(result.reason).toContain('1 MB');
  });

  it('selects seven most-used shortcuts and keeps the eighth slot for the modal', () => {
    const usage = new Map<string, number>([
      ['unicode:🔥', 10],
      ['custom:party', 8],
      ['unicode:😂', 7],
      ['unicode:👍', 6],
      ['unicode:❤️', 5],
      ['custom:wave', 4],
      ['unicode:🎉', 3],
      ['unicode:👀', 2]
    ]);
    const entries = selectEmojiShortcutEntries({
      customEmojis: [customEmoji('party', 'party'), customEmoji('wave', 'wave')],
      usage,
      limit: 7
    });

    expect(entries).toHaveLength(7);
    expect(entries.map((entry) => entry.key)).toEqual([
      'unicode:🔥',
      'custom:party',
      'unicode:😂',
      'unicode:👍',
      'unicode:❤️',
      'custom:wave',
      'unicode:🎉'
    ]);
  });

  it('builds stable inline message markup for custom emoji sends', () => {
    expect(buildCustomEmojiMessageToken(customEmoji('party', 'party'))).toBe(':emoji[party](party)');
  });

  it('splits custom emoji data into bounded data-channel chunks', () => {
    const transfer = splitCustomEmojiDataUrl('data:image/webp;base64,abcdefghijkl', 5);

    expect(transfer).toEqual({
      chunks: [
        'abcde',
        'fghij',
        'kl'
      ],
      mime: 'image/webp',
      total: 3
    });

    expect(assembleCustomEmojiDataUrl(transfer.mime, transfer.chunks)).toBe('data:image/webp;base64,abcdefghijkl');
  });

  it('keeps default custom emoji chunks well below risky data-channel message sizes', () => {
    expect(CUSTOM_EMOJI_DATA_CHANNEL_CHUNK_SIZE_BYTES).toBeLessThanOrEqual(4 * 1024);
  });

  it('rewrites readable composer emoji aliases to stable custom emoji tokens', () => {
    const party = customEmoji('party-id', 'party');
    const wave = customEmoji('wave-id', 'wave');

    expect(buildCustomEmojiTextAlias(party)).toBe(':party:');
    expect(replaceCustomEmojiTextAliases('This is :party: cool and :wave:', (name) => ({ party, wave })[name] ?? null))
      .toBe('This is :emoji[party-id](party) cool and :emoji[wave-id](wave)');
  });

  it('detects when custom emoji are mixed with text', () => {
    expect(isCustomEmojiOnlyMessage(':emoji[party-id](party)')).toBe(true);
    expect(isCustomEmojiOnlyMessage(':emoji[party-id](party) :emoji[wave-id](wave)')).toBe(true);
    expect(isCustomEmojiOnlyMessage('This is :emoji[party-id](party) cool')).toBe(false);
  });

  it('detects a lone unicode emoji message for large chat rendering', () => {
    expect(isSingleUnicodeEmojiOnlyMessage('😢')).toBe(true);
    expect(isSingleUnicodeEmojiOnlyMessage('  😢  ')).toBe(true);
    expect(isSingleUnicodeEmojiOnlyMessage('😮')).toBe(true);
    expect(isSingleUnicodeEmojiOnlyMessage('❤️')).toBe(true);
    expect(isSingleUnicodeEmojiOnlyMessage('😢 😂')).toBe(false);
    expect(isSingleUnicodeEmojiOnlyMessage('Hello 😢')).toBe(false);
    expect(isSingleUnicodeEmojiOnlyMessage('😢!')).toBe(false);
    expect(isSingleUnicodeEmojiOnlyMessage(':emoji[party-id](party)')).toBe(false);
    expect(isSingleUnicodeEmojiOnlyMessage('')).toBe(false);
  });

  it('rewrites custom emoji tokens for deferred message placeholders', () => {
    expect(replaceCustomEmojiTokensForPreview('Hi :emoji[party-id](party) there'))
      .toBe('Hi :party: there');
  });

  it('renders a sized placeholder image while a custom emoji asset is still loading', () => {
    const markdown = replaceCustomEmojiMessageTokens(':emoji[party-id](party)', () => null);

    expect(markdown).toContain('![custom-emoji:party-id:party]');
    expect(markdown).toContain('data:image/gif;base64,');
    expect(markdown).not.toContain(':emoji[party-id](party)');
    expect(markdown).not.toContain(':party:');
  });

  it('keeps custom emoji ids available for right-click save actions', () => {
    const party = customEmoji('party-id', 'party');
    const markdown = replaceCustomEmojiMessageTokens(':emoji[party-id](party)', () => party);

    expect(markdown).toBe(`![custom-emoji:party-id:party](${party.dataUrl})`);
    expect(getCustomEmojiIdFromMarkdownAlt('custom-emoji:party-id:party')).toBe('party-id');
    expect(getCustomEmojiLabelFromMarkdownAlt('custom-emoji:party-id:party')).toBe('party');
  });

  it('allows small custom emoji payloads to travel inline in one data-channel event', () => {
    const party = customEmoji('party-id', 'party');

    expect(canInlineCustomEmojiTransfer(party)).toBe(true);
  });

  it('extracts custom emoji ids from messages for repair sync requests', () => {
    expect(extractCustomEmojiIds('Look :emoji[download](download) :emoji[party](party) :emoji[download](download)'))
      .toEqual(['download', 'party']);
  });

  it('returns all picker entries when the search query is blank', () => {
    expect(filterUnicodeEmojiPickerEntries(UNICODE_EMOJI_PICKER_ENTRIES, ''))
      .toEqual([...UNICODE_EMOJI_PICKER_ENTRIES]);

    expect(filterCustomEmojisForPicker([customEmoji('party-id', 'party'), customEmoji('wave-id', 'wave')], '   ')).toHaveLength(2);
  });

  it('filters unicode picker entries by emoji and search terms', () => {
    expect(filterUnicodeEmojiPickerEntries(UNICODE_EMOJI_PICKER_ENTRIES, 'heart').map((entry) => entry.emoji))
      .toEqual(['❤️']);

    expect(filterUnicodeEmojiPickerEntries(UNICODE_EMOJI_PICKER_ENTRIES, 'fire').map((entry) => entry.emoji))
      .toEqual(['🔥']);

    expect(filterUnicodeEmojiPickerEntries(UNICODE_EMOJI_PICKER_ENTRIES, '😂').map((entry) => entry.emoji))
      .toEqual(['😂']);
  });

  it('resolves add and remove custom emoji context menu targets from marked hosts', () => {
    const addHost = {
      closest: vi.fn((selector: string) => selector === `[${CUSTOM_EMOJI_CONTEXT_MENU_TARGET}]` ? addHost : null),
      getAttribute: vi.fn((name: string) => name === CUSTOM_EMOJI_ID_ATTRIBUTE ? 'party-id' : null)
    };
    const libraryHost = {
      closest: vi.fn((selector: string) => selector === `[${CUSTOM_EMOJI_LIBRARY_CONTEXT_MENU_TARGET}]` ? libraryHost : null),
      getAttribute: vi.fn((name: string) => name === CUSTOM_EMOJI_ID_ATTRIBUTE ? 'wave-id' : null)
    };

    expect(resolveCustomEmojiContextMenuTarget(addHost as unknown as EventTarget))
      .toEqual({ action: 'add',
        emojiId: 'party-id' });

    expect(resolveCustomEmojiContextMenuTarget(libraryHost as unknown as EventTarget))
      .toEqual({ action: 'remove',
        emojiId: 'wave-id' });
  });

  it('filters saved custom emoji by name in the picker search', () => {
    const emojis = [
      customEmoji('party-id', 'party-parrot'),
      customEmoji('wave-id', 'wave'),
      customEmoji('cat-id', 'cat-jam')
    ];

    expect(filterCustomEmojisForPicker(emojis, 'par').map((emoji) => emoji.id))
      .toEqual(['party-id']);

    expect(filterCustomEmojisForPicker(emojis, 'CAT').map((emoji) => emoji.id))
      .toEqual(['cat-id']);
  });

  it('splits chat text into plain and emoji segments for inline rendering', () => {
    expect(splitTextIntoEmojiSegments('Hello 🔥 world')).toEqual([
      { kind: 'text',
        value: 'Hello ' },
      { kind: 'emoji',
        value: '🔥' },
      { kind: 'text',
        value: ' world' }
    ]);

    expect(splitTextIntoEmojiSegments('❤️😂')).toEqual([
      { kind: 'emoji',
        value: '❤️' },
      { kind: 'emoji',
        value: '😂' }
    ]);

    expect(splitTextIntoEmojiSegments('plain text')).toEqual([
      { kind: 'text',
        value: 'plain text' }
    ]);
  });
});
