import { RichText } from '../src/index.js'

describe('RichText', () => {
  it('converts entities to facets correctly', () => {
    const rt = new RichText({
      text: 'test',
      entities: [
        {
          index: { start: 0, end: 1 },
          type: 'link',
          value: 'https://example.com',
        },
        {
          index: { start: 1, end: 2 },
          type: 'mention',
          value: 'did:plc:1234',
        },
        {
          index: { start: 2, end: 3 },
          type: 'other',
          value: 'willbedropped',
        },
      ],
    })
    expect(rt.facets).toEqual([
      {
        $type: 'app.bsky.richtext.facet',
        index: { byteStart: 0, byteEnd: 1 },
        features: [
          {
            $type: 'app.bsky.richtext.facet#link',
            uri: 'https://example.com',
          },
        ],
      },
      {
        $type: 'app.bsky.richtext.facet',
        index: { byteStart: 1, byteEnd: 2 },
        features: [
          {
            $type: 'app.bsky.richtext.facet#mention',
            did: 'did:plc:1234',
          },
        ],
      },
    ])
  })

  it('converts entity utf16 indices to facet utf8 indices', () => {
    const rt = new RichText({
      text: '👨‍👩‍👧‍👧👨‍👩‍👧‍👧👨‍👩‍👧‍👧',
      entities: [
        {
          index: { start: 0, end: 11 },
          type: 'link',
          value: 'https://example.com',
        },
        {
          index: { start: 11, end: 22 },
          type: 'mention',
          value: 'did:plc:1234',
        },
        {
          index: { start: 22, end: 33 },
          type: 'other',
          value: 'willbedropped',
        },
      ],
    })
    expect(rt.facets).toEqual([
      {
        $type: 'app.bsky.richtext.facet',
        index: { byteStart: 0, byteEnd: 25 },
        features: [
          {
            $type: 'app.bsky.richtext.facet#link',
            uri: 'https://example.com',
          },
        ],
      },
      {
        $type: 'app.bsky.richtext.facet',
        index: { byteStart: 25, byteEnd: 50 },
        features: [
          {
            $type: 'app.bsky.richtext.facet#mention',
            did: 'did:plc:1234',
          },
        ],
      },
    ])
  })

  it('calculates bytelength and grapheme length correctly', () => {
    {
      const rt = new RichText({ text: 'Hello!' })
      expect(rt.length).toBe(6)
      expect(rt.graphemeLength).toBe(6)
    }
    {
      const rt = new RichText({ text: '👨‍👩‍👧‍👧' })
      expect(rt.length).toBe(25)
      expect(rt.graphemeLength).toBe(1)
    }
    {
      const rt = new RichText({ text: '👨‍👩‍👧‍👧🔥 good!✅' })
      expect(rt.length).toBe(38)
      expect(rt.graphemeLength).toBe(9)
    }
  })
})

describe('RichText#insert', () => {
  const input = new RichText({
    text: 'hello world',
    facets: [
      { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },
    ],
  })

  it('correctly adjusts facets (scenario A - before)', () => {
    const output = input.clone().insert(0, 'test')
    expect(output.text).toEqual('testhello world')
    expect(output.facets?.[0].index.byteStart).toEqual(6)
    expect(output.facets?.[0].index.byteEnd).toEqual(11)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('llo w')
  })

  it('correctly adjusts facets (scenario B - inner)', () => {
    const output = input.clone().insert(4, 'test')
    expect(output.text).toEqual('helltesto world')
    expect(output.facets?.[0].index.byteStart).toEqual(2)
    expect(output.facets?.[0].index.byteEnd).toEqual(11)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('lltesto w')
  })

  it('correctly adjusts facets (scenario C - after)', () => {
    const output = input.clone().insert(8, 'test')
    expect(output.text).toEqual('hello wotestrld')
    expect(output.facets?.[0].index.byteStart).toEqual(2)
    expect(output.facets?.[0].index.byteEnd).toEqual(7)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('llo w')
  })
})

