import '@testing-library/jest-dom'

import { act, render, screen, waitFor } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'

import { Truncate } from './Truncate'

expect.extend(toHaveNoViolations)

describe('MiddleTruncate', () => {
  let resizeObserverCallback: ResizeObserverCallback
  let observedElements: Set<Element>
  let originalGetComputedStyle: typeof window.getComputedStyle

  beforeEach(() => {
    observedElements = new Set()

    originalGetComputedStyle = window.getComputedStyle
    // Stable line-height/font-size for overflow calculations in tests
    window.getComputedStyle = ((...args: any[]) => {
      const style = originalGetComputedStyle(...(args as [Element]))
      // Keep CSSStyleDeclaration methods (like getPropertyValue) intact.
      return new Proxy(style, {
        get(target, prop) {
          if (prop === 'lineHeight') return '16px'
          return (target as any)[prop]
        },
      })
    }) as any

    // Mock ResizeObserver
    global.ResizeObserver = class ResizeObserver {
      constructor(callback: ResizeObserverCallback) {
        resizeObserverCallback = callback
      }

      observe(element: Element) {
        observedElements.add(element)
      }

      unobserve(element: Element) {
        observedElements.delete(element)
      }

      disconnect() {
        observedElements.clear()
      }
    } as any
  })

  afterEach(() => {
    observedElements.clear()
    window.getComputedStyle = originalGetComputedStyle
  })

  const triggerResize = (element: HTMLElement) => {
    resizeObserverCallback(
      [
        {
          target: element,
          contentRect: {} as DOMRectReadOnly,
          borderBoxSize: [] as ResizeObserverSize[],
          contentBoxSize: [] as ResizeObserverSize[],
          devicePixelContentBoxSize: [] as ResizeObserverSize[],
        },
      ],
      {} as ResizeObserver,
    )
  }

  test('renders text content correctly', () => {
    render(<Truncate>short-filename.txt</Truncate>)

    expect(
      screen.getByText(
        (content, element) =>
          content === 'short-filename.txt' && element?.getAttribute('data-pkt-truncate-part') === 'first',
      ),
    ).toBeInTheDocument()
  })

  test('renders with custom tail length', () => {
    const longText = 'very-long-filename-that-needs-truncation.txt'
    const { container } = render(<Truncate tail={3}>{longText}</Truncate>)

    // Check that the full text is in the first part
    const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]')
    const firstPart = container.querySelector('[data-pkt-truncate-part="first"]')
    expect(wrapper).toBeTruthy()
    expect(firstPart).toBeTruthy()
    expect(firstPart).toHaveTextContent(longText)
    expect(wrapper as HTMLElement).toContainElement(firstPart as HTMLElement)

    // Check that the tail part contains the last 3 characters
    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveTextContent('txt')
  })

  test('uses default tail length of 5', () => {
    const longText = 'very-long-filename.txt'
    const { container } = render(<Truncate>{longText}</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveTextContent('e.txt') // last 5 characters
  })

  test('does not show tail for very short strings', () => {
    const shortText = 'file.txt' // 8 characters, tail=5, so 5+3=8, no tail shown
    const { container } = render(<Truncate tail={5}>{shortText}</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveTextContent('') // no tail content (secondPart is null)
  })

  test('tail is hidden when not overflowing', () => {
    const { container } = render(<Truncate>some-filename.txt</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')

    expect(tailSpan).toHaveStyle({ display: 'none' })
  })

  test('tail becomes visible when overflowing', async () => {
    const { container } = render(<Truncate>very-long-filename-that-overflows.txt</Truncate>)

    const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') as HTMLElement
    const measure = container.querySelector('[data-pkt-truncate-part="measure"]') as HTMLElement
    const tail = container.querySelector('[data-pkt-truncate-part="tail"]') as HTMLElement
    expect(wrapper).toBeTruthy()
    expect(measure).toBeTruthy()
    expect(tail).toBeTruthy()

    // Simulate > 2 lines (lineHeight=16px -> maxHeight=32px)
    measure.getBoundingClientRect = () => ({ height: 40 } as any)

    // Trigger overflow recalculation (ResizeObserver observes wrapper)
    act(() => triggerResize(wrapper))

    await waitFor(() => {
      expect(tail).toHaveStyle({ display: 'inline' })
    })
  })

  test('observes the wrapper element with ResizeObserver', () => {
    const { container } = render(<Truncate>filename.txt</Truncate>)

    const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]')
    expect(observedElements.has(wrapper!)).toBe(true)
  })

  test('disconnects ResizeObserver on unmount', () => {
    const { unmount } = render(<Truncate>filename.txt</Truncate>)

    expect(observedElements.size).toBe(1)

    unmount()

    expect(observedElements.size).toBe(0)
  })

  test('forwards additional props to wrapper span', () => {
    render(
      <Truncate className="custom-class" data-testid="test-truncate">
        filename.txt
      </Truncate>,
    )

    const wrapper = screen.getByTestId('test-truncate')
    expect(wrapper).toHaveClass('custom-class')
    expect(wrapper.tagName).toBe('SPAN')
  })

  test('merges custom styles with default styles', () => {
    const { container } = render(
      <Truncate style={{ color: 'rgb(255, 0, 0)', fontSize: '14px' }}>filename.txt</Truncate>,
    )

    const wrapper = container.firstChild as HTMLElement
    expect(wrapper).toHaveStyle({
      display: 'grid',
      color: 'rgb(255, 0, 0)',
      fontSize: '14px',
    })
  })

  test('tail span has aria-hidden attribute', () => {
    const { container } = render(<Truncate>very-long-filename.txt</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveAttribute('aria-hidden', 'true')
  })

  test('applies correct clamp styles to first part', () => {
    const { container } = render(<Truncate>filename.txt</Truncate>)

    const firstPart = container.querySelector('[data-pkt-truncate-part="first"]')
    expect(firstPart).toHaveStyle({
      display: '-webkit-box',
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      whiteSpace: 'normal',
      overflowWrap: 'anywhere',
    })
    expect(firstPart).toHaveStyle('-webkit-line-clamp: 2')
  })

  test('applies correct styles to tail span', () => {
    const { container } = render(<Truncate>very-long-filename.txt</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveStyle({
      whiteSpace: 'nowrap',
      paddingRight: '0.5rem',
    })
  })

  test('handles empty string', () => {
    const { container } = render(<Truncate>{''}</Truncate>)

    const firstPart = container.querySelector('[data-pkt-truncate-part="first"]')
    expect(firstPart).toHaveTextContent('')
  })

  test('handles string exactly equal to tail + 3', () => {
    const text = '12345678' // 8 characters, tail=5, so 5+3=8
    const { container } = render(<Truncate tail={5}>{text}</Truncate>)

    const tailSpan = container.querySelector('[data-pkt-truncate-part="tail"]')
    expect(tailSpan).toHaveTextContent('') // secondPart should be null
  })

  describe('accessibility', () => {
    test('renders with no wcag errors', async () => {
      const { container } = render(<Truncate>filename-with-accessibility.txt</Truncate>)
      const results = await axe(container)

      expect(results).toHaveNoViolations()
    })

    test('renders with no wcag errors when overflowing', async () => {
      const { container } = render(
        <Truncate>very-long-filename-that-definitely-overflows-the-container.txt</Truncate>,
      )

      const wrapper = container.querySelector('[data-pkt-truncate-part="wrapper"]') as HTMLElement
      const measure = container.querySelector('[data-pkt-truncate-part="measure"]') as HTMLElement
      measure.getBoundingClientRect = () => ({ height: 40 } as any)
      act(() => triggerResize(wrapper))

      await waitFor(async () => {
        const results = await axe(container)
        expect(results).toHaveNoViolations()
      })
    })
  })
})
