import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { defineComponent } from 'vue'
import BCarousel from '@components/carousel/Carousel.vue'
import InjectedChildMixin, { Sorted } from '../../utils/InjectedChildMixin'

type BCarouselInstance = InstanceType<typeof BCarousel>

let wrapper: VueWrapper<BCarouselInstance>

const mockCarouselItems = defineComponent({
    name: 'BCarouselItem',
    mixins: [InjectedChildMixin<typeof Sorted, BCarouselInstance>('carousel', Sorted)],
    computed: {
        isActive() {
            return this.parent.activeChild === this.index
        }
    },
    template: '<div></div>'
})

describe('BCarousel', () => {
    beforeEach(() => {
        wrapper = shallowMount(BCarousel, {
            props: {
                autoplay: false,
                repeat: false
            },
            slots: {
                default: [
                    '<b-carousel-item/>',
                    '<b-carousel-item/>'
                ]
            },
            global: {
                stubs: { 'b-carousel-item': mockCarouselItems }
            }
        })
    })

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

    it('is called', () => {
        expect(wrapper.vm).toBeTruthy()
        expect(wrapper.vm.$options.name).toBe('BCarousel')
    })

    it('render correctly', () => {
        expect(wrapper.html()).toMatchSnapshot()
    })

    it('reacts when value changes', async () => {
        let value = 1
        await wrapper.setProps({ modelValue: value })
        expect(wrapper.vm.activeChild).toBe(value)

        value = 3
        await wrapper.setProps({ modelValue: value })
        expect(wrapper.vm.activeChild).toBe(1)

        value = 0
        await wrapper.setProps({ modelValue: value })
        expect(wrapper.vm.activeChild).toBe(value)
    })

    it('reacts when autoplay changes', async () => {
        wrapper.vm.startTimer = vi.fn(wrapper.vm.startTimer)
        wrapper.vm.pauseTimer = vi.fn(wrapper.vm.pauseTimer)
        wrapper.vm.next = vi.fn(wrapper.vm.next)

        let autoplay = true
        await wrapper.setProps({ autoplay })

        expect(wrapper.vm.autoplay).toBe(autoplay)
        expect(wrapper.vm.startTimer).toHaveBeenCalled()

        autoplay = false
        await wrapper.setProps({ autoplay })

        expect(wrapper.vm.autoplay).toBe(autoplay)
        expect(wrapper.vm.pauseTimer).toHaveBeenCalled()
    })

    it('returns item classes accordingly', async () => {
        const indicatorBackground = true
        const indicatorCustom = true
        const indicatorInside = true
        const indicatorCustomSize = 'is-small'
        const indicatorPosition = 'is-bottom'
        await wrapper.setProps({
            indicatorBackground,
            indicatorCustom,
            indicatorInside,
            indicatorCustomSize,
            indicatorPosition
        })
        expect(wrapper.vm.indicatorClasses).toEqual([
            {
                'has-background': indicatorBackground,
                'has-custom': indicatorCustom,
                'is-inside': indicatorInside
            },
            indicatorCustom && indicatorCustomSize,
            indicatorInside && indicatorPosition
        ])
    })

    it('manage next and previous accordingly', async () => {
        wrapper.vm.startTimer = vi.fn(() => wrapper.vm.startTimer)
        wrapper.vm.pauseTimer = vi.fn(() => wrapper.vm.pauseTimer)

        const first = 0
        const last = 1
        let repeat = false
        await wrapper.setProps({ modelValue: last, repeat })

        wrapper.vm.prev()
        expect(wrapper.vm.activeChild).toBe(first)
        wrapper.vm.prev()
        expect(wrapper.vm.activeChild).toBe(first) // Wont go below 0 without repeat prop
        repeat = true
        await wrapper.setProps({ repeat })
        wrapper.vm.prev()
        expect(wrapper.vm.activeChild).toBe(last) // Will be set to the last value using repeat
        expect(wrapper.vm.startTimer).toHaveBeenCalled()

        wrapper.vm.next()
        expect(wrapper.vm.activeChild).toBe(first) // Navigate to the first value with repeat
        wrapper.vm.next()
        expect(wrapper.vm.activeChild).toBe(last)
        repeat = false
        await wrapper.setProps({ repeat })
        wrapper.vm.next()
        expect(wrapper.vm.activeChild).toBe(last) // Wont go above last when not using repeat
    })

    it('manage interaction with indicators', async () => {
        const indicator = wrapper.find('.indicator-item')

        const first = 0
        const last = 1
        await wrapper.setProps({ modelValue: last })

        await indicator.trigger('mouseover') // no change since indicatorMode is 'click'
        expect(wrapper.vm.activeChild).toBe(last)

        await indicator.trigger('click')
        expect(wrapper.vm.activeChild).toBe(first)
    })

    it('autoplays', async () => {
        vi.useFakeTimers()
        await wrapper.setProps({ autoplay: true, pauseHover: false, repeat: false })

        expect(wrapper.vm.activeChild).toBe(0)

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()
        expect(wrapper.vm.activeChild).toBe(1)

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()
        expect(wrapper.vm.activeChild).toBe(1)

        await wrapper.setProps({ repeat: true })

        await wrapper.vm.$nextTick()
        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()
        expect(wrapper.vm.activeChild).toBe(0)
    })

    it('pauses on hover', async () => {
        vi.useFakeTimers()
        await wrapper.setProps({ autoplay: true, pauseHover: true, repeat: true })

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()

        expect(wrapper.vm.activeChild).toBe(1)

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()

        expect(wrapper.vm.activeChild).toBe(0)

        await wrapper.find('.carousel').trigger('mouseenter')

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()

        expect(wrapper.vm.activeChild).toBe(0)

        await wrapper.find('.carousel').trigger('mouseleave')
        expect(wrapper.vm.activeChild).toBe(0)

        vi.runOnlyPendingTimers()
        await wrapper.vm.$nextTick()

        expect(wrapper.vm.activeChild).toBe(1)
    })

    it('drags correctly on mobile', async () => {
        const first = 0
        const last = 1
        await wrapper.setProps({ modelValue: first })
        wrapper.vm.startTimer = vi.fn(() => wrapper.vm.startTimer)
        wrapper.vm.pauseTimer = vi.fn(() => wrapper.vm.pauseTimer)

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const event: any = {
            target: {
                draggable: true
            },
            touches: true,
            changedTouches: [{
                pageX: 50
            }]
        } // predents to be a TouchEvent

        // Dragging enough to go to next slide
        await wrapper.vm.$nextTick()
        wrapper.vm.dragStart(event)
        expect(wrapper.vm.pauseTimer).toHaveBeenCalled()
        await wrapper.vm.$nextTick()
        event.changedTouches[0].pageX = 0
        wrapper.vm.dragEnd(event)
        expect(wrapper.vm.activeChild).toBe(last)

        // Dragging enough to go to previous slide
        await wrapper.vm.$nextTick()
        wrapper.vm.dragStart(event)
        expect(wrapper.vm.pauseTimer).toHaveBeenCalled()
        await wrapper.vm.$nextTick()
        event.changedTouches[0].pageX = 50
        wrapper.vm.dragEnd(event)
        expect(wrapper.vm.activeChild).toBe(first)
        expect(wrapper.vm.startTimer).toHaveBeenCalled()
    })

    it('drags correctly on desktop', async () => {
        const first = 0
        const last = 1
        await wrapper.setProps({ modelValue: first })

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const event: any = {
            target: {
                draggable: true,
                click: vi.fn()
            },
            pageX: 50,
            preventDefault: vi.fn()
        } // pretends to be a MouseEvent

        // Dragging enough to go to next slide
        await wrapper.vm.$nextTick()
        wrapper.vm.dragStart(event)
        expect(event.preventDefault).toHaveBeenCalled()
        await wrapper.vm.$nextTick()
        event.pageX = 0
        wrapper.vm.dragEnd(event)
        expect(wrapper.vm.activeChild).toBe(last)

        // Dragging enough to go to previous slide
        await wrapper.vm.$nextTick()
        wrapper.vm.dragStart(event)
        expect(event.preventDefault).toHaveBeenCalled()
        await wrapper.vm.$nextTick()
        event.pageX = 50
        wrapper.vm.dragEnd(event)
        expect(wrapper.vm.activeChild).toBe(first)

        // Considering a tiny slide for a click
        await wrapper.vm.$nextTick()
        wrapper.vm.dragStart(event)
        expect(event.preventDefault).toHaveBeenCalled()
        await wrapper.vm.$nextTick()
        event.pageX = 55
        wrapper.vm.dragEnd(event)
        expect(wrapper.vm.activeChild).toBe(first)
        expect(event.target.click).toHaveBeenCalled()
        expect(wrapper.emitted().click).toBeTruthy()
    })

    it('destroys correctly', async () => {
        await wrapper.setProps({ autoplay: true })
        await wrapper.vm.$nextTick()
        expect(wrapper.vm.timer).toBeTruthy()
        wrapper.unmount()
        expect(wrapper.vm.timer).toBeFalsy()
    })

    it('reset timer before destroy', () => {
        wrapper.vm.pauseTimer = vi.fn(() => wrapper.vm.pauseTimer)

        wrapper.unmount()

        expect(wrapper.vm.pauseTimer).toHaveBeenCalled()
    })
})