describe('RichText#insert w/fat unicode', () => {
  const input = new RichText({
    text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',
    facets: [
      { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },
      { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
      { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },
    ],
  })

  it('correctly adjusts facets (scenario A - before)', () => {
    const output = input.clone().insert(0, 'test')
    expect(output.text).toEqual('testone👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('one👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[1].index.byteStart,
        output.facets?.[1].index.byteEnd,
      ),
    ).toEqual('two👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[2].index.byteStart,
        output.facets?.[2].index.byteEnd,
      ),
    ).toEqual('three👨‍👩‍👧‍👧')
  })

  it('correctly adjusts facets (scenario B - inner)', () => {
    const output = input.clone().insert(3, 'test')
    expect(output.text).toEqual('onetest👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('onetest👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[1].index.byteStart,
        output.facets?.[1].index.byteEnd,
      ),
    ).toEqual('two👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[2].index.byteStart,
        output.facets?.[2].index.byteEnd,
      ),
    ).toEqual('three👨‍👩‍👧‍👧')
  })

  it('correctly adjusts facets (scenario C - after)', () => {
    const output = input.clone().insert(28, 'test')
    expect(output.text).toEqual('one👨‍👩‍👧‍👧test two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('one👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[1].index.byteStart,
        output.facets?.[1].index.byteEnd,
      ),
    ).toEqual('two👨‍👩‍👧‍👧')
    expect(
      output.unicodeText.slice(
        output.facets?.[2].index.byteStart,
        output.facets?.[2].index.byteEnd,
      ),
    ).toEqual('three👨‍👩‍👧‍👧')
  })
})

describe('RichText#delete', () => {
  const input = new RichText({
    text: 'hello world',
    facets: [
      { index: { byteStart: 2, byteEnd: 7 }, features: [{ $type: '' }] },
    ],
  })

  it('correctly adjusts facets (scenario A - entirely outer)', () => {
    const output = input.clone().delete(0, 9)
    expect(output.text).toEqual('ld')
    expect(output.facets?.length).toEqual(0)
  })

  it('correctly adjusts facets (scenario B - entirely after)', () => {
    const output = input.clone().delete(7, 11)
    expect(output.text).toEqual('hello w')
    expect(output.facets?.[0].index.byteStart).toEqual(2)
    expect(output.facets?.[0].index.byteEnd).toEqual(7)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('llo w')
  })

  it('correctly adjusts facets (scenario C - partially after)', () => {
    const output = input.clone().delete(4, 11)
    expect(output.text).toEqual('hell')
    expect(output.facets?.[0].index.byteStart).toEqual(2)
    expect(output.facets?.[0].index.byteEnd).toEqual(4)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('ll')
  })

  it('correctly adjusts facets (scenario D - entirely inner)', () => {
    const output = input.clone().delete(3, 5)
    expect(output.text).toEqual('hel world')
    expect(output.facets?.[0].index.byteStart).toEqual(2)
    expect(output.facets?.[0].index.byteEnd).toEqual(5)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('l w')
  })

  it('correctly adjusts facets (scenario E - partially before)', () => {
    const output = input.clone().delete(1, 5)
    expect(output.text).toEqual('h world')
    expect(output.facets?.[0].index.byteStart).toEqual(1)
    expect(output.facets?.[0].index.byteEnd).toEqual(3)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual(' w')
  })

  it('correctly adjusts facets (scenario F - entirely before)', () => {
    const output = input.clone().delete(0, 2)
    expect(output.text).toEqual('llo world')
    expect(output.facets?.[0].index.byteStart).toEqual(0)
    expect(output.facets?.[0].index.byteEnd).toEqual(5)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('llo w')
  })
})

