import { describe, it, expect, beforeEach, vi } from 'vitest'
import { initSortable } from '../../assets/modules/sortable.js'

function createList(reorderUrl = '/reorder') {
    const list = document.createElement('div')
    list.id = 'categories-list'
    if (reorderUrl) {
        list.dataset.reorderUrl = reorderUrl
    }
    document.body.appendChild(list)

    return list
}

function addItem(list, id) {
    const el = document.createElement('div')
    el.dataset.id = String(id)
    list.appendChild(el)

    return el
}

function createDragEvent(type, overrides = {}) {
    const event = new Event(type, { bubbles: true, cancelable: true })
    event.dataTransfer = { effectAllowed: '' }
    event.preventDefault = vi.fn()
    Object.assign(event, overrides)

    return event
}

describe('initSortable', () => {
    beforeEach(() => {
        document.body.innerHTML = ''
        vi.restoreAllMocks()
    })

    it('does nothing without categories-list element', () => {
        expect(() => initSortable()).not.toThrow()
    })

    it('does nothing without reorderUrl', () => {
        const list = document.createElement('div')
        list.id = 'categories-list'
        document.body.appendChild(list)
        expect(() => initSortable()).not.toThrow()
        expect(list.querySelector('[draggable]')).toBeNull()
    })

    it('sets draggable on all data-id elements', () => {
        const list = createList()
        addItem(list, 1)
        addItem(list, 2)

        initSortable()

        list.querySelectorAll('[data-id]').forEach(el => {
            expect(el.getAttribute('draggable')).toBe('true')
        })
    })

    it('adds opacity-50 on dragstart', () => {
        const list = createList()
        const item = addItem(list, 1)

        initSortable()

        const event = createDragEvent('dragstart')
        item.dispatchEvent(event)

        expect(item.classList.contains('opacity-50')).toBe(true)
        expect(event.dataTransfer.effectAllowed).toBe('move')
    })

    it('dragstart does nothing when no data-id parent', () => {
        const list = createList()
        const child = document.createElement('span')
        list.appendChild(child)

        initSortable()

        const event = createDragEvent('dragstart')
        child.dispatchEvent(event)

        expect(child.classList.contains('opacity-50')).toBe(false)
    })

    it('removes opacity-50 on dragend', () => {
        const list = createList()
        const item = addItem(list, 1)

        initSortable()

        const startEvent = createDragEvent('dragstart')
        item.dispatchEvent(startEvent)
        expect(item.classList.contains('opacity-50')).toBe(true)

        const endEvent = createDragEvent('dragend')
        item.dispatchEvent(endEvent)
        expect(item.classList.contains('opacity-50')).toBe(false)
    })

    it('dragend does nothing when dragEl is null', () => {
        const list = createList()
        addItem(list, 1)

        initSortable()

        const item = list.querySelector('[data-id]')
        const endEvent = createDragEvent('dragend')
        list.dispatchEvent(endEvent)
        expect(item.classList.contains('opacity-50')).toBe(false)
    })

    it('moves element before target on dragover above midpoint', () => {
        const list = createList()
        const item1 = addItem(list, 1)
        const item2 = addItem(list, 2)

        initSortable()

        // Start dragging item2
        const startEvent = createDragEvent('dragstart')
        item2.dispatchEvent(startEvent)

        // Dragover item1 above midpoint
        vi.spyOn(item1, 'getBoundingClientRect').mockReturnValue({
            top: 100, height: 50, left: 0, right: 100, bottom: 150, width: 100,
        })
        const overEvent = createDragEvent('dragover', { clientY: 110 })
        item1.dispatchEvent(overEvent)

        expect(overEvent.preventDefault).toHaveBeenCalled()
        // item2 should be before item1
        const items = list.querySelectorAll('[data-id]')
        expect(items[0].dataset.id).toBe('2')
        expect(items[1].dataset.id).toBe('1')
    })

    it('moves element after target on dragover below midpoint', () => {
        const list = createList()
        const item1 = addItem(list, 1)
        const item2 = addItem(list, 2)
        const item3 = addItem(list, 3)

        initSortable()

        // Start dragging item1
        const startEvent = createDragEvent('dragstart')
        item1.dispatchEvent(startEvent)

        // Dragover item2 below midpoint
        vi.spyOn(item2, 'getBoundingClientRect').mockReturnValue({
            top: 100, height: 50, left: 0, right: 100, bottom: 150, width: 100,
        })
        const overEvent = createDragEvent('dragover', { clientY: 140 })
        item2.dispatchEvent(overEvent)

        // item1 should be after item2 (between item2 and item3)
        const items = list.querySelectorAll('[data-id]')
        expect(items[0].dataset.id).toBe('2')
        expect(items[1].dataset.id).toBe('1')
        expect(items[2].dataset.id).toBe('3')
    })

    it('dragover does nothing when target is dragEl', () => {
        const list = createList()
        const item = addItem(list, 1)

        initSortable()

        const startEvent = createDragEvent('dragstart')
        item.dispatchEvent(startEvent)

        const overEvent = createDragEvent('dragover', { clientY: 100 })
        item.dispatchEvent(overEvent)

        // Should still just have one item, no error
        expect(list.querySelectorAll('[data-id]').length).toBe(1)
    })

    it('dragover does nothing when target has no data-id', () => {
        const list = createList()
        addItem(list, 1)
        const span = document.createElement('span')
        list.appendChild(span)

        initSortable()

        const startEvent = createDragEvent('dragstart')
        list.querySelector('[data-id]').dispatchEvent(startEvent)

        const overEvent = createDragEvent('dragover', { clientY: 100 })
        span.dispatchEvent(overEvent)

        expect(overEvent.preventDefault).toHaveBeenCalled()
    })

    it('sends reorder request on drop', () => {
        const fetchMock = vi.fn().mockResolvedValue({ ok: true })
        globalThis.fetch = fetchMock

        const list = createList('/api/reorder')
        addItem(list, 3)
        addItem(list, 1)
        addItem(list, 2)

        initSortable()

        const dropEvent = createDragEvent('drop')
        list.dispatchEvent(dropEvent)

        expect(dropEvent.preventDefault).toHaveBeenCalled()
        expect(fetchMock).toHaveBeenCalledWith('/api/reorder', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify([3, 1, 2]),
        })
    })

    it('reloads page on fetch error during drop', async () => {
        const fetchMock = vi.fn().mockRejectedValue(new Error('Network'))
        globalThis.fetch = fetchMock
        const reloadMock = vi.fn()
        globalThis.location = { reload: reloadMock }

        const list = createList('/api/reorder')
        addItem(list, 1)

        initSortable()

        const dropEvent = createDragEvent('drop')
        list.dispatchEvent(dropEvent)

        await new Promise(r => setTimeout(r, 10))

        expect(reloadMock).toHaveBeenCalled()
    })

    it('initializes billets-list sortable', () => {
        const list = document.createElement('div')
        list.classList.add('billets-list')
        list.dataset.reorderUrl = '/billet-reorder'
        document.body.appendChild(list)

        const el1 = document.createElement('div')
        el1.dataset.billetId = '10'
        list.appendChild(el1)

        const el2 = document.createElement('div')
        el2.dataset.billetId = '20'
        list.appendChild(el2)

        initSortable()

        expect(el1.getAttribute('draggable')).toBe('true')
        expect(el2.getAttribute('draggable')).toBe('true')
    })
})
