import { describe, expect, it, vi } from 'vitest'
import { createArrowLigaturesExtension } from './arrowLigaturesExtension'

function createFixture() {
  let beforeInputListener: ((event: InputEvent) => void) | null = null
  const transaction = { insertText: vi.fn(() => transaction) }
  const paragraphNode = { type: { name: 'paragraph', spec: {} } }
  const view = {
    dom: {
      isConnected: true,
    },
    dispatch: vi.fn(),
    state: {
      doc: {
        textBetween: vi.fn(),
      },
      selection: {
        from: 2,
        to: 2,
        $from: {
          depth: 0,
          node: vi.fn(() => paragraphNode),
        },
      },
      tr: transaction,
    },
  }
  const dom = {
    addEventListener: vi.fn((type: string, listener: (event: InputEvent) => void) => {
      if (type === 'beforeinput') {
        beforeInputListener = listener
      }
    }),
  }
  const editor = {
    _tiptapEditor: { view },
    prosemirrorView: view,
  }
  const extension = createArrowLigaturesExtension()({ editor: editor as never })

  return {
    dom,
    editor,
    extension,
    fireInput(event: Partial<InputEvent> = {}) {
      if (!beforeInputListener) {
        throw new Error('Arrow ligatures extension did not register a beforeinput listener')
      }

      const inputEvent = {
        data: '>',
        inputType: 'insertText',
        preventDefault: vi.fn(),
        ...event,
      }

      beforeInputListener(inputEvent as InputEvent)
      return inputEvent
    },
    mount() {
      const controller = new AbortController()
      extension.mount?.({
        dom: dom as never,
        root: document,
        signal: controller.signal,
      })
      return controller
    },
    transaction,
    view,
  }
}

describe('createArrowLigaturesExtension', () => {
  it('registers a beforeinput listener when the editor mounts', () => {
    const fixture = createFixture()

    fixture.mount()

    expect(fixture.dom.addEventListener).toHaveBeenCalledTimes(1)
    expect(fixture.dom.addEventListener).toHaveBeenCalledWith(
      'beforeinput',
      expect.any(Function),
      expect.objectContaining({
        capture: true,
        signal: expect.any(AbortSignal),
      }),
    )
  })

  it('replaces completed ASCII arrows through the mounted input listener', () => {
    const fixture = createFixture()
    fixture.mount()
    fixture.view.state.doc.textBetween.mockReturnValue('-')

    const event = fixture.fireInput()

    expect(event.preventDefault).toHaveBeenCalledTimes(1)
    expect(fixture.transaction.insertText).toHaveBeenCalledWith('→', 1, 2)
    expect(fixture.view.dispatch).toHaveBeenCalledWith(fixture.transaction)
  })

  it('preserves escaped <-> as literal ASCII on the next keystroke', () => {
    const fixture = createFixture()
    fixture.mount()
    fixture.view.state.selection = { from: 3, to: 3 }

    fixture.view.state.doc.textBetween.mockReturnValueOnce('\\<')
    fixture.fireInput({ data: '-' })

    fixture.view.state.doc.textBetween.mockReturnValueOnce('<-')
    const event = fixture.fireInput({ data: '>' })

    expect(event.preventDefault).not.toHaveBeenCalled()
    expect(fixture.transaction.insertText).toHaveBeenCalledTimes(1)
    expect(fixture.transaction.insertText).toHaveBeenCalledWith('<-', 1, 3)
  })

  it('ignores non-text input so paste stays literal', () => {
    const fixture = createFixture()
    fixture.mount()

    const event = fixture.fireInput({ inputType: 'insertFromPaste' })

    expect(event.preventDefault).not.toHaveBeenCalled()
    expect(fixture.transaction.insertText).not.toHaveBeenCalled()
    expect(fixture.view.dispatch).not.toHaveBeenCalled()
  })

  it('does not replace arrows while typing inside code blocks', () => {
    const fixture = createFixture()
    fixture.mount()
    fixture.view.state.doc.textBetween.mockReturnValue('-')
    fixture.view.state.selection.$from.node.mockReturnValue({
      type: { name: 'codeBlock', spec: { code: true } },
    })

    const event = fixture.fireInput()

    expect(event.preventDefault).not.toHaveBeenCalled()
    expect(fixture.transaction.insertText).not.toHaveBeenCalled()
    expect(fixture.view.dispatch).not.toHaveBeenCalled()
  })

  it('ignores composing input so IME text is not rewritten', () => {
    const fixture = createFixture()
    fixture.mount()

    const event = fixture.fireInput({ isComposing: true })

    expect(event.preventDefault).not.toHaveBeenCalled()
    expect(fixture.transaction.insertText).not.toHaveBeenCalled()
    expect(fixture.view.dispatch).not.toHaveBeenCalled()
  })

  it('falls through when a reload leaves the ProseMirror view stale during beforeinput', () => {
    const fixture = createFixture()
    fixture.mount()
    Object.defineProperty(fixture.view, 'state', {
      configurable: true,
      get: () => {
        throw new Error('stale editor view')
      },
    })

    expect(() => fixture.fireInput()).not.toThrow()
  })
})
