import { describe, expect, test } from 'bun:test';

import { decodeAttributedBody } from '../decode';

/**
 * Mirror of the iMessage Mac client's AttributedBodyExtractionTests fixtures.
 * The TS decoder is a faithful port of the Swift heuristic, so both sides
 * must agree on the same inputs.
 */

const NS_STRING_MARKER = [0x4e, 0x53, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67];
// Mac client's 5-byte padding between marker and length byte. The bytes
// themselves are arbitrary — only the offset matters.
const PADDING = [0x01, 0x95, 0x84, 0x01, 0x2b];

function fixtureShortForm(text: string): string {
  const textBytes = Buffer.from(text, 'utf-8');
  if (textBytes.length > 0x80) {
    throw new Error('short-form length must be < 128; use fixtureLongForm');
  }
  const buf = Buffer.concat([
    Buffer.from(NS_STRING_MARKER),
    Buffer.from(PADDING),
    Buffer.from([textBytes.length]),
    textBytes,
  ]);
  return buf.toString('base64');
}

function fixtureLongForm(text: string): string {
  const textBytes = Buffer.from(text, 'utf-8');
  const len = textBytes.length;
  const buf = Buffer.concat([
    Buffer.from(NS_STRING_MARKER),
    Buffer.from(PADDING),
    Buffer.from([0x81, len & 0xff, (len >> 8) & 0xff]),
    textBytes,
  ]);
  return buf.toString('base64');
}

describe('decodeAttributedBody', () => {
  test('empty input returns null', () => {
    expect(decodeAttributedBody('')).toEqual({ text: null });
  });

  test('invalid base64 falls through to fallback (no throw)', () => {
    expect(() => decodeAttributedBody('!!!!not-base64!!!!')).not.toThrow();
  });

  test('NSString short-form: extracts simple ASCII', () => {
    const result = decodeAttributedBody(fixtureShortForm('hello world'));
    expect(result.text).toBe('hello world');
  });

  test('NSString long-form (0x81 + uint16 LE) extracts 200-char payload', () => {
    const payload = 'a'.repeat(200);
    const result = decodeAttributedBody(fixtureLongForm(payload));
    expect(result.text).toBe(payload);
  });

  test('NSString with multi-byte UTF-8 (emoji)', () => {
    const result = decodeAttributedBody(fixtureShortForm('café 👋'));
    expect(result.text).toBe('café 👋');
  });

  test('truncated NSString marker payload does not crash and may fall back', () => {
    // Marker + padding + length=100 but only 10 actual bytes
    const truncated = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([100]),
      Buffer.from('shortrun!!', 'utf-8'),
    ]).toString('base64');
    const result = decodeAttributedBody(truncated);
    // The NSString path returns null due to truncation; the fallback may
    // still recover the visible "shortrun" run.
    if (result.text !== null) {
      expect(result.text).toContain('shortrun');
    }
  });

  test('no marker: fallback returns longest printable run with letters', () => {
    const raw = Buffer.concat([
      Buffer.from([0x00, 0x00, 0x00]),
      Buffer.from('greetings', 'utf-8'),
      Buffer.from([0x00, 0x00]),
      Buffer.from('hi', 'utf-8'),
    ]).toString('base64');
    const result = decodeAttributedBody(raw);
    expect(result.text).toBe('greetings');
  });

  test('binary-only input with no letters returns null', () => {
    const binary = Buffer.from([0x00, 0x01, 0x02, 0x03, 0xff, 0xfe]).toString('base64');
    const result = decodeAttributedBody(binary);
    expect(result.text).toBeNull();
  });

  test('NSString with empty length returns null (not empty string)', () => {
    const empty = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([0]),
    ]).toString('base64');
    const result = decodeAttributedBody(empty);
    expect(result.text).toBeNull();
  });

  test('NSString with whitespace-only payload returns null after trim', () => {
    const result = decodeAttributedBody(fixtureShortForm('   \n\t  '));
    expect(result.text).toBeNull();
  });

  test('fallback is preferred only when marker path yields nothing', () => {
    // Marker path returns "real", fallback would return "junkjunk".
    // Marker wins.
    const buf = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([4]),
      Buffer.from('real', 'utf-8'),
      Buffer.from([0x00]),
      Buffer.from('junkjunk', 'utf-8'),
    ]);
    const result = decodeAttributedBody(buf.toString('base64'));
    expect(result.text).toBe('real');
  });

  test('0x82 long-form (uint32 LE) extracts >64KiB payload', () => {
    const len = 70_000;
    const payload = 'x'.repeat(len);
    const buf = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([0x82, len & 0xff, (len >>> 8) & 0xff, (len >>> 16) & 0xff, (len >>> 24) & 0xff]),
      Buffer.from(payload, 'utf-8'),
    ]).toString('base64');
    const result = decodeAttributedBody(buf);
    expect(result.text).toBe(payload);
  });

  test('multiple NSString markers: longest valid extraction wins', () => {
    // First marker yields "hi", second yields "the real conversation text".
    // The decoder must scan past the first and pick the longer.
    const shortRun = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([2]),
      Buffer.from('hi', 'utf-8'),
    ]);
    const longText = 'the real conversation text';
    const longRun = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([longText.length]),
      Buffer.from(longText, 'utf-8'),
    ]);
    const buf = Buffer.concat([shortRun, Buffer.from([0x00, 0x00]), longRun]).toString('base64');
    expect(decodeAttributedBody(buf).text).toBe(longText);
  });

  test('truncated first marker does not block recovery from a valid second marker', () => {
    // First marker claims length=200 but only has 5 bytes of payload — invalid.
    // Second marker is well-formed. Decoder should still return the second.
    const broken = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([200]),
      Buffer.from('short', 'utf-8'),
    ]);
    const good = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from(PADDING),
      Buffer.from([7]),
      Buffer.from('recover', 'utf-8'),
    ]);
    const buf = Buffer.concat([broken, good]).toString('base64');
    expect(decodeAttributedBody(buf).text).toBe('recover');
  });

  test('U+FFFC object-replacement (NSTextAttachment placeholder) passes through', () => {
    // iMessage messages with inline attachments substitute U+FFFC into the
    // backing string. The decoder treats it as ordinary content.
    const text = 'photo ￼ caption';
    const result = decodeAttributedBody(fixtureShortForm(text));
    expect(result.text).toBe(text);
  });

  test('marker-word fallback filter blocks "NSString" leaking into text', () => {
    // No valid length-prefixed run, but the literal "NSString" appears as a
    // printable byte sequence. Fallback must filter it out and try the next
    // longest run.
    const buf = Buffer.concat([
      Buffer.from(NS_STRING_MARKER),
      Buffer.from([0x00, 0x00, 0x00]),
      Buffer.from('greetings everyone', 'utf-8'),
    ]).toString('base64');
    expect(decodeAttributedBody(buf).text).toBe('greetings everyone');
  });
});
