import { assert, fixture, html, nextFrame } from '@open-wc/testing'
import type { MentionSuggestion } from '../../../src/elements/mention-textarea/ui-mention-textarea.js'
import '../../../src/elements/mention-textarea/ui-mention-textarea.js'

type MentionTextAreaElement =
  import('../../../src/elements/mention-textarea/ui-mention-textarea.js').MentionTextAreaElement

describe('MentionTextArea', () => {
  const mockSuggestions: MentionSuggestion[] = [
    { id: 'user1', label: 'John Doe', description: 'Software Engineer' },
    { id: 'user2', label: 'Jane Smith', description: 'Product Manager' },
    { id: 'user3', label: 'Bob Johnson', description: 'Designer', suffix: 'Design Team' },
  ]

  async function basicFixture(): Promise<MentionTextAreaElement> {
    return fixture(html`<mention-textarea></mention-textarea>`)
  }

  async function withSuggestionsFixture(): Promise<MentionTextAreaElement> {
    return fixture(html`<mention-textarea .suggestions="${mockSuggestions}"></mention-textarea>`)
  }

  async function labeledFixture(): Promise<MentionTextAreaElement> {
    return fixture(html`
      <mention-textarea
        label="Message"
        placeholder="Type your message..."
        .suggestions="${mockSuggestions}"
      ></mention-textarea>
    `)
  }

  function getEditor(element: MentionTextAreaElement): HTMLElement {
    return element.shadowRoot!.querySelector('.editor')!
  }

  function getSuggestionsPopover(element: MentionTextAreaElement): HTMLElement {
    return element.shadowRoot!.querySelector('.suggestions-popover')!
  }

  function getSuggestionItems(element: MentionTextAreaElement): NodeListOf<Element> {
    return element.shadowRoot!.querySelectorAll('.suggestion-item')
  }

  function simulateInput(element: MentionTextAreaElement, text: string): void {
    const editor = getEditor(element)
    editor.textContent = text
    editor.dispatchEvent(new Event('input', { bubbles: true }))
  }

  function simulatePaste(element: MentionTextAreaElement, text: string): void {
    const editor = getEditor(element)
    const clipboardData = new DataTransfer()
    clipboardData.setData('text/plain', text)

    const pasteEvent = new ClipboardEvent('paste', {
      bubbles: true,
      clipboardData,
    })

    editor.dispatchEvent(pasteEvent)
  }

  describe('basic functionality', () => {
    it('should render with default properties', async () => {
      const element = await basicFixture()

      assert.isNotNull(element)
      assert.equal(element.label, '')
      assert.equal(element.value, '')
      assert.equal(element.mentionTrigger, '@')
      assert.equal(element.minQueryLength, 0)
      assert.isFalse(element.disabled)
      assert.isFalse(element.required)
    })

    it('should have proper shadow DOM structure', async () => {
      const element = await labeledFixture()

      const container = element.shadowRoot!.querySelector('.container')
      const editor = getEditor(element)
      const label = element.shadowRoot!.querySelector('.label')

      assert.isNotNull(container)
      assert.isNotNull(editor)
      assert.isNotNull(label)
      assert.equal(editor.getAttribute('contenteditable'), 'true')
      assert.equal(editor.getAttribute('role'), 'textbox')
    })

    it('should display label and placeholder', async () => {
      const element = await labeledFixture()

      const label = element.shadowRoot!.querySelector('.label')
      const editor = getEditor(element)

      assert.equal(label!.textContent!.trim(), 'Message')
      assert.equal(editor.getAttribute('data-placeholder'), 'Type your message...')
    })
  })

  describe('value property', () => {
    it('should update value when set programmatically', async () => {
      const element = await basicFixture()

      element.value = 'Programmatic value'
      await nextFrame()

      assert.equal(element.value, 'Programmatic value')
      const editor = getEditor(element)
      assert.equal(editor.textContent, 'Programmatic value')
    })

    it('should sync value when editor content changes', async () => {
      const element = await basicFixture()

      simulateInput(element, 'Hello world')
      await nextFrame()

      assert.equal(element.value, 'Hello world')
    })

    it('should handle mentions in value', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} how are you?'
      await nextFrame()

      const editor = getEditor(element)
      const mentionChip = editor.querySelector('.mention-chip')
      assert.isNotNull(mentionChip)
      assert.equal(mentionChip!.getAttribute('data-mention-id'), 'user1')
    })
  })

  describe('disabled state', () => {
    it('should disable editor when disabled', async () => {
      const element = await basicFixture()
      element.disabled = true
      await nextFrame()

      const editor = getEditor(element)
      assert.equal(editor.getAttribute('contenteditable'), 'false')
      assert.equal(editor.getAttribute('tabindex'), '-1')
    })
  })

  describe('mention suggestions', () => {
    it('should show suggestions container when created', async () => {
      const element = await withSuggestionsFixture()

      // Check if suggestions container exists
      const popover = getSuggestionsPopover(element)
      assert.isNotNull(popover)
    })

    it('should create mention chip in value format', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1}'
      await nextFrame()

      const editor = getEditor(element)
      const mentionChip = editor.querySelector('.mention-chip')

      assert.isNotNull(mentionChip)
      assert.equal(mentionChip!.getAttribute('data-mention-id'), 'user1')
      assert.equal(mentionChip!.getAttribute('contenteditable'), 'false')
    })
  })

  describe('paste functionality', () => {
    it('should handle pasting plain text', async () => {
      const element = await basicFixture()

      // Focus editor and set up selection
      const editor = getEditor(element)
      editor.focus()

      // Create a range at the beginning
      const range = document.createRange()
      const selection = window.getSelection()!
      range.setStart(editor, 0)
      range.setEnd(editor, 0)
      selection.removeAllRanges()
      selection.addRange(range)

      simulatePaste(element, 'Pasted content')
      await nextFrame()

      assert.equal(element.value, 'Pasted content')
    })

    it('should insert pasted text in editor', async () => {
      const element = await basicFixture()

      // Focus editor and set up selection
      const editor = getEditor(element)
      editor.focus()

      // Create a range at the beginning
      const range = document.createRange()
      const selection = window.getSelection()!
      range.setStart(editor, 0)
      range.setEnd(editor, 0)
      selection.removeAllRanges()
      selection.addRange(range)

      simulatePaste(element, 'Test paste')
      await nextFrame()

      const editorAfter = getEditor(element)
      assert.equal(editorAfter.textContent, 'Test paste')
    })
  })

  describe('accessibility', () => {
    it('should have proper ARIA attributes', async () => {
      const element = await labeledFixture()

      const editor = getEditor(element)

      assert.equal(editor.getAttribute('role'), 'textbox')
      assert.equal(editor.getAttribute('aria-multiline'), 'true')
      assert.equal(editor.getAttribute('aria-label'), 'Message')
    })

    it('should set aria-required when required', async () => {
      const element = await basicFixture()
      element.required = true
      await nextFrame()

      const editor = getEditor(element)
      assert.equal(editor.getAttribute('aria-required'), 'true')
    })

    it('should set aria-invalid when invalid', async () => {
      const element = await basicFixture()
      element.invalid = true
      await nextFrame()

      const editor = getEditor(element)
      assert.equal(editor.getAttribute('aria-invalid'), 'true')
    })

    it('should be focusable with proper tabindex', async () => {
      const element = await basicFixture()

      const editor = getEditor(element)
      assert.equal(editor.getAttribute('tabindex'), '0')
    })

    it('should not be focusable when disabled', async () => {
      const element = await basicFixture()
      element.disabled = true
      await nextFrame()

      const editor = getEditor(element)
      assert.equal(editor.getAttribute('tabindex'), '-1')
    })
  })

  describe('form integration', () => {
    it('should have proper name attribute', async () => {
      const element = (await fixture(
        html`<mention-textarea name="message"></mention-textarea>`
      )) as MentionTextAreaElement

      assert.equal(element.name, 'message')
    })

    it('should integrate with form validation', async () => {
      const element = await basicFixture()
      element.required = true
      await nextFrame()

      // Element should be invalid when empty and required
      assert.isTrue(element.required)
      assert.equal(element.value, '')
    })
  })

  describe('edge cases', () => {
    it('should handle empty suggestions array', async () => {
      const element = (await fixture(
        html`<mention-textarea .suggestions="${[]}"></mention-textarea>`
      )) as MentionTextAreaElement

      simulateInput(element, '@test')
      await nextFrame()

      const suggestions = getSuggestionItems(element)
      assert.equal(suggestions.length, 0)
    })

    it('should handle malformed mention in value', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{nonexistent} user'
      await nextFrame()

      const editor = getEditor(element)
      // Should fallback to text when mention is not found
      assert.include(editor.textContent!, '@{nonexistent}')
    })

    it('should handle multiple mentions in one text', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{user2}!'
      await nextFrame()

      const editor = getEditor(element)
      const mentionChips = editor.querySelectorAll('.mention-chip')

      assert.equal(mentionChips.length, 2)
      assert.equal(mentionChips[0].getAttribute('data-mention-id'), 'user1')
      assert.equal(mentionChips[1].getAttribute('data-mention-id'), 'user2')
    })
  })

  describe('mentions getter', () => {
    it('should return empty array when no mentions exist', async () => {
      const element = await basicFixture()

      element.value = 'Hello world without mentions'
      await nextFrame()

      assert.deepEqual(element.mentions, [])
    })

    it('should return single mention ID', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} how are you?'
      await nextFrame()

      assert.deepEqual(element.mentions, ['user1'])
    })

    it('should return multiple unique mention IDs', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{user2}!'
      await nextFrame()

      assert.deepEqual(element.mentions.sort(), ['user1', 'user2'])
    })

    it('should deduplicate repeated mentions', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1}, @{user1} said @{user2} and @{user1} again'
      await nextFrame()

      const mentions = element.mentions.sort()
      assert.deepEqual(mentions, ['user1', 'user2'])
      assert.equal(mentions.length, 2)
    })

    it('should handle empty value', async () => {
      const element = await withSuggestionsFixture()

      element.value = ''
      await nextFrame()

      assert.deepEqual(element.mentions, [])
    })

    it('should handle malformed mentions', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{ } and @{valid_user}'
      await nextFrame()

      const mentions = element.mentions.sort()
      assert.include(mentions, 'user1')
      assert.include(mentions, ' ') // empty space is still captured
      assert.include(mentions, 'valid_user')
      assert.equal(mentions.length, 3)
    })

    it('should handle special characters in mention IDs', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user-1} and @{user_2} and @{user.3}'
      await nextFrame()

      const mentions = element.mentions.sort()
      assert.deepEqual(mentions, ['user-1', 'user.3', 'user_2'])
    })

    it('should handle unicode characters in mention IDs', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user-🎉} and @{user_ñ} and @{用户1}'
      await nextFrame()

      const mentions = element.mentions.sort()
      assert.deepEqual(mentions, ['user-🎉', 'user_ñ', '用户1'])
    })

    it('should handle nested curly braces gracefully', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user{nested}} and @{user1}'
      await nextFrame()

      const mentions = element.mentions.sort()
      // The regex matches @{user{nested}} as 'user{nested' and @{user1} as 'user1'
      assert.include(mentions, 'user1')
      assert.include(mentions, 'user{nested')
      assert.equal(mentions.length, 2)
    })

    it('should handle very long mention IDs', async () => {
      const element = await withSuggestionsFixture()
      const longId = 'a'.repeat(1000)

      element.value = `Hello @{${longId}} world`
      await nextFrame()

      const mentions = element.mentions
      assert.include(mentions, longId)
      assert.equal(mentions.length, 1)
    })

    it('should handle mentions at the beginning and end of text', async () => {
      const element = await withSuggestionsFixture()

      element.value = '@{user1} said hello to @{user2}'
      await nextFrame()

      const mentions = element.mentions.sort()
      assert.deepEqual(mentions, ['user1', 'user2'])
    })
  })

  describe('getPlainText method', () => {
    it('should return plain text without mentions when no mentions exist', async () => {
      const element = await basicFixture()

      element.value = 'Hello world without mentions'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello world without mentions')
    })

    it('should replace mention with field label when field exists in suggestions', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} how are you?'
      await nextFrame()

      // user1 should be replaced with "John Doe" from suggestions
      assert.equal(element.getPlainText(), 'Hello John Doe how are you?')
    })

    it('should replace multiple mentions with their labels', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{user2}!'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello John Doe and Jane Smith!')
    })

    it('should keep original field name when field not found in suggestions', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{nonexistent} user'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello nonexistent user')
    })

    it('should handle mixed existing and non-existing mentions', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{nonexistent} and @{user2}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello John Doe and nonexistent and Jane Smith')
    })

    it('should handle duplicate mentions correctly', async () => {
      const element = await withSuggestionsFixture()

      element.value = '@{user1} said hello to @{user1}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'John Doe said hello to John Doe')
    })

    it('should handle empty value', async () => {
      const element = await withSuggestionsFixture()

      element.value = ''
      await nextFrame()

      assert.equal(element.getPlainText(), '')
    })

    it('should preserve text formatting and whitespace', async () => {
      const element = await withSuggestionsFixture()

      element.value = '  Hello @{user1},\n\nHow are you @{user2}?  \n'
      await nextFrame()

      assert.equal(element.getPlainText(), '  Hello John Doe,\n\nHow are you Jane Smith?  \n')
    })

    it('should handle mention-like text that is not a mention', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Use at{symbol} and @{user1}'
      await nextFrame()

      // Only @{user1} should be replaced, at{symbol} should remain as is
      assert.equal(element.getPlainText(), 'Use at{symbol} and John Doe')
    })

    it('should find field by ID not label', async () => {
      const element = await withSuggestionsFixture()

      // Use the ID 'user1' which should match and return label 'John Doe'
      element.value = 'Hello @{user1}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello John Doe')
    })

    it('should handle mention IDs with special characters', async () => {
      const element = await withSuggestionsFixture()
      // Add a suggestion with special characters in ID
      element.suggestions = [
        ...element.suggestions,
        { id: 'user-special_123', label: 'Special User', description: 'Test user' },
      ]

      element.value = 'Hello @{user-special_123}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello Special User')
    })

    it('should handle unicode characters in mention IDs', async () => {
      const element = await withSuggestionsFixture()
      element.suggestions = [
        ...element.suggestions,
        { id: 'user-🎉', label: 'Party User', description: 'Celebration' },
        { id: '用户1', label: 'Chinese User', description: 'Test' },
      ]

      element.value = 'Hello @{user-🎉} and @{用户1}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello Party User and Chinese User')
    })

    it('should handle complex text with multiple lines and mentions', async () => {
      const element = await withSuggestionsFixture()

      element.value = `Line 1 with @{user1}
Line 2 with @{user2}
Line 3 with @{user1} again`
      await nextFrame()

      const expected = `Line 1 with John Doe
Line 2 with Jane Smith
Line 3 with John Doe again`

      assert.equal(element.getPlainText(), expected)
    })

    it('should handle malformed mentions gracefully', async () => {
      const element = await withSuggestionsFixture()

      element.value = 'Hello @{user1} and @{invalid and @{user2}'
      await nextFrame()

      // The regex matches @{user1} and @{invalid and @{user2} as one mention
      // @{user1} gets replaced with "John Doe"
      // @{invalid and @{user2} doesn't match any suggestion, so falls back to the field ID
      assert.equal(element.getPlainText(), 'Hello John Doe and invalid and @{user2')
    })

    it('should handle empty suggestion array', async () => {
      const element = await basicFixture()
      element.suggestions = []

      element.value = 'Hello @{user1} world'
      await nextFrame()

      // Should fallback to the mention ID since no suggestions available
      assert.equal(element.getPlainText(), 'Hello user1 world')
    })

    it('should handle null and undefined values in suggestions', async () => {
      const element = await withSuggestionsFixture()
      element.suggestions = [
        { id: 'user1', label: 'John Doe', description: 'Engineer' },
        { id: 'user2', label: '', description: 'Empty label' }, // Empty label
        { id: 'user3', label: 'Jane Smith' }, // No description
      ]

      element.value = 'Hello @{user1} and @{user2} and @{user3}'
      await nextFrame()

      assert.equal(element.getPlainText(), 'Hello John Doe and  and Jane Smith')
    })
  })
})
