import { describe, expect, it } from 'vitest';

import {
  findChannelForDeliveryRole,
  neutralizeStrayBackticks,
  neutralizeStrayMarkdown,
  resolveChannelForDeliveryRole,
} from './router.js';
import { type Channel } from './types.js';

function createChannel(name: string, ownedJids: string[]): Channel {
  return {
    name,
    connect: async () => {},
    sendMessage: async () => {},
    isConnected: () => true,
    ownsJid: (jid) => ownedJids.includes(jid),
    disconnect: async () => {},
  };
}

describe('findChannelForDeliveryRole', () => {
  const jid = 'dc:123';

  it('prefers the reviewer channel for reviewer IPC messages', () => {
    const ownerChannel = createChannel('discord-main', [jid]);
    const reviewerChannel = createChannel('discord-review', [jid]);

    expect(
      findChannelForDeliveryRole(
        [ownerChannel, reviewerChannel],
        jid,
        'reviewer',
      ),
    ).toBe(reviewerChannel);
  });

  it('prefers the arbiter channel for arbiter IPC messages', () => {
    const ownerChannel = createChannel('discord-main', [jid]);
    const reviewerChannel = createChannel('discord-review', [jid]);
    const arbiterChannel = createChannel('discord-arbiter', [jid]);

    expect(
      findChannelForDeliveryRole(
        [ownerChannel, reviewerChannel, arbiterChannel],
        jid,
        'arbiter',
      ),
    ).toBe(arbiterChannel);
  });

  it('falls back to the first JID-owning channel when no role channel exists', () => {
    const ownerChannel = createChannel('discord-main', [jid]);

    expect(findChannelForDeliveryRole([ownerChannel], jid, 'reviewer')).toBe(
      ownerChannel,
    );
  });

  it('reports fallback metadata when a role-specific channel is unavailable', () => {
    const ownerChannel = createChannel('discord-main', [jid]);

    expect(
      resolveChannelForDeliveryRole([ownerChannel], jid, 'reviewer'),
    ).toEqual(
      expect.objectContaining({
        channel: ownerChannel,
        requestedRoleChannelName: 'discord-review',
        selectedChannelName: 'discord-main',
        usedRoleChannel: false,
        fallbackUsed: true,
      }),
    );
  });

  it('reports direct role routing metadata when the role channel exists', () => {
    const ownerChannel = createChannel('discord-main', [jid]);
    const reviewerChannel = createChannel('discord-review', [jid]);

    expect(
      resolveChannelForDeliveryRole(
        [ownerChannel, reviewerChannel],
        jid,
        'reviewer',
      ),
    ).toEqual(
      expect.objectContaining({
        channel: reviewerChannel,
        requestedRoleChannelName: 'discord-review',
        selectedChannelName: 'discord-review',
        usedRoleChannel: true,
        fallbackUsed: false,
      }),
    );
  });
});

