import type { LexicalEditor } from 'lexical'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { act, render, waitFor } from '@testing-library/react'
import { $getRoot } from 'lexical'
import { CustomTextNode } from '../custom-text/node'
import UpdateBlock, {
  PROMPT_EDITOR_INSERT_QUICKLY,
  PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
} from '../update-block'
import { CaptureEditorPlugin } from './test-utils'

const { mockUseEventEmitterContextContext } = vi.hoisted(() => ({
  mockUseEventEmitterContextContext: vi.fn(),
}))

vi.mock('@/context/event-emitter', () => ({
  useEventEmitterContextContext: () => mockUseEventEmitterContextContext(),
}))

type TestEvent = {
  type: string
  instanceId?: string
  payload?: string
}

const readEditorText = (editor: LexicalEditor) => {
  let content = ''

  editor.getEditorState().read(() => {
    content = $getRoot().getTextContent()
  })

  return content
}

const selectRootEnd = (editor: LexicalEditor) => {
  act(() => {
    editor.update(() => {
      $getRoot().selectEnd()
    })
  })
}

const setup = (props?: { instanceId?: string; withEventEmitter?: boolean }) => {
  const callbacks: Array<(event: TestEvent) => void> = []

  const eventEmitter =
    props?.withEventEmitter === false
      ? null
      : {
          useSubscription: vi.fn((callback: (event: TestEvent) => void) => {
            callbacks.push(callback)
          }),
        }

  mockUseEventEmitterContextContext.mockReturnValue({ eventEmitter })

  let editor: LexicalEditor | null = null
  const onReady = (value: LexicalEditor) => {
    editor = value
  }

  render(
    <LexicalComposer
      initialConfig={{
        namespace: 'update-block-plugin-test',
        onError: (error: Error) => {
          throw error
        },
        nodes: [CustomTextNode],
      }}
    >
      <UpdateBlock instanceId={props?.instanceId} />
      <CaptureEditorPlugin onReady={onReady} />
    </LexicalComposer>,
  )

  const emit = (event: TestEvent) => {
    act(() => {
      callbacks.forEach((callback) => callback(event))
    })
  }

  return {
    callbacks,
    emit,
    eventEmitter,
    getEditor: () => editor,
  }
}

describe('UpdateBlock', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  describe('Subscription setup', () => {
    it('should register two subscriptions when event emitter is available', () => {
      const { callbacks, eventEmitter } = setup({ instanceId: 'instance-1' })

      expect(eventEmitter).not.toBeNull()
      expect(eventEmitter?.useSubscription).toHaveBeenCalledTimes(2)
      expect(callbacks).toHaveLength(2)
    })

    it('should render without subscriptions when event emitter is null', () => {
      const { callbacks, eventEmitter } = setup({ withEventEmitter: false })

      expect(eventEmitter).toBeNull()
      expect(callbacks).toHaveLength(0)
    })
  })

  describe('Update value event', () => {
    it('should update editor state when update event matches instance id', async () => {
      const { emit, getEditor } = setup({ instanceId: 'instance-1' })

      await waitFor(() => {
        expect(getEditor()).not.toBeNull()
      })
      const editor = getEditor()
      expect(editor).not.toBeNull()

      emit({
        type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
        instanceId: 'instance-1',
        payload: 'updated text',
      })

      await waitFor(() => {
        expect(readEditorText(editor!)).toBe('updated text')
      })
    })

    it('should ignore update event when instance id does not match', async () => {
      const { emit, getEditor } = setup({ instanceId: 'instance-1' })

      await waitFor(() => {
        expect(getEditor()).not.toBeNull()
      })
      const editor = getEditor()
      expect(editor).not.toBeNull()

      emit({
        type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
        instanceId: 'instance-2',
        payload: 'should not apply',
      })

      await waitFor(() => {
        expect(readEditorText(editor!)).toBe('')
      })
    })
  })

  describe('Quick insert event', () => {
    it('should insert slash when quick insert event matches instance id', async () => {
      const { emit, getEditor } = setup({ instanceId: 'instance-1' })

      await waitFor(() => {
        expect(getEditor()).not.toBeNull()
      })
      const editor = getEditor()
      expect(editor).not.toBeNull()

      selectRootEnd(editor!)

      emit({
        type: PROMPT_EDITOR_INSERT_QUICKLY,
        instanceId: 'instance-1',
      })

      await waitFor(() => {
        expect(readEditorText(editor!)).toBe('/')
      })
    })

    it('should ignore quick insert event when instance id does not match', async () => {
      const { emit, getEditor } = setup({ instanceId: 'instance-1' })

      await waitFor(() => {
        expect(getEditor()).not.toBeNull()
      })
      const editor = getEditor()
      expect(editor).not.toBeNull()

      selectRootEnd(editor!)

      emit({
        type: PROMPT_EDITOR_INSERT_QUICKLY,
        instanceId: 'instance-2',
      })

      await waitFor(() => {
        expect(readEditorText(editor!)).toBe('')
      })
    })
  })
})
