import '@testing-library/jest-dom'
import { axe, toHaveNoViolations } from 'jest-axe'
import { vi } from 'vitest'

expect.extend(toHaveNoViolations)

import './heading'
import { PktHeading, TPktHeadingLevel, TPktHeadingSize } from './heading'

const waitForCustomElements = async () => {
  await customElements.whenDefined('pkt-heading')
}

// Helper function to create heading element
const createHeading = async (headingProps = '', content = 'Test Heading') => {
  const container = document.createElement('div')
  container.innerHTML = `
    <pkt-heading ${headingProps}>${content}</pkt-heading>
  `
  document.body.appendChild(container)
  await waitForCustomElements()
  return container
}

// Cleanup after each test
afterEach(() => {
  document.body.innerHTML = ''
})

describe('PktHeading', () => {
  describe('Rendering and basic functionality', () => {
    test('renders without errors', async () => {
      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading

      expect(heading).toBeInTheDocument()
      expect(heading.shadowRoot).toBeTruthy()
    })

    test('renders with default properties', async () => {
      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.level).toBe(2)
      expect(heading.visuallyHidden).toBe(false)
      expect(heading.align).toBe(undefined)
    })

    test('renders content in shadow DOM slot', async () => {
      const container = await createHeading('', 'Custom Heading Text')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      const slot = heading.shadowRoot?.querySelector('slot')
      expect(slot).toBeInTheDocument()
      expect(heading.textContent).toContain('Custom Heading Text')
    })
  })

  describe('Properties and attributes', () => {
    test('applies default properties correctly', async () => {
      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.getAttribute('size')).toBe('large')
      expect(heading.getAttribute('level')).toBe('2')
      expect(heading.getAttribute('visually-hidden')).toBe(null)
      expect(heading.getAttribute('align')).toBe(null)
    })

    test('sets size property correctly', async () => {
      const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']

      for (const size of sizes) {
        const container = await createHeading(`size="${size}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.size).toBe(size)
        expect(heading.getAttribute('size')).toBe(size)
        document.body.innerHTML = ''
      }
    })

    test('sets level property correctly', async () => {
      const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]

      for (const level of levels) {
        const container = await createHeading(`level="${level}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.level).toBe(level)
        expect(heading.getAttribute('level')).toBe(String(level))
        document.body.innerHTML = ''
      }
    })

    test('sets visuallyHidden property correctly', async () => {
      const container = await createHeading('visuallyHidden="true"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.visuallyHidden).toBe(true)
      expect(heading.hasAttribute('visuallyHidden')).toBe(true)
    })

    test('sets align property correctly', async () => {
      const alignments = ['start', 'center', 'end'] as const

      for (const align of alignments) {
        const container = await createHeading(`align="${align}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.align).toBe(align)
        expect(heading.getAttribute('align')).toBe(align)
        document.body.innerHTML = ''
      }
    })
  })

  describe('CSS classes and styling', () => {
    test('applies default CSS classes', async () => {
      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading')).toBe(true)
      expect(heading.classList.contains('pkt-heading--medium')).toBe(false)
      expect(heading.classList.contains('pkt-heading--start')).toBe(false)
    })

    test('applies size-specific CSS classes', async () => {
      const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']

      for (const size of sizes) {
        const container = await createHeading(`size="${size}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.classList.contains(`pkt-heading--${size}`)).toBe(true)
        document.body.innerHTML = ''
      }
    })

    test('applies visually hidden class when enabled', async () => {
      const container = await createHeading('visuallyHidden="true"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-sr-only')).toBe(true)
    })

    test('applies alignment-specific CSS classes', async () => {
      const alignments = ['start', 'center', 'end'] as const

      for (const align of alignments) {
        const container = await createHeading(`align="${align}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.classList.contains(`pkt-heading--${align}`)).toBe(true)
        document.body.innerHTML = ''
      }
    })

    test('removes old classes when properties change', async () => {
      const container = await createHeading('size="small" align="center"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--small')).toBe(true)
      expect(heading.classList.contains('pkt-heading--center')).toBe(true)

      // Change properties
      heading.size = 'large'
      heading.align = 'end'
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--small')).toBe(false)
      expect(heading.classList.contains('pkt-heading--center')).toBe(false)
      expect(heading.classList.contains('pkt-heading--large')).toBe(true)
      expect(heading.classList.contains('pkt-heading--end')).toBe(true)
    })
  })

  describe('ARIA and accessibility attributes', () => {
    test('sets role and aria-level attributes on connection', async () => {
      const container = await createHeading('level="3"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.getAttribute('role')).toBe('heading')
      expect(heading.getAttribute('aria-level')).toBe('3')
    })

    test('updates aria-level when level property changes', async () => {
      const container = await createHeading('level="2"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.getAttribute('aria-level')).toBe('2')

      heading.level = 4
      await heading.updateComplete

      expect(heading.getAttribute('aria-level')).toBe('4')
    })

    test('updates aria-level when level attribute changes', async () => {
      const container = await createHeading('level="1"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.getAttribute('aria-level')).toBe('1')

      heading.setAttribute('level', '5')
      await heading.updateComplete

      expect(heading.getAttribute('aria-level')).toBe('5')
      expect(heading.level).toBe(5)
    })
  })

  describe('Level validation', () => {
    test('accepts valid heading levels (1-6)', async () => {
      const validLevels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]

      for (const level of validLevels) {
        const container = await createHeading(`level="${level}"`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        expect(heading.level).toBe(level)
        expect(heading.getAttribute('aria-level')).toBe(String(level))
        document.body.innerHTML = ''
      }
    })

    test('handles invalid levels gracefully', async () => {
      const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      // Test invalid level via property
      heading.level = 0 as TPktHeadingLevel
      await heading.updateComplete

      expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 0. Must be between 1 and 6.')

      heading.level = 7 as TPktHeadingLevel
      await heading.updateComplete

      expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 7. Must be between 1 and 6.')

      consoleSpy.mockRestore()
    })
  })

  describe('Property updates and lifecycle', () => {
    test('updates classes when size property changes', async () => {
      const container = await createHeading('size="small"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--small')).toBe(true)

      heading.size = 'xlarge'
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--small')).toBe(false)
      expect(heading.classList.contains('pkt-heading--xlarge')).toBe(true)
    })

    test('updates classes when visuallyHidden property changes', async () => {
      const container = await createHeading()
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-sr-only')).toBe(false)

      heading.visuallyHidden = true
      await heading.updateComplete

      expect(heading.classList.contains('pkt-sr-only')).toBe(true)
    })

    test('updates classes when align property changes', async () => {
      const container = await createHeading('align="start"')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--start')).toBe(true)

      heading.align = 'center'
      await heading.updateComplete

      expect(heading.classList.contains('pkt-heading--start')).toBe(false)
      expect(heading.classList.contains('pkt-heading--center')).toBe(true)
    })
  })

  describe('Content rendering', () => {
    test('renders simple text content', async () => {
      const container = await createHeading('', 'Simple Heading')
      const heading = container.querySelector('pkt-heading') as PktHeading

      expect(heading.textContent).toContain('Simple Heading')
    })

    test('renders HTML content safely', async () => {
      const container = await createHeading('', '<strong>Bold</strong> heading')
      const heading = container.querySelector('pkt-heading') as PktHeading

      expect(heading.innerHTML).toContain('<strong>Bold</strong>')
      expect(heading.innerHTML).toContain('heading')
      expect(heading.querySelector('strong')).toBeInTheDocument()
    })

    test('renders multiple child elements', async () => {
      const container = document.createElement('div')
      container.innerHTML = `
        <pkt-heading>
          <span>Part 1</span>
          <em>Part 2</em>
        </pkt-heading>
      `
      document.body.appendChild(container)
      await waitForCustomElements()

      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      expect(heading.querySelector('span')).toBeInTheDocument()
      expect(heading.querySelector('em')).toBeInTheDocument()
      expect(heading.textContent).toContain('Part 1')
      expect(heading.textContent).toContain('Part 2')
    })
  })

  describe('Accessibility', () => {
    test('basic heading is accessible', async () => {
      const container = await createHeading('', 'Accessible Heading')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      const results = await axe(heading)
      expect(results).toHaveNoViolations()
    })

    test('heading with different levels is accessible', async () => {
      const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]

      for (const level of levels) {
        const container = await createHeading(`level="${level}"`, `Level ${level} Heading`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        const results = await axe(heading)
        expect(results).toHaveNoViolations()
        document.body.innerHTML = ''
      }
    })

    test('visually hidden heading is accessible', async () => {
      const container = await createHeading('visually-hidden="true"', 'Hidden Heading')
      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      const results = await axe(heading)
      expect(results).toHaveNoViolations()
    })

    test('heading with different alignments is accessible', async () => {
      const alignments = ['start', 'center', 'end'] as const

      for (const align of alignments) {
        const container = await createHeading(`align="${align}"`, `${align} aligned heading`)
        const heading = container.querySelector('pkt-heading') as PktHeading
        await heading.updateComplete

        const results = await axe(heading)
        expect(results).toHaveNoViolations()
        document.body.innerHTML = ''
      }
    })

    test('complex heading content is accessible', async () => {
      const container = document.createElement('div')
      container.innerHTML = `
        <pkt-heading level="1" size="large" align="center">
          <span>Main</span> <em>Title</em> with <strong>emphasis</strong>
        </pkt-heading>
      `
      document.body.appendChild(container)
      await waitForCustomElements()

      const heading = container.querySelector('pkt-heading') as PktHeading
      await heading.updateComplete

      const results = await axe(heading)
      expect(results).toHaveNoViolations()
    })
  })

  describe('Integration scenarios', () => {
    test('works correctly with multiple headings', async () => {
      const container = document.createElement('div')
      container.innerHTML = `
        <pkt-heading level="1" size="xlarge">Main Title</pkt-heading>
        <pkt-heading level="2" size="large">Subtitle</pkt-heading>
        <pkt-heading level="3" size="medium">Section</pkt-heading>
      `
      document.body.appendChild(container)
      await waitForCustomElements()

      const headings = container.querySelectorAll('pkt-heading') as NodeListOf<PktHeading>
      await Promise.all([...headings].map((h) => h.updateComplete))

      expect(headings[0].level).toBe(1)
      expect(headings[0].size).toBe('xlarge')
      expect(headings[1].level).toBe(2)
      expect(headings[1].size).toBe('large')
      expect(headings[2].level).toBe(3)
      expect(headings[2].size).toBe('medium')
    })

    test('maintains independence between multiple instances', async () => {
      const container = document.createElement('div')
      container.innerHTML = `
        <pkt-heading id="h1" level="1" size="large">Heading 1</pkt-heading>
        <pkt-heading id="h2" level="2" size="small">Heading 2</pkt-heading>
      `
      document.body.appendChild(container)
      await waitForCustomElements()

      const heading1 = container.querySelector('#h1') as PktHeading
      const heading2 = container.querySelector('#h2') as PktHeading
      await Promise.all([heading1.updateComplete, heading2.updateComplete])

      // Change properties on first heading
      heading1.size = 'xlarge'
      heading1.align = 'center'
      await heading1.updateComplete

      // Verify second heading is unaffected
      expect(heading2.size).toBe('small')
      expect(heading2.align).toBe(undefined)
      expect(heading1.size).toBe('xlarge')
      expect(heading1.align).toBe('center')
    })
  })
})