describe('neutralizeStrayMarkdown', () => {
  it('escapes inline backticks so they render literally', () => {
    expect(neutralizeStrayMarkdown('use `foo` and `bar` here')).toBe(
      'use \\`foo\\` and \\`bar\\` here',
    );
  });

  it('preserves well-formed fenced code blocks and escapes outside prose', () => {
    const input = 'before\n```ts\nconst x = `tpl`;\n```\nafter `inline`';
    expect(neutralizeStrayMarkdown(input)).toBe(
      'before\n```ts\nconst x = `tpl`;\n```\nafter \\`inline\\`',
    );
  });

  it('escapes emphasis in an unterminated fence (treated as prose)', () => {
    expect(neutralizeStrayMarkdown('oops ```code without close')).toBe(
      'oops \\`\\`\\`code without close',
    );
  });

  it('passes through plain text unchanged', () => {
    const s = 'plain text with no markup';
    expect(neutralizeStrayMarkdown(s)).toBe(s);
  });

  it('escapes italic asterisks', () => {
    expect(neutralizeStrayMarkdown('이건 *기울임* 처리')).toBe(
      '이건 \\*기울임\\* 처리',
    );
  });

  it('escapes bold asterisks', () => {
    expect(neutralizeStrayMarkdown('이건 **강조** 처리')).toBe(
      '이건 \\*\\*강조\\*\\* 처리',
    );
  });

  it('escapes bold-italic triple asterisks', () => {
    expect(neutralizeStrayMarkdown('***hybrid***')).toBe(
      '\\*\\*\\*hybrid\\*\\*\\*',
    );
  });

  it('escapes italic underscores at word boundaries', () => {
    expect(neutralizeStrayMarkdown('이건 _italic_ 처리')).toBe(
      '이건 \\_italic\\_ 처리',
    );
  });

  it('escapes every underscore including snake_case identifiers', () => {
    expect(neutralizeStrayMarkdown('use foo_bar_baz here')).toBe(
      'use foo\\_bar\\_baz here',
    );
  });

  it('escapes underscores in absolute paths', () => {
    expect(
      neutralizeStrayMarkdown(
        '경로: /home/claude/EJClaw/groups/mc_the_scene_plugin/src/main_file.ts',
      ),
    ).toBe(
      '경로: /home/claude/EJClaw/groups/mc\\_the\\_scene\\_plugin/src/main\\_file.ts',
    );
  });

  it('escapes underscores and backslashes in Windows paths', () => {
    expect(
      neutralizeStrayMarkdown(
        '경로: C:\\Custom_vscode\\minecraft_launcher\\src\\main_file.ts',
      ),
    ).toBe(
      '경로: C:\\\\Custom\\_vscode\\\\minecraft\\_launcher\\\\src\\\\main\\_file.ts',
    );
  });

  it('escapes double-underscore underline markers', () => {
    expect(neutralizeStrayMarkdown('__bold__ text')).toBe(
      '\\_\\_bold\\_\\_ text',
    );
  });

  it('escapes heading hashes at line start', () => {
    expect(neutralizeStrayMarkdown('## 빌드 타임 메타\n본문')).toBe(
      '\\## 빌드 타임 메타\n본문',
    );
  });

  it('escapes block-quote markers at line start', () => {
    expect(neutralizeStrayMarkdown('> quoted line\n> another')).toBe(
      '\\> quoted line\n\\> another',
    );
  });

  it('leaves mid-line hash and gt unchanged (not heading/quote markers)', () => {
    expect(neutralizeStrayMarkdown('a #tag here and a > b')).toBe(
      'a #tag here and a > b',
    );
  });

  it('escapes strikethrough tildes', () => {
    expect(neutralizeStrayMarkdown('이건 ~~취소선~~ 입니다')).toBe(
      '이건 \\~\\~취소선\\~\\~ 입니다',
    );
  });

  it('escapes spoiler pipes', () => {
    expect(neutralizeStrayMarkdown('||비밀||')).toBe('\\|\\|비밀\\|\\|');
  });

  it('preserves Discord mention syntax (no markdown delimiters inside)', () => {
    expect(neutralizeStrayMarkdown('<@123> hi <#456>')).toBe(
      '<@123> hi <#456>',
    );
  });

  it('preserves code fence content while escaping prose emphasis', () => {
    const input = '**bold** outside\n```py\nx = *2\n```\nand *more*';
    expect(neutralizeStrayMarkdown(input)).toBe(
      '\\*\\*bold\\*\\* outside\n```py\nx = *2\n```\nand \\*more\\*',
    );
  });

  it('escapes literal backslashes first so new escapes are not doubled', () => {
    expect(neutralizeStrayMarkdown('a\\b *c*')).toBe('a\\\\b \\*c\\*');
  });

  it('back-compat alias still exported', () => {
    expect(neutralizeStrayBackticks('*x*')).toBe('\\*x\\*');
  });
});
