import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { WidgetNote } from './note'

describe('WidgetNote', () => {
  let mockResizeObserver: {
    observe: ReturnType<typeof vi.fn>
    disconnect: ReturnType<typeof vi.fn>
    unobserve: ReturnType<typeof vi.fn>
  }

  beforeEach(() => {
    // Mock ResizeObserver
    mockResizeObserver = {
      observe: vi.fn(),
      disconnect: vi.fn(),
      unobserve: vi.fn(),
    }

    global.ResizeObserver = class {
      observe = mockResizeObserver.observe
      disconnect = mockResizeObserver.disconnect
      unobserve = mockResizeObserver.unobserve
    } as unknown as typeof ResizeObserver

    // Default: no overflow
    vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
    vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  test('returns null when children is undefined', () => {
    // @ts-expect-error - testing runtime behavior with undefined
    const { container } = render(<WidgetNote>{undefined}</WidgetNote>)
    expect(container.firstChild).toBeNull()
  })

  test('returns null when children is empty string', () => {
    const { container } = render(<WidgetNote>{''}</WidgetNote>)
    expect(container.firstChild).toBeNull()
  })

  test('returns null when children is null', () => {
    // @ts-expect-error - testing runtime behavior with null
    const { container } = render(<WidgetNote>{null}</WidgetNote>)
    expect(container.firstChild).toBeNull()
  })

  test('renders Markdown component when children exists', () => {
    render(<WidgetNote>Simple note text</WidgetNote>)
    expect(screen.getByText('Simple note text')).toBeTruthy()
  })

  test('renders markdown-formatted note content', () => {
    render(<WidgetNote>{'# Heading\n\nParagraph text'}</WidgetNote>)
    expect(screen.getByText('Heading')).toBeTruthy()
    expect(screen.getByText('Paragraph text')).toBeTruthy()
  })

  test('renders note with list items', () => {
    render(<WidgetNote>{'- Item 1\n- Item 2\n- Item 3'}</WidgetNote>)
    expect(screen.getByText('Item 1')).toBeTruthy()
    expect(screen.getByText('Item 2')).toBeTruthy()
    expect(screen.getByText('Item 3')).toBeTruthy()
  })

  test('renders note with links', () => {
    render(
      <WidgetNote>{'Check out [this link](https://example.com)'}</WidgetNote>,
    )
    const link = screen.getByText('this link')
    expect(link.tagName).toBe('A')
    expect(link.getAttribute('href')).toBe('https://example.com')
  })

  test('renders note with complex markdown', () => {
    render(
      <WidgetNote>{`# Title

## Subtitle

Paragraph with **bold** and *italic*.

- List item 1
- List item 2

[Link](https://example.com)`}</WidgetNote>,
    )
    expect(screen.getByText('Title')).toBeTruthy()
    expect(screen.getByText('Subtitle')).toBeTruthy()
    expect(screen.getByText('bold')).toBeTruthy()
    expect(screen.getByText('italic')).toBeTruthy()
    expect(screen.getByText('List item 1')).toBeTruthy()
    expect(screen.getByText('List item 2')).toBeTruthy()
    expect(screen.getByText('Link')).toBeTruthy()
  })

  test('renders note with bold text', () => {
    render(<WidgetNote>{'This is **bold** text'}</WidgetNote>)
    expect(screen.getByText('bold')).toBeTruthy()
  })

  test('renders note with italic text', () => {
    render(<WidgetNote>{'This is *italic* text'}</WidgetNote>)
    expect(screen.getByText('italic')).toBeTruthy()
  })

  test('renders note with code blocks', () => {
    render(<WidgetNote>{'Here is `inline code`'}</WidgetNote>)
    expect(screen.getByText('inline code')).toBeTruthy()
  })

  test('renders note with blockquote', () => {
    render(<WidgetNote>{'> This is a quote'}</WidgetNote>)
    expect(screen.getByText('This is a quote')).toBeTruthy()
  })

  test('uses default labels', () => {
    // Mock overflow to show toggle button
    vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
    vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

    render(<WidgetNote>{'Long content\n'.repeat(10)}</WidgetNote>)

    expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
  })

  test('uses custom labels', async () => {
    const user = userEvent.setup()

    // Mock overflow to show toggle button
    vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
    vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

    render(
      <WidgetNote labels={{ showMore: 'Expand', showLess: 'Collapse' }}>
        {'Long content\n'.repeat(10)}
      </WidgetNote>,
    )

    const button = screen.getByRole('button', { name: 'Expand' })
    expect(button).toBeTruthy()

    await user.click(button)

    await waitFor(() => {
      expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
    })
  })

  test('merges custom labels with defaults', async () => {
    const user = userEvent.setup()

    // Mock overflow to show toggle button
    vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(100)
    vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

    render(
      <WidgetNote labels={{ showMore: 'Expand' }}>
        {'Long content\n'.repeat(10)}
      </WidgetNote>,
    )

    const button = await screen.findByRole('button', { name: 'Expand' })
    expect(button).toBeTruthy()

    await user.click(button)

    await waitFor(() => {
      // showLess should use default since not provided
      expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
    })
  })

  describe('Expand/Collapse functionality', () => {
    test('does not show toggle button when content does not overflow', () => {
      // Content fits within container (no overflow)
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>Short content</WidgetNote>)

      expect(screen.queryByRole('button')).toBeNull()
    })

    test('shows toggle button when content overflows', async () => {
      // Content overflows container
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
      })
    })

    test('toggles expansion state on button click', async () => {
      const user = userEvent.setup()

      // Content overflows container
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button', { name: 'Show More' })
      expect(button.getAttribute('aria-expanded')).toBe('false')

      await user.click(button)

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
        expect(
          screen
            .getByRole('button', { name: 'Show Less' })
            .getAttribute('aria-expanded'),
        ).toBe('true')
      })
    })

    test('collapses content on second button click', async () => {
      const user = userEvent.setup()

      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button', { name: 'Show More' })

      // Expand
      await user.click(button)

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show Less' })).toBeTruthy()
      })

      // Collapse
      await user.click(screen.getByRole('button', { name: 'Show Less' }))

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
        expect(
          screen
            .getByRole('button', { name: 'Show More' })
            .getAttribute('aria-expanded'),
        ).toBe('false')
      })
    })

    test('sets up ResizeObserver on mount', () => {
      render(<WidgetNote>Test content</WidgetNote>)

      // Verify ResizeObserver was used by checking observe was called
      expect(mockResizeObserver.observe).toHaveBeenCalled()
    })

    test('disconnects ResizeObserver on unmount', () => {
      const { unmount } = render(<WidgetNote>Test content</WidgetNote>)

      unmount()

      expect(mockResizeObserver.disconnect).toHaveBeenCalled()
    })

    test('recalculates overflow when ResizeObserver triggers', async () => {
      let resizeCallback: ResizeObserverCallback

      global.ResizeObserver = class {
        constructor(callback: ResizeObserverCallback) {
          resizeCallback = callback
        }
        observe = mockResizeObserver.observe
        disconnect = mockResizeObserver.disconnect
        unobserve = mockResizeObserver.unobserve
      } as unknown as typeof ResizeObserver

      // Start with no overflow
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(60)
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>Test content</WidgetNote>)

      expect(screen.queryByRole('button')).toBeNull()

      // Simulate overflow after resize
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )

      // Trigger resize callback
      resizeCallback!([], mockResizeObserver as unknown as ResizeObserver)

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
      })
    })

    test('maintains expanded state correctly after multiple toggles', async () => {
      const user = userEvent.setup()

      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button')

      // Toggle multiple times
      await user.click(button) // Expand
      await user.click(button) // Collapse
      await user.click(button) // Expand
      await user.click(button) // Collapse

      await waitFor(() => {
        expect(screen.getByRole('button', { name: 'Show More' })).toBeTruthy()
        expect(button.getAttribute('aria-expanded')).toBe('false')
      })
    })
  })

  describe('Accessibility', () => {
    test('button has aria-expanded attribute', async () => {
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button')
      expect(button.getAttribute('aria-expanded')).toBe('false')
    })

    test('button has aria-controls attribute', async () => {
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button')
      expect(button.getAttribute('aria-controls')).toBe('note-content')
    })

    test('button has aria-label for Show More', async () => {
      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button', { name: 'Show More' })
      expect(button.getAttribute('aria-label')).toBe('Show More')
    })

    test('button has aria-label for Show Less when expanded', async () => {
      const user = userEvent.setup()

      vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockReturnValue(
        100,
      )
      vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockReturnValue(60)

      render(<WidgetNote>{'Long content\n\n'.repeat(10)}</WidgetNote>)

      const button = await screen.findByRole('button')
      await user.click(button)

      await waitFor(() => {
        const expandedButton = screen.getByRole('button', { name: 'Show Less' })
        expect(expandedButton.getAttribute('aria-label')).toBe('Show Less')
      })
    })
  })
})
