import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
import { render } from '@testing-library/react'
import { EchartUI } from './echart-ui'
import type { EChartsOption } from 'echarts'
import * as echarts from 'echarts'
import { resetSharedResizeObserver } from './shared-resize-observer'

// Mock echarts module
vi.mock('echarts', () => {
  const mockChart = {
    setOption: vi.fn(),
    dispose: vi.fn(),
    resize: vi.fn(),
    on: vi.fn(),
    off: vi.fn(),
    getZr: vi.fn(() => ({
      setCursorStyle: vi.fn(),
    })),
  }

  return {
    init: vi.fn(() => mockChart),
  }
})

describe('EchartUI', () => {
  let mockChart: {
    setOption: ReturnType<typeof vi.fn>
    dispose: ReturnType<typeof vi.fn>
    resize: ReturnType<typeof vi.fn>
    on: ReturnType<typeof vi.fn>
    off: ReturnType<typeof vi.fn>
    getZr: ReturnType<typeof vi.fn>
  }
  let mockResizeObserver: {
    observe: ReturnType<typeof vi.fn>
    disconnect: ReturnType<typeof vi.fn>
    unobserve: ReturnType<typeof vi.fn>
  }
  let resizeObserverCallback: ResizeObserverCallback | undefined

  const defaultProps = {
    id: 'test-echart',
  }

  const basicOption: EChartsOption = {
    title: {
      text: 'Test Chart',
    },
    series: [
      {
        type: 'bar',
        data: [1, 2, 3, 4, 5],
      },
    ],
  }

  beforeEach(() => {
    vi.clearAllMocks()

    // Create a fresh mock chart instance for each test
    mockChart = {
      setOption: vi.fn(),
      dispose: vi.fn(),
      resize: vi.fn(),
      on: vi.fn(),
      off: vi.fn(),
      getZr: vi.fn(() => ({
        setCursorStyle: vi.fn(),
      })),
    }

    // Make echarts.init return our mock chart
    vi.mocked(echarts.init).mockReturnValue(
      mockChart as unknown as echarts.ECharts,
    )

    // Reset the shared observer so each test gets a fresh one using the mock
    resetSharedResizeObserver()

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

    resizeObserverCallback = undefined

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

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

  test('renders div container', () => {
    const { container } = render(
      <EchartUI {...defaultProps} option={basicOption} />,
    )
    const div = container.querySelector('div')
    expect(div).toBeTruthy()
  })

  test('initializes echarts with default options', () => {
    render(<EchartUI {...defaultProps} option={basicOption} />)

    expect(echarts.init).toHaveBeenCalledWith(
      expect.any(HTMLDivElement),
      null,
      expect.objectContaining({
        renderer: 'svg',
        height: 304,
      }),
    )
  })

  test('initializes echarts with custom renderer', () => {
    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        init={{ renderer: 'canvas' }}
      />,
    )

    expect(echarts.init).toHaveBeenCalledWith(
      expect.any(HTMLDivElement),
      null,
      expect.objectContaining({
        renderer: 'canvas',
        height: 304,
      }),
    )
  })

  test('initializes echarts with custom height', () => {
    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        init={{ height: 500 }}
      />,
    )

    expect(echarts.init).toHaveBeenCalledWith(
      expect.any(HTMLDivElement),
      null,
      expect.objectContaining({
        renderer: 'svg',
        height: 500,
      }),
    )
  })

  test('initializes echarts with custom init options', () => {
    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        init={{ renderer: 'canvas', height: 600, width: 800 }}
      />,
    )

    expect(echarts.init).toHaveBeenCalledWith(
      expect.any(HTMLDivElement),
      null,
      expect.objectContaining({
        renderer: 'canvas',
        height: 600,
        width: 800,
      }),
    )
  })

  test('sets chart options on mount', () => {
    render(<EchartUI {...defaultProps} option={basicOption} />)

    expect(mockChart.setOption).toHaveBeenCalledWith(basicOption, {
      lazyUpdate: true,
      notMerge: true,
    })
  })

  test('updates chart options when option prop changes', () => {
    const { rerender } = render(
      <EchartUI {...defaultProps} option={basicOption} />,
    )

    const newOption: EChartsOption = {
      title: {
        text: 'Updated Chart',
      },
      series: [
        {
          type: 'line',
          data: [5, 4, 3, 2, 1],
        },
      ],
    }

    rerender(<EchartUI {...defaultProps} option={newOption} />)

    expect(mockChart.setOption).toHaveBeenCalledWith(newOption, {
      lazyUpdate: true,
      notMerge: true,
    })
  })

  test('applies custom className', () => {
    const { container } = render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        className='custom-chart'
      />,
    )
    const div = container.querySelector('div')
    expect(div?.className).toBe('custom-chart')
  })

  test('applies custom style', () => {
    const customStyle = { width: '100%', height: '400px' }
    const { container } = render(
      <EchartUI {...defaultProps} option={basicOption} style={customStyle} />,
    )
    const div = container.querySelector('div')!
    expect(div?.style.width).toBe('100%')
    expect(div?.style.height).toBe('400px')
  })

  test('disposes chart instance on unmount', () => {
    const { unmount } = render(
      <EchartUI {...defaultProps} option={basicOption} />,
    )

    unmount()

    expect(mockChart.dispose).toHaveBeenCalled()
  })

  test('sets up ResizeObserver on mount', () => {
    render(<EchartUI {...defaultProps} option={basicOption} />)

    // Verify ResizeObserver was used by checking observe was called
    expect(mockResizeObserver.observe).toHaveBeenCalledWith(
      expect.any(HTMLDivElement),
    )
  })

  test('unobserves element from shared ResizeObserver on unmount', () => {
    const { unmount } = render(
      <EchartUI {...defaultProps} option={basicOption} />,
    )

    unmount()

    // The shared observer calls unobserve() for the specific element, not disconnect()
    expect(mockResizeObserver.unobserve).toHaveBeenCalled()
  })

  test('calls resize on chart when ResizeObserver triggers', () => {
    render(<EchartUI {...defaultProps} option={basicOption} />)

    // Trigger the shared observer's callback with a mock entry targeting the chart element
    const chartElement = document.getElementById(defaultProps.id)!
    const entries: ResizeObserverEntry[] = [
      { target: chartElement } as unknown as ResizeObserverEntry,
    ]
    resizeObserverCallback!(entries, {} as ResizeObserver)

    expect(mockChart.resize).toHaveBeenCalled()
  })

  test('accepts ref prop without errors', () => {
    const chartRef = {
      current: null,
    } as unknown as React.RefObject<echarts.ECharts>

    // Component accepts ref as a prop but since it doesn't use forwardRef,
    // the useImperativeHandle won't properly set the ref.
    // This test verifies the component doesn't crash when ref is provided.
    expect(() => {
      render(<EchartUI {...defaultProps} ref={chartRef} option={basicOption} />)
    }).not.toThrow()
  })

  test('attaches event listeners when onEvents prop is provided', () => {
    const clickHandler = vi.fn()
    const hoverHandler = vi.fn()

    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        onEvents={{
          click: clickHandler,
          mouseover: hoverHandler,
        }}
      />,
    )

    expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler)
    expect(mockChart.on).toHaveBeenCalledWith('mouseover', hoverHandler)
  })

  test('updates event listeners when onEvents prop changes', () => {
    const clickHandler1 = vi.fn()
    const clickHandler2 = vi.fn()

    const { rerender } = render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        onEvents={{
          click: clickHandler1,
        }}
      />,
    )

    expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler1)

    rerender(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        onEvents={{
          click: clickHandler2,
        }}
      />,
    )

    expect(mockChart.off).toHaveBeenCalledWith('click')
    expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler2)
  })

  test('sets default cursor style when no click event is provided', () => {
    const mockGetZr = vi.fn(() => ({
      setCursorStyle: vi.fn(),
    }))
    mockChart.getZr = mockGetZr

    render(<EchartUI {...defaultProps} option={basicOption} />)

    expect(mockChart.on).toHaveBeenCalledWith('mousemove', expect.any(Function))
  })

  test('does not set default cursor style when click event is provided', () => {
    const clickHandler = vi.fn()

    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        onEvents={{
          click: clickHandler,
        }}
      />,
    )

    // Should not attach mousemove handler when click is provided
    const mousemoveCalls = mockChart.on.mock.calls.filter(
      (call) => call[0] === 'mousemove',
    )

    expect(mousemoveCalls.length).toBe(0)
  })

  test('handles multiple event types simultaneously', () => {
    const clickHandler = vi.fn()
    const dblclickHandler = vi.fn()
    const mouseoverHandler = vi.fn()
    const mouseoutHandler = vi.fn()

    render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        onEvents={{
          click: clickHandler,
          dblclick: dblclickHandler,
          mouseover: mouseoverHandler,
          mouseout: mouseoutHandler,
        }}
      />,
    )

    expect(mockChart.on).toHaveBeenCalledWith('click', clickHandler)
    expect(mockChart.on).toHaveBeenCalledWith('dblclick', dblclickHandler)
    expect(mockChart.on).toHaveBeenCalledWith('mouseover', mouseoverHandler)
    expect(mockChart.on).toHaveBeenCalledWith('mouseout', mouseoutHandler)
  })

  test('reinitializes chart when init options change', () => {
    vi.clearAllMocks() // Clear mocks from beforeEach

    const { rerender } = render(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        init={{ height: 300 }}
      />,
    )

    expect(echarts.init).toHaveBeenCalledTimes(1)
    expect(mockChart.dispose).not.toHaveBeenCalled()

    rerender(
      <EchartUI
        {...defaultProps}
        option={basicOption}
        init={{ height: 400 }}
      />,
    )

    expect(mockChart.dispose).toHaveBeenCalled()
    expect(echarts.init).toHaveBeenCalledTimes(2)
  })

  test('handles empty onEvents object', () => {
    render(<EchartUI {...defaultProps} option={basicOption} onEvents={{}} />)

    // Should still set default mousemove handler
    expect(mockChart.on).toHaveBeenCalledWith('mousemove', expect.any(Function))
  })

  test('generates unique id for container', () => {
    const { container: container1 } = render(
      <EchartUI {...defaultProps} id='test-echart-1' option={basicOption} />,
    )
    const { container: container2 } = render(
      <EchartUI {...defaultProps} id='test-echart-2' option={basicOption} />,
    )

    const div1 = container1.querySelector('div')
    const div2 = container2.querySelector('div')

    expect(div1?.id).toBe('test-echart-1')
    expect(div2?.id).toBe('test-echart-2')
    expect(div1?.id).not.toBe(div2?.id)
  })

  test('handles complex chart options', () => {
    const complexOption: EChartsOption = {
      title: {
        text: 'Complex Chart',
        left: 'center',
      },
      tooltip: {
        trigger: 'axis',
      },
      legend: {
        data: ['Series 1', 'Series 2'],
        bottom: 10,
      },
      xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
      },
      yAxis: {
        type: 'value',
      },
      series: [
        {
          name: 'Series 1',
          type: 'line',
          data: [120, 200, 150, 80, 70, 110, 130],
        },
        {
          name: 'Series 2',
          type: 'bar',
          data: [220, 180, 190, 234, 290, 330, 310],
        },
      ],
    }

    render(<EchartUI {...defaultProps} option={complexOption} />)

    expect(mockChart.setOption).toHaveBeenCalledWith(complexOption, {
      lazyUpdate: true,
      notMerge: true,
    })
  })

  test('does not break when ref is not provided', () => {
    expect(() => {
      render(<EchartUI {...defaultProps} option={basicOption} />)
    }).not.toThrow()
  })

  test('handles undefined onEvents gracefully', () => {
    render(
      <EchartUI {...defaultProps} option={basicOption} onEvents={undefined} />,
    )

    expect(mockChart.on).toHaveBeenCalledWith('mousemove', expect.any(Function))
  })

  test('cleans up properly with multiple rerenders', () => {
    const { rerender, unmount } = render(
      <EchartUI {...defaultProps} option={basicOption} />,
    )

    const option2: EChartsOption = {
      series: [{ type: 'line', data: [1, 2, 3] }],
    }
    rerender(<EchartUI {...defaultProps} option={option2} />)

    const option3: EChartsOption = {
      series: [{ type: 'bar', data: [3, 2, 1] }],
    }
    rerender(<EchartUI {...defaultProps} option={option3} />)

    unmount()

    expect(mockChart.dispose).toHaveBeenCalled()
    expect(mockResizeObserver.unobserve).toHaveBeenCalled()
  })
})
