import React from 'react'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'

import {
  fireEvent,
  renderWithProviders,
  screen,
  waitFor,
} from '../../test/utils'

import MediaPlayer from './MediaPlayer'

const SOURCE = 'https://cdn.example.com/preview.mp4'
const POSTER = 'https://cdn.example.com/poster.jpg'

describe('MediaPlayer', () => {
  beforeEach(() => {
    vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined)
    vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
    vi.stubGlobal(
      'requestAnimationFrame',
      vi.fn(() => 1)
    )
    vi.stubGlobal('cancelAnimationFrame', vi.fn())
  })

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

  it('selects audio or video from the mime type and wires media props', () => {
    const { container, rerender } = renderWithProviders(
      <MediaPlayer
        source={SOURCE}
        mimeType="audio/mpeg"
        loop
        muted
        controls={false}
      />
    )

    const audio = container.querySelector('audio') as HTMLAudioElement
    expect(audio).toBeInTheDocument()
    expect(audio).toHaveAttribute('src', SOURCE)
    expect(audio.loop).toBe(true)
    expect(audio.muted).toBe(true)
    expect(container.querySelector('video')).toBeNull()

    rerender(
      <MediaPlayer
        source={SOURCE}
        mimeType="video/mp4"
        loop
        muted
        controls={false}
      />
    )

    const video = container.querySelector('video') as HTMLVideoElement
    expect(video).toBeInTheDocument()
    expect(video).toHaveAttribute('src', SOURCE)
    expect(video.loop).toBe(true)
    expect(video.muted).toBe(true)
    expect(video.playsInline).toBe(true)
    expect(container.querySelector('audio')).toBeNull()
  })

  it('keeps an audio poster after canPlay but hides a video poster', () => {
    const audio = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="audio/mpeg" poster={POSTER} />
    )
    const audioElement = audio.container.querySelector(
      'audio'
    ) as HTMLAudioElement
    expect(audio.container.querySelectorAll('img')).toHaveLength(1)
    fireEvent.canPlay(audioElement)
    expect(audio.container.querySelectorAll('img')).toHaveLength(1)
    audio.unmount()

    const video = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" poster={POSTER} />
    )
    const videoElement = video.container.querySelector(
      'video'
    ) as HTMLVideoElement
    expect(video.container.querySelectorAll('img')).toHaveLength(1)
    fireEvent.canPlay(videoElement)
    expect(video.container.querySelectorAll('img')).toHaveLength(0)
  })

  it('uses a type icon as the initial placeholder when no poster is provided', async () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" />
    )
    const video = container.querySelector('video') as HTMLVideoElement
    const placeholderIcon = () =>
      Array.from(container.querySelectorAll('svg')).find((svg) =>
        svg.classList.contains('size-12')
      )

    expect(placeholderIcon()).toBeInTheDocument()
    fireEvent.canPlay(video)
    await waitFor(() => expect(placeholderIcon()).toBeUndefined())
  })

  it('syncs controlled playback and calls play or pause imperatively', async () => {
    const play = vi.mocked(HTMLMediaElement.prototype.play)
    const pause = vi.mocked(HTMLMediaElement.prototype.pause)
    const { container, rerender } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
    )
    const video = container.querySelector('video') as HTMLVideoElement
    expect(pause).toHaveBeenCalled()

    rerender(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" playing={true} />
    )
    await waitFor(() => expect(play).toHaveBeenCalledWith())
    expect(video).toBeInTheDocument()

    rerender(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" playing={false} />
    )
    await waitFor(() => expect(pause.mock.calls.length).toBeGreaterThan(1))
  })

  it('shows a manual play overlay after rejected playback and starts from a gesture', async () => {
    const play = vi.mocked(HTMLMediaElement.prototype.play)
    play.mockRejectedValueOnce(new Error('autoplay blocked'))
    const onContainerClick = vi.fn()

    renderWithProviders(
      <MediaPlayer
        source={SOURCE}
        mimeType="video/mp4"
        autoPlay
        controls={false}
        onContainerClick={onContainerClick}
      />
    )

    const overlay = await screen.findByRole('button', { name: 'Play preview' })
    expect(play).toHaveBeenCalledTimes(1)
    fireEvent.click(overlay)
    await waitFor(() => expect(play).toHaveBeenCalledTimes(2))
    expect(onContainerClick).not.toHaveBeenCalled()
    expect(screen.queryByRole('button', { name: 'Play preview' })).toBeNull()
  })

  it('toggles from the container only with controls, while an override owns the click', async () => {
    const play = vi.mocked(HTMLMediaElement.prototype.play)
    const { container, rerender } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
    )
    const root = container.firstElementChild as HTMLElement

    fireEvent.click(root)
    await waitFor(() =>
      expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
    )
    fireEvent.click(root)
    await waitFor(() =>
      expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
    )
    expect(play).toHaveBeenCalledTimes(1)

    const onContainerClick = vi.fn()
    rerender(
      <MediaPlayer
        source={SOURCE}
        mimeType="video/mp4"
        controls
        onContainerClick={onContainerClick}
      />
    )
    fireEvent.click(root)
    expect(onContainerClick).toHaveBeenCalledTimes(1)
    expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
    fireEvent.keyDown(root, { key: 'Enter' })
    expect(onContainerClick).toHaveBeenCalledTimes(2)
  })

  it('handles Enter and Space but ignores other container keys and prevents defaults', () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
    )
    const root = container.firstElementChild as HTMLElement
    const pause = () => screen.getByRole('button', { name: 'Pause' })

    const otherKey = new KeyboardEvent('keydown', {
      key: 'ArrowDown',
      bubbles: true,
      cancelable: true,
    })
    fireEvent(root, otherKey)
    expect(otherKey.defaultPrevented).toBe(false)
    expect(screen.queryByRole('button', { name: 'Pause' })).toBeNull()

    const enter = new KeyboardEvent('keydown', {
      key: 'Enter',
      bubbles: true,
      cancelable: true,
    })
    fireEvent(root, enter)
    expect(enter.defaultPrevented).toBe(true)
    expect(pause()).toBeInTheDocument()

    const space = new KeyboardEvent('keydown', {
      key: ' ',
      bubbles: true,
      cancelable: true,
    })
    fireEvent(root, space)
    expect(space.defaultPrevented).toBe(true)
    expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
  })

  it('does not let the controls button bubble into the container toggle', async () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" controls />
    )
    const button = screen.getByRole('button', { name: 'Play' })

    fireEvent.click(button)
    await waitFor(() =>
      expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
    )
    expect(container.firstElementChild).toBeInTheDocument()
  })

  it('shows buffering while loading and suppresses it after manual play is required', async () => {
    const play = vi.mocked(HTMLMediaElement.prototype.play)
    play.mockRejectedValueOnce(new Error('autoplay blocked'))
    const { container } = renderWithProviders(
      <MediaPlayer
        source={SOURCE}
        mimeType="video/mp4"
        autoPlay
        controls={false}
      />
    )
    const video = container.querySelector('video') as HTMLVideoElement
    await screen.findByRole('button', { name: 'Play preview' })

    fireEvent.loadStart(video)
    expect(container.querySelector('.animate-spin')).toBeNull()

    fireEvent.canPlay(video)
    expect(container.querySelector('.animate-spin')).toBeNull()
  })

  it('shows and hides the buffering spinner around media readiness', () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" />
    )
    const video = container.querySelector('video') as HTMLVideoElement

    fireEvent.loadStart(video)
    expect(container.querySelector('.animate-spin')).toBeInTheDocument()
    fireEvent.waiting(video)
    expect(container.querySelector('.animate-spin')).toBeInTheDocument()
    fireEvent.canPlay(video)
    expect(container.querySelector('.animate-spin')).toBeNull()
  })

  it('seeks with keyboard and pointer input and expands the scrubber on hover', () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" controls showProgress />
    )
    const video = container.querySelector('video') as HTMLVideoElement
    Object.defineProperty(video, 'duration', {
      configurable: true,
      value: 100,
    })
    const slider = screen.getByRole('slider', { name: 'Playback position' })
    const track = slider.firstElementChild as HTMLElement
    const thumb = slider.lastElementChild as HTMLElement
    vi.spyOn(slider, 'getBoundingClientRect').mockReturnValue({
      left: 100,
      right: 300,
      top: 0,
      bottom: 10,
      width: 200,
      height: 10,
      x: 100,
      y: 0,
      toJSON: () => ({}),
    })

    expect(track).toHaveClass('h-1')
    expect(thumb).toHaveClass('scale-0', 'opacity-0')
    fireEvent.mouseEnter(slider)
    expect(track).toHaveClass('h-1.5')
    expect(thumb).toHaveClass('scale-100', 'opacity-100')
    fireEvent.mouseLeave(slider)
    expect(track).toHaveClass('h-1')

    fireEvent.mouseDown(slider, { clientX: 150 })
    expect(slider).toHaveAttribute('aria-valuenow', '25')
    expect(video.currentTime).toBe(25)
    expect(track).toHaveClass('h-1.5')
    expect(thumb).toHaveClass('scale-100', 'opacity-100')

    fireEvent.mouseUp(window, { clientX: 150 })
    fireEvent.keyDown(slider, { key: 'ArrowRight' })
    expect(video.currentTime).toBe(30)
    fireEvent.keyDown(slider, { key: 'ArrowLeft' })
    expect(video.currentTime).toBe(20)
  })

  it('renders the progress-only track without controls when requested', () => {
    const { container } = renderWithProviders(
      <MediaPlayer
        source={SOURCE}
        mimeType="video/mp4"
        controls={false}
        showProgress
      />
    )

    expect(
      screen.getByRole('slider', { name: 'Playback position' })
    ).toBeInTheDocument()
    expect(screen.queryByRole('button', { name: 'Play' })).toBeNull()
    expect(container.querySelectorAll('[role="slider"]')).toHaveLength(1)
  })

  it('uses natural video dimensions after metadata and aspect-video before then', () => {
    const { container } = renderWithProviders(
      <MediaPlayer source={SOURCE} mimeType="video/mp4" />
    )
    const root = container.firstElementChild as HTMLElement
    const video = container.querySelector('video') as HTMLVideoElement
    Object.defineProperties(video, {
      videoWidth: { configurable: true, value: 1920 },
      videoHeight: { configurable: true, value: 1080 },
    })

    expect(root).toHaveClass('aspect-video')
    expect(root).not.toHaveStyle({ aspectRatio: '1.7777777777777777' })
    fireEvent.loadedMetadata(video)
    expect(root).not.toHaveClass('aspect-video')
    expect(root.style.aspectRatio).toBe(String(1920 / 1080))
  })
})
