import { describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import type { RichTextEditorToolbarState } from './DefaultToolbar'
import { Extension } from '@tiptap/core'
import { Document } from '@tiptap/extension-document'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Text } from '@tiptap/extension-text'
import { RichTextEditor } from './RichTextEditor'

async function getEditor(name = 'Body') {
  return waitFor(() => screen.getByRole('textbox', { name }))
}

describe('RichTextEditor', () => {
  it('renders an editable region with the passed aria-label', async () => {
    render(<RichTextEditor value="" onChange={vi.fn()} aria-label="Article body" />)
    const editor = await getEditor('Article body')
    expect(editor.getAttribute('aria-label')).toBe('Article body')
    expect(editor.getAttribute('contenteditable')).toBe('true')
    expect(editor.getAttribute('aria-readonly')).toBe('false')
  })

  it('forwards aria-describedby to the editable textbox', async () => {
    render(
      <RichTextEditor
        value=""
        onChange={vi.fn()}
        aria-label="Article body"
        aria-describedby="body-error"
      />,
    )
    expect((await getEditor('Article body')).getAttribute('aria-describedby')).toBe('body-error')
  })

  it('updates and removes aria-describedby after mount', async () => {
    const { rerender } = render(
      <RichTextEditor value="" onChange={vi.fn()} aria-label="Article body" />,
    )
    const editor = await getEditor('Article body')
    expect(editor.getAttribute('aria-describedby')).toBeNull()

    rerender(
      <RichTextEditor
        value=""
        onChange={vi.fn()}
        aria-label="Article body"
        aria-describedby="body-error"
      />,
    )
    await waitFor(() => expect(editor.getAttribute('aria-describedby')).toBe('body-error'))

    rerender(<RichTextEditor value="" onChange={vi.fn()} aria-label="Article body" />)
    await waitFor(() => expect(editor.getAttribute('aria-describedby')).toBeNull())
  })

  it('does not emit changes while mounting or synchronizing a controlled value', async () => {
    const onChange = vi.fn()
    const { rerender } = render(
      <RichTextEditor value="<p>Initial</p>" onChange={onChange} aria-label="Body" />,
    )
    await getEditor('Body')
    await waitFor(() => expect(onChange).not.toHaveBeenCalled())

    rerender(<RichTextEditor value="<p>External</p>" onChange={onChange} aria-label="Body" />)
    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' }).textContent).toBe('External'))
    expect(onChange).not.toHaveBeenCalled()
  })

  it("keeps a pristine empty controlled value as '' without emitting mount-generated HTML", async () => {
    const onChange = vi.fn()
    render(<RichTextEditor value="" onChange={onChange} aria-label="Body" />)
    await getEditor('Body')
    await waitFor(() => expect(onChange).not.toHaveBeenCalled())
  })

  it('clears entity A history when an authoritative controlled value changes to B', async () => {
    const onChange = vi.fn()
    const toolbar = ({ editor }: RichTextEditorToolbarState) => (
      <>
        <button type="button" onClick={() => editor?.commands.insertContent(' edited')}>
          Edit
        </button>
        <button type="button" onClick={() => editor?.chain().focus().undo().run()}>
          Undo
        </button>
      </>
    )
    const { rerender } = render(
      <RichTextEditor
        value="<p>A</p>"
        onChange={onChange}
        aria-label="Body"
        renderToolbar={toolbar}
      />,
    )
    await getEditor('Body')
    fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
    await waitFor(() => expect(onChange).toHaveBeenCalledWith(expect.stringContaining('edited')))
    onChange.mockClear()

    rerender(
      <RichTextEditor
        value="<p>B</p>"
        onChange={onChange}
        aria-label="Body"
        renderToolbar={toolbar}
      />,
    )
    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' }).textContent).toBe('B'))
    expect(onChange).not.toHaveBeenCalled()

    fireEvent.click(screen.getByRole('button', { name: 'Undo' }))

    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' }).textContent).toBe('B'))
    expect(onChange).not.toHaveBeenCalledWith(expect.stringContaining('A'))
  })

  it('restores the authoritative value when a host rejects an emitted edit without changing the prop string', async () => {
    const onChange = vi.fn()
    const toolbar = ({ editor }: RichTextEditorToolbarState) => (
      <>
        <button type="button" onClick={() => editor?.commands.insertContent(' rejected')}>Edit</button>
        <button type="button" onClick={() => editor?.chain().focus().undo().run()}>Undo</button>
      </>
    )
    render(
      <RichTextEditor
        value="<p>A</p>"
        onChange={onChange}
        aria-label="Body"
        renderToolbar={toolbar}
      />,
    )
    await getEditor('Body')

    fireEvent.click(screen.getByRole('button', { name: 'Edit' }))
    await waitFor(() => expect(onChange).toHaveBeenCalledWith(expect.stringContaining('rejected')))
    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' }).textContent).toBe('A'))
    onChange.mockClear()

    fireEvent.click(screen.getByRole('button', { name: 'Undo' }))
    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' }).textContent).toBe('A'))
    expect(onChange).not.toHaveBeenCalled()
  })

  it('preserves an existing H1 through a user edit without exposing an H1 authoring control', async () => {
    const onChange = vi.fn()
    render(
      <RichTextEditor
        value="<h1>Legacy</h1><p>Body</p>"
        onChange={onChange}
        aria-label="Body"
        renderToolbar={({ editor }) => (
          <button type="button" onClick={() => editor?.commands.insertContentAt(8, ' edited')}>Edit body</button>
        )}
      />,
    )
    const textbox = await getEditor('Body')
    expect(textbox.querySelector('h1')?.textContent).toBe('Legacy')

    fireEvent.click(screen.getByRole('button', { name: 'Edit body' }))
    await waitFor(() => expect(onChange).toHaveBeenCalled())
    expect(String(onChange.mock.calls.at(-1)?.[0])).toContain('<h1')
    expect(String(onChange.mock.calls.at(-1)?.[0])).toContain('Legacy')
  })

  it('fires onChange with HTML when content is updated', async () => {
    const onChange = vi.fn()
    render(
      <RichTextEditor
        value="<p></p>"
        onChange={onChange}
        aria-label="Body"
        renderToolbar={({ editor }) => (
          <button type="button" onClick={() => editor?.commands.insertContent('Hello')}>
            Insert text
          </button>
        )}
      />,
    )
    await getEditor('Body')

    fireEvent.click(screen.getByRole('button', { name: 'Insert text' }))

    await waitFor(() => {
      expect(onChange).toHaveBeenLastCalledWith('<p dir="ltr">Hello</p>')
    })
  })

  it('loads value HTML into the editor (round-trip)', async () => {
    const html = '<p><strong>Hello</strong></p>'
    render(<RichTextEditor value={html} onChange={vi.fn()} aria-label="Body" />)
    const editor = await getEditor('Body')
    await waitFor(() => {
      expect(editor.querySelector('strong')?.textContent).toBe('Hello')
    })
  })

  it("applies dir='rtl' to the editor root", async () => {
    render(<RichTextEditor value="" onChange={vi.fn()} dir="rtl" aria-label="Body" />)
    const editor = await getEditor('Body')
    expect(editor.getAttribute('dir')).toBe('rtl')
  })

  it('makes the editor non-editable when readOnly', async () => {
    render(<RichTextEditor value="<p>x</p>" onChange={vi.fn()} readOnly aria-label="Body" />)
    const editor = await getEditor('Body')
    await waitFor(() => {
      expect(editor.getAttribute('contenteditable')).toBe('false')
      expect(editor.getAttribute('aria-readonly')).toBe('true')
    })
  })

  it('toolbar Bold button toggles bold and reflects aria-pressed', async () => {
    render(<RichTextEditor value="<p>word</p>" onChange={vi.fn()} aria-label="Body" />)
    await getEditor('Body')

    const bold = screen.getByRole('button', { name: 'Bold' })
    expect(bold.getAttribute('aria-pressed')).toBe('false')

    fireEvent.click(bold)
    await waitFor(() => {
      expect(bold.getAttribute('aria-pressed')).toBe('true')
    })

    fireEvent.click(bold)
    await waitFor(() => {
      expect(bold.getAttribute('aria-pressed')).toBe('false')
    })
  })

  it('preserves the editor instance for equivalent inline extension configs', async () => {
    const extensions = () => ({ extensions: [Document, Paragraph, Text] })
    const { rerender } = render(
      <RichTextEditor
        value="<p>plain</p>"
        onChange={vi.fn()}
        aria-label="Body"
        extensions={extensions()}
        renderToolbar={() => null}
      />,
    )
    const initialEditor = await getEditor('Body')
    initialEditor.focus()

    rerender(
      <RichTextEditor
        value="<p>plain</p>"
        onChange={vi.fn()}
        aria-label="Body"
        extensions={extensions()}
        renderToolbar={() => null}
      />,
    )

    expect(screen.getByRole('textbox', { name: 'Body' })).toBe(initialEditor)
    expect(document.activeElement).toBe(initialEditor)
  })

  it('recreates for same-name extensions with different implementations', async () => {
    const first = Extension.create({ name: 'hostExtension' })
    const second = Extension.create({ name: 'hostExtension', addStorage: () => ({ version: 2 }) })
    const { rerender } = render(
      <RichTextEditor
        value="<p>plain</p>"
        onChange={vi.fn()}
        aria-label="Body"
        extensions={{ extensions: [Document, Paragraph, Text, first] }}
        renderToolbar={() => null}
      />,
    )
    const initialEditor = await getEditor('Body')

    rerender(
      <RichTextEditor
        value="<p>plain</p>"
        onChange={vi.fn()}
        aria-label="Body"
        extensions={{ extensions: [Document, Paragraph, Text, second] }}
        renderToolbar={() => null}
      />,
    )

    await waitFor(() => expect(screen.getByRole('textbox', { name: 'Body' })).not.toBe(initialEditor))
  })

  it('custom extensions override the default set', async () => {
    render(
      <RichTextEditor
        value="<h2>Title</h2><p>plain</p>"
        onChange={vi.fn()}
        aria-label="Body"
        extensions={{ extensions: [Document, Paragraph, Text] }}
        renderToolbar={() => null}
      />,
    )
    const editor = await getEditor('Body')
    await waitFor(() => {
      expect(editor.querySelector('h2')).toBeNull()
      expect(editor.textContent).toContain('plain')
    })
  })
})
