import { defineComponent } from 'vue'
import { shallowMount, mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { MockInstance } from 'vitest'
import BInput from '@components/input/Input.vue'
import BIcon from '@components/icon/Icon.vue'
import FormElementMixin from '@utils/FormElementMixin'
import type { ExtractComponentData } from '@utils/helpers'

type BInputInstance = InstanceType<typeof BInput>
type FormElementMixinInstance = InstanceType<typeof FormElementMixin>

type BInputData = ExtractComponentData<typeof BInput>

let wrapper: VueWrapper<BInputInstance>

describe('BInput', () => {
    beforeEach(() => {
        wrapper = shallowMount(BInput)
    })

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

    it('is vue instance', () => {
        expect(wrapper.vm).toBeTruthy()
        expect(wrapper.vm.$options.name).toBe('BInput')
    })

    it('renders input element by default', () => {
        expect(wrapper.find('input').exists()).toBeTruthy()
        expect(wrapper.classes()).toContain('control')
    })

    it('render textarea element when type is textarea', async () => {
        await wrapper.setProps({ type: 'textarea' })
        const target = wrapper.find('textarea')

        expect(target.exists()).toBeTruthy()
        expect(target.classes()).toContain('textarea')
    })

    it('displays the icon when the icon property is true', async () => {
        await wrapper.setProps({ icon: 'magnify' })
        const target = wrapper.findComponent(BIcon)

        expect(target.exists()).toBeTruthy()
    })

    it('display counter when the maxlength property is passed', async () => {
        await wrapper.setProps({
            modelValue: 'foo',
            maxlength: 100
        })
        const counter = wrapper.find('small.counter')

        expect(counter.exists()).toBeTruthy()
        expect(counter.text()).toBe('3 / 100')

        await wrapper.setProps({
            modelValue: 1234
        })
        expect(counter.text()).toBe('4 / 100')
    })

    it('display correct input value length when value contains some emoji', async () => {
        await wrapper.setProps({
            modelValue: '😀2',
            maxlength: 5
        })
        const counter = wrapper.find('small.counter')

        expect(counter.exists()).toBeTruthy()
        expect(counter.text()).toBe('2 / 5')
    })

    it('no display counter when hasCounter property set for false', async () => {
        await wrapper.setProps({ maxlength: 100 })
        expect(wrapper.find('small.counter').exists()).toBeTruthy()

        await wrapper.setProps({ hasCounter: false })
        expect(wrapper.find('small.counter').exists()).toBeFalsy()
    })

    it('render field password when the type property is password', () => {
        const wrapper = shallowMount(BInput, {
            propsData: {
                type: 'password',
                passwordReveal: true
            }
        })

        const target = wrapper.find('input')
        expect(target.exists()).toBeTruthy()
        expect(target.attributes().type).toBe('password')
    })

    it('toggles the visibility of the password to true when the togglePasswordVisibility method is called', async () => {
        const wrapper = mount(BInput, {
            props: {
                modelValue: 'foo',
                type: 'password',
                passwordReveal: true
            }
        })

        await wrapper.setProps({ modelValue: 'bar' })

        expect(wrapper.find('input').exists()).toBeTruthy()
        expect(wrapper.vm.newType).toBe('password')
        expect(wrapper.vm.isPasswordVisible).toBeFalsy()
        expect(wrapper.find('input').attributes().type).toBe('password')

        const visibilityIcon = wrapper.find('.icon.is-clickable')
        expect(visibilityIcon.exists()).toBeTruthy()
        visibilityIcon.trigger('click')
        await wrapper.setProps({ passwordReveal: false })
        expect(wrapper.vm.newType).toBe('text')
        expect(wrapper.vm.isPasswordVisible).toBeTruthy()
        expect(wrapper.find('input').attributes().type).toBe('text')
    })

    it('render the placeholder and readonly attribute when passed', () => {
        const wrapper = shallowMount(BInput, {
            attrs: { placeholder: 'Awesome!', readonly: true }
        })
        const target = wrapper.find('input')

        expect(target.element.getAttribute('placeholder')).toBe('Awesome!')
        expect(target.element.getAttribute('readonly')).toBe('')
    })

    it('expands input when expanded property is passed', async () => {
        await wrapper.setProps({ expanded: true })

        expect(wrapper.classes()).toContain('is-expanded')
    })

    it('display loading icon when loading property passed', async () => {
        await wrapper.setProps({
            loading: true,
            icon: 'magnify'
        })

        expect(wrapper.classes()).toContain('is-loading')
    })

    it('keep its value on blur', async () => {
        const wrapper = mount(BInput, {
            props: {
                modelValue: 'foo',
                // overriding the method `checkHtml5Validity` won't work
                // because `mount` no longer accepts `methods` option on
                // @vue/test-utils V2
                //
                // kikuomax: I decided to disable validation instead.
                // I do not think it matters to the outcome anyway.
                useHtml5Validation: false
            }
        })

        const input = wrapper.find('input')

        input.element.value = 'bar'
        input.trigger('input')
        input.trigger('blur')

        expect(input.element.value).toBe('bar')
    })

    it('change status icon when statusType updated', async () => {
        const parent = {
            data: () => ({
                newType: 'is-success',
                // the following internal property is required
                // so that a child Input can locate the parent (this component)
                // and fetch `newType` from it.
                // see `FormElementMixin` for more details
                _isField: true
            }),
            components: { BInput },
            template: '<b-input />'
        }
        const wrapper = mount(parent)

        const input = wrapper.findComponent(BInput)
        expect(input.vm.statusTypeIcon).toBe('check')
        await wrapper.setData({ newType: 'is-danger' })
        expect(input.vm.statusTypeIcon).toBe('alert-circle')
        await wrapper.setData({ newType: 'is-info' })
        expect(input.vm.statusTypeIcon).toBe('information')
        await wrapper.setData({ newType: 'is-warning' })
        expect(input.vm.statusTypeIcon).toBe('alert')
    })

    it('manage the click on icon', async () => {
        const wrapper = mount(BInput, {
            propsData: {
                icon: 'magnify',
                iconClickable: true
            }
        })

        expect(wrapper.find('input').exists()).toBeTruthy()

        const visibilityIcon = wrapper.find('.icon.is-clickable')
        expect(visibilityIcon.exists()).toBeTruthy()
        visibilityIcon.trigger('click')

        await wrapper.vm.$nextTick()
        expect(wrapper.emitted()['icon-click']).toBeTruthy()
    })

    describe('validation', () => {
        let spyOnCheckHtml5Validity: MockInstance

        beforeEach(() => {
            spyOnCheckHtml5Validity = vi
                .spyOn(FormElementMixin.methods as FormElementMixinInstance, 'checkHtml5Validity')
                .mockImplementation(function () {
                    this.isValid = false
                    return false
                } as (this: FormElementMixinInstance) => boolean)
        })

        afterEach(() => {
            spyOnCheckHtml5Validity.mockReset()
        })

        it('should validate value at input event', async () => {
            const wrapper = shallowMount(BInput, {
                data: () => ({ isValid: false } as BInputData)
            })

            const inputElement = wrapper.get('input')

            await inputElement.element.focus()
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(0)

            await inputElement.trigger('input')
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)

            await inputElement.trigger('change')
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
        })

        it('should validate value at change event if lazy', async () => {
            const wrapper = shallowMount(BInput, {
                props: { lazy: true },
                data: () => ({ isValid: false } as BInputData)
            })

            const inputElement = wrapper.get('input')

            await inputElement.element.focus()
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(0)

            await inputElement.trigger('input')
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(0)

            await inputElement.trigger('change')
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
        })

        it('should validate value when programmatically updated', async () => {
            const wrapper = shallowMount(BInput, {
                data: () => ({ isValid: false } as BInputData)
            })
            await wrapper.setProps({ modelValue: 'foo' })
            await wrapper.vm.$nextTick()
            expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
        })

        describe('via v-model', () => {
            const rootComponent = defineComponent({
                components: { 'b-input': BInput },
                data: () => ({
                    value: '',
                    lazy: false
                }),
                template: '<b-input v-model="value" :lazy="lazy" />'
            })

            let root: VueWrapper<InstanceType<typeof rootComponent>>
            let wrapper: VueWrapper<BInputInstance>

            beforeEach(async () => {
                root = mount(rootComponent)
                wrapper = root.findComponent(BInput)
                // triggers validation and invalidates
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                wrapper.vm.onBlur({} as any)
                await wrapper.vm.$nextTick()
                spyOnCheckHtml5Validity.mockClear()
            })

            it('should update and validate value at input event', async () => {
                const inputElement = wrapper.get('input')

                inputElement.element.value = 'foo'

                await inputElement.trigger('input')
                expect(root.vm.value).toBe('foo')
                expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)

                await inputElement.trigger('change')
                expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
            })

            it('should update and validate value at change event if lazy', async () => {
                await root.setData({ lazy: true })
                // input element must be queried after changing `lazy`
                const inputElement = root.get('input')

                inputElement.element.value = 'foo'

                await inputElement.trigger('input')
                expect(root.vm.value).toBe('')
                expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(0)

                await inputElement.trigger('change')
                expect(root.vm.value).toBe('foo')
                expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
            })

            it('should validate value once when programmatically updated', async () => {
                await root.setData({ value: 'foo' })
                await wrapper.vm.$nextTick()
                expect(spyOnCheckHtml5Validity).toHaveBeenCalledTimes(1)
            })
        })
    })

    describe('with fallthrough attributes', () => {
        const attrs = {
            class: 'fallthrough-class',
            style: 'font-size: 2rem;',
            id: 'fallthrough-id'
        }

        it('should bind class, style, and id to the root div if compatFallthrough is true (default)', () => {
            const wrapper = shallowMount(BInput, { attrs })

            const root = wrapper.find('div.control')
            expect(root.classes(attrs.class)).toBe(true)
            expect(root.attributes('style')).toBe(attrs.style)
            expect(root.attributes('id')).toBe(attrs.id)

            const input = wrapper.find('input')
            expect(input.classes(attrs.class)).toBe(false)
            expect(input.attributes('style')).toBeUndefined()
            expect(input.attributes('id')).toBeUndefined()
        })

        it('should bind class, style, and id to the input element if compatFallthrough is false', () => {
            const wrapper = shallowMount(BInput, {
                attrs,
                props: {
                    compatFallthrough: false
                }
            })

            const input = wrapper.find('input')
            expect(input.classes(attrs.class)).toBe(true)
            expect(input.attributes('style')).toBe(attrs.style)
            expect(input.attributes('id')).toBe(attrs.id)

            const root = wrapper.find('div.control')
            expect(root.classes(attrs.class)).toBe(false)
            expect(root.attributes('style')).toBeUndefined()
            expect(root.attributes('id')).toBeUndefined()
        })

        it('should bind class, style, and id to the textarea element if compatFallthrough is false', () => {
            const wrapper = shallowMount(BInput, {
                attrs,
                props: {
                    compatFallthrough: false,
                    type: 'textarea'
                }
            })

            const textarea = wrapper.find('textarea')
            expect(textarea.classes(attrs.class)).toBe(true)
            expect(textarea.attributes('style')).toBe(attrs.style)
            expect(textarea.attributes('id')).toBe(attrs.id)

            const root = wrapper.find('div.control')
            expect(root.classes(attrs.class)).toBe(false)
            expect(root.attributes('style')).toBeUndefined()
            expect(root.attributes('id')).toBeUndefined()
        })
    })
})
