import { describe, it, expect } from 'vitest';
import {
  allowTrustedOrPairedSender,
  buildResetInboundMessage,
  isAllowedByAllowlist,
  normalizeResetCommandText,
  shouldIgnoreForMissingMention,
  splitMessage,
} from './utils.js';
import { PairingManager } from './pairing.js';

describe('splitMessage', () => {
  it('returns single chunk for empty string', () => {
    const result = splitMessage('', 100);
    // empty string never enters the while loop → returns empty array
    expect(result).toEqual([]);
  });

  it('returns single chunk when text is under maxLength', () => {
    const result = splitMessage('hello world', 100);
    expect(result).toEqual(['hello world']);
  });

  it('returns single chunk when text equals maxLength', () => {
    const text = 'a'.repeat(50);
    const result = splitMessage(text, 50);
    expect(result).toEqual([text]);
  });

  it('splits at newline when possible', () => {
    const text = 'line one\nline two\nline three';
    // maxLength 18 → "line one\nline two\n" is 18 chars, lastIndexOf('\n', 18) = 17
    const result = splitMessage(text, 18);
    expect(result).toEqual(['line one\nline two', 'line three']);
  });

  it('splits at space when no newline available', () => {
    const text = 'word1 word2 word3 word4';
    // maxLength 12 → "word1 word2 " lastIndexOf(' ', 12) = 11
    const result = splitMessage(text, 12);
    expect(result[0]).toBe('word1 word2');
    expect(result.length).toBeGreaterThanOrEqual(2);
  });

  it('hard-cuts when no whitespace available', () => {
    const text = 'abcdefghijklmnop';
    const result = splitMessage(text, 5);
    expect(result[0]).toBe('abcde');
    expect(result[1]).toBe('fghij');
    expect(result[2]).toBe('klmno');
    expect(result[3]).toBe('p');
  });

  it('produces multiple chunks for long text', () => {
    const text = 'chunk one\nchunk two\nchunk three\nchunk four';
    const result = splitMessage(text, 20);
    expect(result.length).toBeGreaterThan(1);
    // Every chunk respects the limit
    for (const chunk of result) {
      expect(chunk.length).toBeLessThanOrEqual(20);
    }
  });

  it('preserves all content (joined chunks equal original minus trimmed whitespace)', () => {
    const text = 'The quick brown fox jumps over the lazy dog. ' +
      'Pack my box with five dozen liquor jugs. ' +
      'How vexingly quick daft zebras jump.';
    const result = splitMessage(text, 30);

    // Reassemble: since trimStart() removes leading whitespace between chunks,
    // we verify all words are preserved
    const originalWords = text.split(/\s+/);
    const resultWords = result.join(' ').split(/\s+/);
    expect(resultWords).toEqual(originalWords);
  });

  it('prefers newline split over space split', () => {
    // Place newline at a good position and space later
    const text = 'first part\nsecond part of the message';
    // maxLength 15: lastIndexOf('\n', 15) = 10, which is >= 15/2 = 7.5 → splits at newline
    const result = splitMessage(text, 15);
    expect(result[0]).toBe('first part');
  });

  it('falls back to space when newline is too early', () => {
    // Newline at position 2, which is < maxLength/2 for maxLength=14
    const text = 'ab\ncdefghij klmnopqrst';
    // lastIndexOf('\n', 14) = 2, but 2 < 14/2=7 → falls back to space
    // lastIndexOf(' ', 14) = 11, which is >= 7 → splits at space
    const result = splitMessage(text, 14);
    expect(result[0]).toBe('ab\ncdefghij');
    expect(result[1]).toBe('klmnopqrst');
  });

  it('splits on sentence boundaries when available', () => {
    const text = 'First sentence is here. Second sentence follows with more detail.';
    const result = splitMessage(text, 36);
    expect(result[0]).toBe('First sentence is here.');
    expect(result.length).toBeGreaterThan(1);
  });

  it('balances markdown code fences across chunks', () => {
    const text = '```ts\\nconst value = 1;\\nconsole.log(value);\\n```\\nAfter code.';
    const result = splitMessage(text, 22);
    expect(result.length).toBeGreaterThan(1);
    for (const chunk of result) {
      const fenceCount = (chunk.match(/```/g) ?? []).length;
      expect(fenceCount % 2).toBe(0);
    }
  });
});

describe('normalizeResetCommandText', () => {
  it('normalizes reset to !reset', () => {
    expect(normalizeResetCommandText('reset')).toBe('!reset');
  });

  it('keeps !reset unchanged', () => {
    expect(normalizeResetCommandText('!reset')).toBe('!reset');
  });

  it('does not change non-command text', () => {
    expect(normalizeResetCommandText('hello')).toBe('hello');
  });
});

describe('buildResetInboundMessage', () => {
  it('builds canonical reset command metadata and text', () => {
    const message = buildResetInboundMessage({
      id: 'id-1',
      channel: 'slack',
      senderId: 'C1:T1',
      senderName: 'Alice',
      timestamp: 123,
    });

    expect(message).toEqual({
      id: 'id-1',
      channel: 'slack',
      senderId: 'C1:T1',
      senderName: 'Alice',
      text: '!reset',
      timestamp: 123,
      metadata: { isCommand: true, command: 'reset' },
    });
  });

  it('includes attachments only when provided', () => {
    const message = buildResetInboundMessage({
      id: 'id-2',
      channel: 'whatsapp',
      senderId: '123@c.us',
      attachments: [{ mimeType: 'image/png', url: 'https://example.com/a.png' }],
    });

    expect(message.attachments).toHaveLength(1);
    expect(message.text).toBe('!reset');
    expect(message.metadata).toEqual({ isCommand: true, command: 'reset' });
  });
});

describe('isAllowedByAllowlist', () => {
  it('allows all values when allowlist is missing', () => {
    expect(isAllowedByAllowlist('C123')).toBe(true);
  });

  it('allows all values when allowlist is empty', () => {
    expect(isAllowedByAllowlist('C123', [])).toBe(true);
  });

  it('filters values when allowlist is present', () => {
    expect(isAllowedByAllowlist('C123', ['C123', 'C456'])).toBe(true);
    expect(isAllowedByAllowlist('C999', ['C123', 'C456'])).toBe(false);
  });
});

describe('shouldIgnoreForMissingMention', () => {
  it('ignores when mention is required and missing', () => {
    expect(shouldIgnoreForMissingMention({
      requireMention: true,
      defaultRequireMention: false,
      mentionsBot: false,
    })).toBe(true);
  });

  it('does not ignore when mention is required and present', () => {
    expect(shouldIgnoreForMissingMention({
      requireMention: true,
      defaultRequireMention: false,
      mentionsBot: true,
    })).toBe(false);
  });

  it('uses default when requireMention is undefined', () => {
    expect(shouldIgnoreForMissingMention({
      requireMention: undefined,
      defaultRequireMention: true,
      mentionsBot: false,
    })).toBe(true);
  });
});

describe('allowTrustedOrPairedSender', () => {
  it('allows trusted senders immediately', async () => {
    const pm = new PairingManager({ enabled: true, codeTtl: 60_000, codeLength: 6 });
    await expect(allowTrustedOrPairedSender({
      pairingManager: pm,
      channel: 'slack',
      senderId: 'U1',
      text: 'hello',
      isTrusted: true,
    })).resolves.toBe(true);
  });

  it('blocks untrusted senders when pairing is disabled', async () => {
    const pm = new PairingManager({ enabled: false, codeTtl: 60_000, codeLength: 6 });
    await expect(allowTrustedOrPairedSender({
      pairingManager: pm,
      channel: 'slack',
      senderId: 'U1',
      text: 'hello',
      isTrusted: false,
    })).resolves.toBe(false);
  });

  it('consumes valid pairing code and runs onPaired callback', async () => {
    const pm = new PairingManager({ enabled: true, codeTtl: 60_000, codeLength: 6 });
    const code = pm.generateCode('test');
    let paired = false;
    await expect(allowTrustedOrPairedSender({
      pairingManager: pm,
      channel: 'slack',
      senderId: 'U1',
      text: code,
      isTrusted: false,
      onPaired: () => { paired = true; },
    })).resolves.toBe(false);
    expect(paired).toBe(true);
    expect(pm.isApproved('slack', 'U1')).toBe(true);
  });
});