describe('RichText#delete w/fat unicode', () => {
  const input = new RichText({
    text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',
    facets: [
      { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
    ],
  })

  it('correctly adjusts facets (scenario A - entirely outer)', () => {
    const output = input.clone().delete(28, 58)
    expect(output.text).toEqual('one👨‍👩‍👧‍👧three👨‍👩‍👧‍👧')
    expect(output.facets?.length).toEqual(0)
  })

  it('correctly adjusts facets (scenario B - entirely after)', () => {
    const output = input.clone().delete(57, 88)
    expect(output.text).toEqual('one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧')
    expect(output.facets?.[0].index.byteStart).toEqual(29)
    expect(output.facets?.[0].index.byteEnd).toEqual(57)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('two👨‍👩‍👧‍👧')
  })

  it('correctly adjusts facets (scenario C - partially after)', () => {
    const output = input.clone().delete(31, 88)
    expect(output.text).toEqual('one👨‍👩‍👧‍👧 tw')
    expect(output.facets?.[0].index.byteStart).toEqual(29)
    expect(output.facets?.[0].index.byteEnd).toEqual(31)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('tw')
  })

  it('correctly adjusts facets (scenario D - entirely inner)', () => {
    const output = input.clone().delete(30, 32)
    expect(output.text).toEqual('one👨‍👩‍👧‍👧 t👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(output.facets?.[0].index.byteStart).toEqual(29)
    expect(output.facets?.[0].index.byteEnd).toEqual(55)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('t👨‍👩‍👧‍👧')
  })

  it('correctly adjusts facets (scenario E - partially before)', () => {
    const output = input.clone().delete(28, 31)
    expect(output.text).toEqual('one👨‍👩‍👧‍👧o👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(output.facets?.[0].index.byteStart).toEqual(28)
    expect(output.facets?.[0].index.byteEnd).toEqual(54)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('o👨‍👩‍👧‍👧')
  })

  it('correctly adjusts facets (scenario F - entirely before)', () => {
    const output = input.clone().delete(0, 2)
    expect(output.text).toEqual('e👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧')
    expect(output.facets?.[0].index.byteStart).toEqual(27)
    expect(output.facets?.[0].index.byteEnd).toEqual(55)
    expect(
      output.unicodeText.slice(
        output.facets?.[0].index.byteStart,
        output.facets?.[0].index.byteEnd,
      ),
    ).toEqual('two👨‍👩‍👧‍👧')
  })
})

describe('RichText#segments', () => {
  it('produces an empty output for an empty input', () => {
    const input = new RichText({ text: '' })
    expect(Array.from(input.segments())).toEqual([{ text: '' }])
  })

  it('produces a single segment when no facets are present', () => {
    const input = new RichText({ text: 'hello' })
    expect(Array.from(input.segments())).toEqual([{ text: 'hello' }])
  })

  it('produces 3 segments with 1 entity in the middle', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      { text: 'one ' },
      {
        text: 'two',
        facet: {
          index: { byteStart: 4, byteEnd: 7 },
          features: [{ $type: '' }],
        },
      },
      { text: ' three' },
    ])
  })

  it('produces 2 segments with 1 entity in the byteStart', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 0, byteEnd: 7 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one two',
        facet: {
          index: { byteStart: 0, byteEnd: 7 },
          features: [{ $type: '' }],
        },
      },
      { text: ' three' },
    ])
  })

  it('produces 2 segments with 1 entity in the end', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 4, byteEnd: 13 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      { text: 'one ' },
      {
        text: 'two three',
        facet: {
          index: { byteStart: 4, byteEnd: 13 },
          features: [{ $type: '' }],
        },
      },
    ])
  })

  it('produces 1 segments with 1 entity around the entire string', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 0, byteEnd: 13 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one two three',
        facet: {
          index: { byteStart: 0, byteEnd: 13 },
          features: [{ $type: '' }],
        },
      },
    ])
  })

  it('produces 5 segments with 3 facets covering each word', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
        { index: { byteStart: 4, byteEnd: 7 }, features: [{ $type: '' }] },
        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one',
        facet: {
          index: { byteStart: 0, byteEnd: 3 },
          features: [{ $type: '' }],
        },
      },
      { text: ' ' },
      {
        text: 'two',
        facet: {
          index: { byteStart: 4, byteEnd: 7 },
          features: [{ $type: '' }],
        },
      },
      { text: ' ' },
      {
        text: 'three',
        facet: {
          index: { byteStart: 8, byteEnd: 13 },
          features: [{ $type: '' }],
        },
      },
    ])
  })

  it('uses utf8 indices', () => {
    const input = new RichText({
      text: 'one👨‍👩‍👧‍👧 two👨‍👩‍👧‍👧 three👨‍👩‍👧‍👧',
      facets: [
        { index: { byteStart: 0, byteEnd: 28 }, features: [{ $type: '' }] },
        { index: { byteStart: 29, byteEnd: 57 }, features: [{ $type: '' }] },
        { index: { byteStart: 58, byteEnd: 88 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one👨‍👩‍👧‍👧',
        facet: {
          index: { byteStart: 0, byteEnd: 28 },
          features: [{ $type: '' }],
        },
      },
      { text: ' ' },
      {
        text: 'two👨‍👩‍👧‍👧',
        facet: {
          index: { byteStart: 29, byteEnd: 57 },
          features: [{ $type: '' }],
        },
      },
      { text: ' ' },
      {
        text: 'three👨‍👩‍👧‍👧',
        facet: {
          index: { byteStart: 58, byteEnd: 88 },
          features: [{ $type: '' }],
        },
      },
    ])
  })

  it('correctly identifies mentions and links', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        {
          index: { byteStart: 0, byteEnd: 3 },
          features: [
            {
              $type: 'app.bsky.richtext.facet#mention',
              did: 'did:plc:123',
            },
          ],
        },
        {
          index: { byteStart: 4, byteEnd: 7 },
          features: [
            {
              $type: 'app.bsky.richtext.facet#link',
              uri: 'https://example.com',
            },
          ],
        },
        {
          index: { byteStart: 8, byteEnd: 13 },
          features: [{ $type: 'other' }],
        },
      ],
    })
    const segments = Array.from(input.segments())
    expect(segments[0].isLink()).toBe(false)
    expect(segments[0].isMention()).toBe(true)
    expect(segments[1].isLink()).toBe(false)
    expect(segments[1].isMention()).toBe(false)
    expect(segments[2].isLink()).toBe(true)
    expect(segments[2].isMention()).toBe(false)
    expect(segments[3].isLink()).toBe(false)
    expect(segments[3].isMention()).toBe(false)
    expect(segments[4].isLink()).toBe(false)
    expect(segments[4].isMention()).toBe(false)
  })

  it('skips facets that incorrectly overlap (left edge)', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
        { index: { byteStart: 2, byteEnd: 9 }, features: [{ $type: '' }] },
        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one',
        facet: {
          index: { byteStart: 0, byteEnd: 3 },
          features: [{ $type: '' }],
        },
      },
      {
        text: ' two ',
      },
      {
        text: 'three',
        facet: {
          index: { byteStart: 8, byteEnd: 13 },
          features: [{ $type: '' }],
        },
      },
    ])
  })

  it('skips facets that incorrectly overlap (right edge)', () => {
    const input = new RichText({
      text: 'one two three',
      facets: [
        { index: { byteStart: 0, byteEnd: 3 }, features: [{ $type: '' }] },
        { index: { byteStart: 4, byteEnd: 9 }, features: [{ $type: '' }] },
        { index: { byteStart: 8, byteEnd: 13 }, features: [{ $type: '' }] },
      ],
    })
    expect(Array.from(input.segments())).toEqual([
      {
        text: 'one',
        facet: {
          index: { byteStart: 0, byteEnd: 3 },
          features: [{ $type: '' }],
        },
      },
      { text: ' ' },
      {
        text: 'two t',
        facet: {
          index: { byteStart: 4, byteEnd: 9 },
          features: [{ $type: '' }],
        },
      },
      {
        text: 'hree',
      },
    ])
  })

  it("doesn't duplicate text when negative-length facets are present", () => {
    const input = {
      text: 'hello world',
      facets: [
        // invalid zero-length
        {
          features: [],
          index: {
            byteStart: 6,
            byteEnd: 0,
          },
        },
        // valid normal facet
        {
          features: [],
          index: {
            byteEnd: 11,
            byteStart: 6,
          },
        },
        // valid zero-length
        {
          features: [],
          index: {
            byteEnd: 0,
            byteStart: 0,
          },
        },
      ],
    }

    const rt = new RichText(input)

    let output = ''
    for (const segment of rt.segments()) {
      output += segment.text
    }

    expect(output).toEqual(input.text)

    // invalid one should have been removed
    expect(rt.facets?.length).toEqual(2)
  })
})
