import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import { nextTick } from 'vue'
import ScrollingList from '../ScrollingList.vue'

// Mock image cache
vi.mock('@/common/imageCache', () => ({
  getCachedImageUrl: vi.fn((id: string) => Promise.resolve(`/cached/${id}.png`)),
  revokeAllImageUrls: vi.fn(),
}))

global.fetch = vi.fn()

describe('ScrollingList', () => {
  let wrapper: VueWrapper<any>

  const defaultProps = {
    title: 'Test List',
    fetchBaseUrl: '/api/test',
    ids: ['item-1', 'item-2'],
    itemKey: 'items',
  }

  const mockItems = [
    { id: 'item-1', name: 'Item One', points: 10, image_id: 'img1' },
    { id: 'item-2', name: 'Item Two', points: 20, image_id: 'img2' },
  ]

  const mockItemsWithOverride = [
    { id: 'item-1', name: 'Item One', points: 10, image_id: 'img1', custom_value: 15 },
    { id: 'item-2', name: 'Item Two', points: 20, image_id: 'img2', custom_value: null },
  ]

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

    // Mock fetch responses
    ;(global.fetch as any).mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ items: mockItems }),
    })
  })

  afterEach(() => {
    if (wrapper) {
      wrapper.unmount()
    }
  })

  describe('Component Mounting', () => {
    it('renders with title', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()

      expect(wrapper.find('h3').text()).toBe('Test List')
    })

    it('fetches items on mount', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(global.fetch).toHaveBeenCalledWith('/api/test?ids=item-1,item-2')
    })

    it('displays loading state initially', () => {
      wrapper = mount(ScrollingList, { props: defaultProps })

      expect(wrapper.find('.loading').exists()).toBe(true)
    })

    it('displays items after loading', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.find('.loading').exists()).toBe(false)
      expect(wrapper.findAll('.item-card').length).toBe(2)
    })

    it('displays empty message when no items', async () => {
      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: [] }),
      })

      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.find('.empty').text()).toContain('No Test List')
    })

    it('displays error message on fetch failure', async () => {
      ;(global.fetch as any).mockRejectedValue(new Error('Network error'))

      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.find('.error').exists()).toBe(true)
    })
  })

  describe('Override Badge Display', () => {
    it('shows override badge when custom_value exists and isParentAuthenticated is true', async () => {
      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: mockItemsWithOverride }),
      })

      wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const badges = wrapper.findAll('.override-badge')
      expect(badges.length).toBe(1) // Only item-1 has custom_value
      expect(badges[0].text()).toBe('⭐')
    })

    it('does not show badge when custom_value is null', async () => {
      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: mockItems }),
      })

      wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.override-badge').length).toBe(0)
    })

    it('does not show badge when custom_value is undefined', async () => {
      const itemsWithUndefined = [{ id: 'item-1', name: 'Item One', points: 10 }]

      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: itemsWithUndefined }),
      })

      wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.override-badge').length).toBe(0)
    })

    it('does not show badge in child mode even when custom_value exists', async () => {
      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: mockItemsWithOverride }),
      })

      wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: false } })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.override-badge').length).toBe(0)
    })

    it('does not show badge when isParentAuthenticated is undefined', async () => {
      ;(global.fetch as any).mockResolvedValue({
        ok: true,
        json: () => Promise.resolve({ items: mockItemsWithOverride }),
      })

      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.override-badge').length).toBe(0)
    })
  })

  describe('Two-Step Click Interaction', () => {
    beforeEach(async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: false, // No edit button for basic click test
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))
    })

    it('emits item-ready on first click', async () => {
      const cards = wrapper.findAll('.item-card')
      await cards[0].trigger('click')

      expect(wrapper.emitted('item-ready')).toBeTruthy()
      expect(wrapper.emitted('item-ready')![0]).toEqual(['item-1'])
    })

    it('emits trigger-item on second click of same item', async () => {
      const cards = wrapper.findAll('.item-card')

      // First click - select item
      await cards[0].trigger('click')
      await nextTick()

      // Update prop to simulate parent setting readyItemId
      await wrapper.setProps({ readyItemId: 'item-1' })
      await nextTick()

      // Second click - trigger item
      await cards[0].trigger('click')

      expect(wrapper.emitted('trigger-item')).toBeTruthy()
      expect(wrapper.emitted('trigger-item')![0][0].id).toBe('item-1')
    })

    it('resets ready state after triggering', async () => {
      const cards = wrapper.findAll('.item-card')

      // First click
      await cards[0].trigger('click')
      await wrapper.setProps({ readyItemId: 'item-1' })

      // Second click
      await cards[0].trigger('click')

      const itemReadyEvents = wrapper.emitted('item-ready')
      expect(itemReadyEvents![itemReadyEvents!.length - 1]).toEqual([''])
    })
  })

  describe('Edit Button Display', () => {
    it('shows edit button only for readyItemId when enableEdit is true', async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: true,
          readyItemId: 'item-1',
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const editButtons = wrapper.findAll('.edit-button')
      expect(editButtons.length).toBe(1)
    })

    it('does not show edit button when enableEdit is false', async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: false,
          readyItemId: 'item-1',
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.edit-button').length).toBe(0)
    })

    it('does not show edit button when readyItemId is null', async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: true,
          readyItemId: null,
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.edit-button').length).toBe(0)
    })

    it('emits edit-item when edit button is clicked', async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: true,
          readyItemId: 'item-1',
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const editButton = wrapper.find('.edit-button')
      await editButton.trigger('click')

      expect(wrapper.emitted('edit-item')).toBeTruthy()
      expect(wrapper.emitted('edit-item')![0][0].id).toBe('item-1')
    })

    it('resets ready state after edit button click', async () => {
      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          enableEdit: true,
          readyItemId: 'item-1',
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const editButton = wrapper.find('.edit-button')
      await editButton.trigger('click')

      const itemReadyEvents = wrapper.emitted('item-ready')
      expect(itemReadyEvents![itemReadyEvents!.length - 1]).toEqual([''])
    })
  })

  describe('Filter Function', () => {
    it('filters items using provided filterFn', async () => {
      const filterFn = (item: any) => item.points > 15

      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          filterFn,
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      expect(wrapper.findAll('.item-card').length).toBe(1) // Only item-2 with 20 points
    })
  })

  describe('Refresh Method', () => {
    it('exposes refresh method', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()

      expect(wrapper.vm.refresh).toBeDefined()
      expect(typeof wrapper.vm.refresh).toBe('function')
    })

    it('refetches items when refresh is called', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      vi.clearAllMocks()

      await wrapper.vm.refresh()

      expect(global.fetch).toHaveBeenCalledWith('/api/test?ids=item-1,item-2')
    })

    it('discards stale fetch when concurrent refreshes occur', async () => {
      const { getCachedImageUrl } = await import('@/common/imageCache')

      const staleItems = [{ id: 'stale-1', name: 'Stale', points: 1, image_id: 'stale-img' }]
      const freshItems = [{ id: 'fresh-1', name: 'Fresh', points: 2, image_id: 'fresh-img' }]

      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      // Set up two concurrent fetches: first one is slow, second one is fast
      let resolveFirst: (v: any) => void
      const firstFetchPromise = new Promise((r) => {
        resolveFirst = r
      })

      vi.clearAllMocks()
      ;(global.fetch as any)
        .mockReturnValueOnce(firstFetchPromise) // slow call
        .mockResolvedValueOnce({
          // fast call
          ok: true,
          json: () => Promise.resolve({ items: freshItems }),
        })

      // Start first refresh (slow)
      const first = wrapper.vm.refresh()
      // Start second refresh (fast) — should supersede the first
      const second = wrapper.vm.refresh()

      // Resolve the fast call first (already resolved via mockResolvedValueOnce)
      await second
      await nextTick()

      // Items should be from the fast (second) call
      expect(wrapper.vm.items).toHaveLength(1)
      expect(wrapper.vm.items[0].name).toBe('Fresh')

      // Now resolve the slow (stale) call
      resolveFirst!({ ok: true, json: () => Promise.resolve({ items: staleItems }) })
      await first
      await nextTick()

      // Items should still be from the fast (second) call — stale result discarded
      expect(wrapper.vm.items).toHaveLength(1)
      expect(wrapper.vm.items[0].name).toBe('Fresh')
      // Image was loaded for fresh item, not stale
      expect(getCachedImageUrl).toHaveBeenCalledWith('fresh-img')
    })
  })

  describe('Image Loading', () => {
    it('loads images for items with image_id', async () => {
      wrapper = mount(ScrollingList, { props: defaultProps })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const { getCachedImageUrl } = await import('@/common/imageCache')
      expect(getCachedImageUrl).toHaveBeenCalledWith('img1')
      expect(getCachedImageUrl).toHaveBeenCalledWith('img2')
    })
  })

  describe('Custom Item Classes', () => {
    it('applies custom classes from getItemClass prop', async () => {
      const getItemClass = (item: any) => ({
        good: item.points > 15,
        bad: item.points <= 15,
      })

      wrapper = mount(ScrollingList, {
        props: {
          ...defaultProps,
          getItemClass,
        },
      })
      await nextTick()
      await new Promise((resolve) => setTimeout(resolve, 50))

      const cards = wrapper.findAll('.item-card')
      expect(cards[0].classes()).toContain('bad') // item-1 with 10 points
      expect(cards[1].classes()).toContain('good') // item-2 with 20 points
    })
  })
})
