import { describe, it, expect, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import PullToRefresh from '@/components/PullToRefresh.vue'

function touchEvent(type: string, x: number, y: number) {
  const e = new Event(type, { bubbles: true, cancelable: true }) as TouchEvent
  Object.defineProperty(e, 'touches', { value: [{ clientX: x, clientY: y }] })
  Object.defineProperty(e, 'changedTouches', { value: [{ clientX: x, clientY: y }] })
  return e
}

describe('PullToRefresh', () => {
  it('renders an arrow icon by default and the slot content', () => {
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh: () => Promise.resolve() },
      slots: { default: '<p class="content">hello</p>' },
    })
    expect(wrapper.find('.ptr__arrow').exists()).toBe(true)
    expect(wrapper.find('.ptr__spinner').exists()).toBe(false)
    expect(wrapper.find('.content').exists()).toBe(true)
  })

  it('triggers onRefresh when the user drags past the threshold and releases', async () => {
    const onRefresh = vi.fn(() => Promise.resolve())
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 50 },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  100, 250)) // dy=150, damped=75 (>50)
    root.dispatchEvent(touchEvent('touchend',   100, 250))
    await flushPromises()
    expect(onRefresh).toHaveBeenCalledTimes(1)
  })

  it('does not trigger onRefresh when the drag is below the threshold', async () => {
    const onRefresh = vi.fn(() => Promise.resolve())
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 80 },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  100, 140)) // dy=40, damped=20
    root.dispatchEvent(touchEvent('touchend',   100, 140))
    await flushPromises()
    expect(onRefresh).not.toHaveBeenCalled()
  })

  it('does not trigger onRefresh when isAtTop returns false', async () => {
    const onRefresh = vi.fn(() => Promise.resolve())
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 50, isAtTop: () => false },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  100, 300))
    root.dispatchEvent(touchEvent('touchend',   100, 300))
    await flushPromises()
    expect(onRefresh).not.toHaveBeenCalled()
  })

  it('bails when the user swipes horizontally instead of vertically', async () => {
    const onRefresh = vi.fn(() => Promise.resolve())
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 50 },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  300, 105)) // big dx, tiny dy
    root.dispatchEvent(touchEvent('touchmove',  300, 200)) // user keeps moving — should still bail
    root.dispatchEvent(touchEvent('touchend',   300, 200))
    await flushPromises()
    expect(onRefresh).not.toHaveBeenCalled()
  })

  it('shows the spinner while onRefresh is pending and clears it when resolved', async () => {
    let resolveRefresh!: () => void
    const onRefresh = vi.fn(() => new Promise<void>(r => { resolveRefresh = r }))
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 50 },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  100, 250))
    root.dispatchEvent(touchEvent('touchend',   100, 250))
    await wrapper.vm.$nextTick()
    expect(wrapper.find('.ptr__spinner').exists()).toBe(true)
    expect(wrapper.find('.ptr__arrow').exists()).toBe(false)

    resolveRefresh()
    await flushPromises()
    expect(wrapper.find('.ptr__spinner').exists()).toBe(false)
    expect(wrapper.find('.ptr__arrow').exists()).toBe(true)
  })

  it('keeps the spinner up while onRefresh rejects, then clears it', async () => {
    let rejectRefresh!: (e: unknown) => void
    const onRefresh = vi.fn(() => new Promise<void>((_, rej) => { rejectRefresh = rej }))
    const wrapper = mount(PullToRefresh, {
      props: { onRefresh, threshold: 50 },
    })
    const root = wrapper.find('.ptr').element as HTMLElement
    root.dispatchEvent(touchEvent('touchstart', 100, 100))
    root.dispatchEvent(touchEvent('touchmove',  100, 250))
    root.dispatchEvent(touchEvent('touchend',   100, 250))
    await wrapper.vm.$nextTick()
    expect(wrapper.find('.ptr__spinner').exists()).toBe(true)

    rejectRefresh(new Error('boom'))
    await flushPromises()
    expect(wrapper.find('.ptr__spinner').exists()).toBe(false)
  })
})
