import '@testing-library/jest-dom/vitest'
import { toRaw } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
import BInput from '@components/input/Input.vue'
import BTable from '@components/table/Table.vue'
import BTablePagination from '@components/table/TablePagination.vue'
import type { ITableColumn } from '@components/table/types'

describe('BTable', () => {
    let wrapper: VueWrapper<InstanceType<typeof BTable>>
    beforeEach(() => {
        wrapper = shallowMount(BTable)
    })

    const tableCols = shallowMount(BTable, {
        props: {
            columns: [
                { label: 'default', width: '100px' },
                { label: 'pecent', width: '50%' },
                { label: 'fixed_num', width: 100 },
                { label: 'fixed_str', width: '100' }
            ]
        }
    })

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

        expect(tableCols.vm).toBeTruthy()
        expect(tableCols.vm.$options.name).toBe('BTable')
    })

    it('has the filter row visible when searchable', async () => {
        await wrapper.setProps({
            columns: [
                {
                    field: 'id',
                    label: 'ID',
                    width: '40',
                    numeric: true
                }
            ]
        })
        // Don't show if no searchable column
        expect(wrapper.vm.hasSearchablenewColumns).toBe(false)
        // Show if one or more searchable column
        await wrapper.setProps({
            columns: [
                {
                    field: 'id',
                    label: 'ID',
                    width: '40',
                    numeric: true,
                    searchable: true
                }
            ]
        })
        expect(wrapper.vm.hasSearchablenewColumns).toBe(true)
    })

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

    it('holds columns', () => {
        const headers = tableCols.findAll('th')

        expect(headers.length).toBeGreaterThanOrEqual(4)

        const cols = headers.filter((th) => {
            const div = th.find('div')

            return div.classes('th-wrap')
        })

        expect(cols.length).toBe(4)
        expect(cols[0].attributes('style')).toBe('width: 100px;')
        expect(cols[1].attributes('style')).toBe('width: 50%;')
        expect(cols[2].attributes('style')).toBe('width: 100px;')
        expect(cols[3].attributes('style')).toBe('width: 100px;')
    })

    describe('Selectable', () => {
        const data = [
            { id: 1, name: 'Jesse' },
            { id: 2, name: 'John' },
            { id: 3, name: 'Tina' },
            { id: 4, name: 'Anne' },
            { id: 5, name: 'Clarence' }
        ]
        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: [
                        { label: 'ID', field: 'id' },
                        { label: 'Name', field: 'name' }
                    ],
                    data
                }
            })
        })

        it('unselected( no column-row-key )', () => {
            expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0)
        })

        it('unselected( column-row-key )', async () => {
            await wrapper.setProps({
                customRowKey: 'id'
            })
            expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0)
        })

        it('compare by instance itself', async () => {
            await wrapper.setProps({
                selected: data[0]
            })
            const rows = wrapper.findAll('tbody tr')
            expect(rows[0].classes()).toContain('is-selected')
        })

        it('target data and key match', async () => {
            await wrapper.setProps({
                selected: data[1],
                customRowKey: 'id'
            })
            const rows = wrapper.findAll('tbody tr')
            expect(rows[1].classes()).toContain('is-selected')
        })

        it('clear data', async () => {
            await wrapper.setProps({
                selected: data[0],
                customRowKey: 'id'
            })
            const rows = wrapper.findAll('tbody tr')
            expect(rows[0].classes()).toContain('is-selected')

            await wrapper.setProps({
                selected: undefined
            })
            expect(wrapper.findAll('tbody tr.is-selected')).toHaveLength(0)
        })
    })

    describe('Searchable', () => {
        const data = [
            { id: 1, name: 'Jesse' },
            { id: 2, name: 'João' },
            { id: 3, name: 'Tina' },
            { id: 4, name: 'Anne' },
            { id: 5, name: 'Clarence' }
        ]
        let headRows: DOMWrapper<Element>[]
        let bodyRows: DOMWrapper<Element>[]
        let searchInput: VueWrapper<InstanceType<typeof BInput>>

        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: [
                        { label: 'ID', field: 'id', numeric: true },
                        { label: 'Name', field: 'name', searchable: true }
                    ],
                    data
                }
            })
            headRows = wrapper.findAll('thead tr')
            bodyRows = wrapper.findAll('tbody tr')
            searchInput = wrapper.findComponent(BInput)
        })

        it('displays filter row when at least one column is searchable', () => {
            expect(headRows).toHaveLength(2)
        })

        it('displays filter input only on searchable columns', () => {
            const filterCells = headRows[1].findAll('.th-wrap')

            expect(filterCells[0].element).toBeEmptyDOMElement() // ID column is not searchable
            expect(
                filterCells[1].findComponent(BInput).exists()
            ).toBe(true) // Name column is searchable
        })

        it('displays all data', () => {
            expect(bodyRows).toHaveLength(5)
        })

        it('displays filtered data when searching', async () => {
            searchInput.vm.$emit('update:modelValue', 'J')
            await searchInput.vm.$nextTick() // makes sure the DOM is updated
            bodyRows = wrapper.findAll('tbody tr')

            expect(bodyRows).toHaveLength(2) // Jesse and João
        })

        it('displays filtered data when searching by name without accent', async () => {
            searchInput.vm.$emit('update:modelValue', 'Joao')
            await searchInput.vm.$nextTick() // makes sure the DOM is updated
            bodyRows = wrapper.findAll('tbody tr')

            expect(bodyRows).toHaveLength(1) // João
        })

        it('displays filtered data when searching by name with accent', async () => {
            searchInput.vm.$emit('update:modelValue', 'João')
            await searchInput.vm.$nextTick() // makes sure the DOM is updated
            bodyRows = wrapper.findAll('tbody tr')

            expect(bodyRows).toHaveLength(1) // João
        })

        it('displays filtered data when searching and updating data', async () => {
            searchInput.vm.$emit('update:modelValue', 'J')
            await wrapper.setProps({
                data: [
                    ...data,
                    { id: 6, name: 'Justin' }
                ]
            })
            bodyRows = wrapper.findAll('tbody tr')

            expect(bodyRows).toHaveLength(3) // Jesse, João and Justin
        })

        it('debounce search filtering when debounce-search is defined', async () => {
            vi.useFakeTimers()
            await wrapper.setProps({
                debounceSearch: 1000
            })
            for (let i = 0; i < 10; i++) {
                searchInput.vm.$emit('update:modelValue', 'J'.repeat(10 - i))
                vi.advanceTimersByTime(500)
                await wrapper.vm.$nextTick() // makes sure the DOM is updated
                bodyRows = wrapper.findAll('tbody tr')
                expect(bodyRows).toHaveLength(5) // No filtering yet
            }
            vi.advanceTimersByTime(1000)
            await wrapper.vm.$nextTick() // makes sure the DOM is updated
            bodyRows = wrapper.findAll('tbody tr')
            expect(bodyRows).toHaveLength(2) // Filtering after debounce
            vi.useRealTimers()
        })
    })

    describe('Sortable', () => {
        let wrapper: VueWrapper<InstanceType<typeof BTable>>
        const data = [
            { id: 1, name: 'Jesse' },
            { id: 2, name: 'João' },
            { id: 3, name: 'Tina' },
            { id: 4, name: 'Anne' },
            { id: 5, name: 'Clarence' }
        ]
        const columnsData = [
            {
                field: 'id',
                label: 'ID',
                numeric: true,
                sortable: true
            },
            {
                field: 'name',
                label: 'Name',
                sortable: true
            }
        ]
        let columns: ITableColumn[]

        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: columnsData,
                    data
                }
            })
            // columnsData is transformed into newColumns with new objects
            columns = wrapper.vm.newColumns
        })

        it('should be able to sort by ID', () => {
            const sorted = [...data]
            wrapper.vm.sort(columns[0])
            expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[0]))
            expect(wrapper.vm.isAsc).toBe(true)
            expect(wrapper.vm.visibleData).toEqual(sorted)
            // toggles
            wrapper.vm.sort(columns[0])
            expect(wrapper.vm.isAsc).toBe(false)
            expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
        })

        it('should be able to sort by Name', () => {
            const sorted = [
                data[3], data[4], data[0], data[1], data[2]
            ]
            wrapper.vm.sort(columns[1])
            expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1]))
            expect(wrapper.vm.isAsc).toBe(true)
            expect(wrapper.vm.visibleData).toEqual(sorted)
            // toggles
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.isAsc).toBe(false)
            expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
        })
    })

    describe('Multi-sortable', () => {
        let wrapper: VueWrapper<InstanceType<typeof BTable>>
        const data = [
            { id: 1, name: 'Jesse', age: 23 },
            { id: 2, name: 'João', age: 22 },
            { id: 3, name: 'Tina', age: 22 },
            { id: 4, name: 'Anne', age: 23 },
            { id: 5, name: 'Clarence', age: 22 }
        ]
        const columnsData = [
            {
                field: 'id',
                label: 'ID'
            },
            {
                field: 'name',
                label: 'Name',
                sortable: true
            },
            {
                field: 'age',
                label: 'Age',
                numeric: true,
                sortable: true
            }
        ]
        let columns: ITableColumn[]

        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: columnsData,
                    data,
                    sortMultiple: true
                }
            })
            // columnsData is transformed into newColumns with new objects
            columns = wrapper.vm.newColumns
        })

        it('should be able to sort by Age then Name', () => {
            wrapper.vm.sort(columns[2])
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'age', order: 'asc' },
                { field: 'name', order: 'asc' }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[4], data[1], data[2], data[3], data[0]
            ])
            // toggles age
            wrapper.vm.sort(columns[2])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'age', order: 'desc' },
                { field: 'name', order: 'asc' }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[3], data[0], data[4], data[1], data[2]
            ])
            // toggles name
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'age', order: 'desc' },
                { field: 'name', order: 'desc' }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[0], data[3], data[2], data[1], data[4]
            ])
        })
    })

    describe('Sortable with custom sort', () => {
        let wrapper: VueWrapper<InstanceType<typeof BTable>>
        const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
        const data = weekdays.map((day, i) => ({
            id: i + 1,
            day
        }))
        const customSort = vi.fn((a, b, isAsc) => {
            const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day)
            return isAsc ? ord : -ord
        })
        const columnsData = [
            {
                field: 'id',
                label: 'ID',
                numeric: true
            },
            {
                field: 'day',
                label: 'Day',
                sortable: true,
                customSort
            }
        ]
        let columns: ITableColumn[]

        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: columnsData,
                    data
                }
            })
            // columnsData is transformed into newColumns with new objects
            columns = wrapper.vm.newColumns
        })

        afterEach(() => {
            customSort.mockClear()
        })

        it('should be able to sort by Day with custom sort', async () => {
            const sorted = [...data]
            wrapper.vm.sort(columns[1])
            expect(toRaw(wrapper.vm.currentSortColumn)).toBe(toRaw(columns[1]))
            expect(wrapper.vm.isAsc).toBe(true)
            expect(wrapper.vm.visibleData).toEqual(sorted)
            expect(customSort).toHaveBeenCalled()
            // toggles
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.isAsc).toBe(false)
            expect(wrapper.vm.visibleData).toEqual(sorted.reverse())
        })
    })

    describe('Multi-sortable with custom sort', () => {
        let wrapper: VueWrapper<InstanceType<typeof BTable>>
        const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
        const data = [
            { id: 1, day: 'Sun', fee: 15 },
            { id: 2, day: 'Mon', fee: 12 },
            { id: 3, day: 'Tue', fee: 12 },
            { id: 4, day: 'Wed', fee: 12 },
            { id: 5, day: 'Thu', fee: 12 },
            { id: 6, day: 'Fri', fee: 12 },
            { id: 7, day: 'Sat', fee: 15 }
        ]
        const dayCustomSort = vi.fn((a, b, isAsc) => {
            const ord = weekdays.indexOf(a.day) - weekdays.indexOf(b.day)
            return isAsc ? ord : -ord
        })
        const feeCustomSort = vi.fn((a, b, isAsc) => {
            const ord = a.fee - b.fee
            return isAsc ? -ord : ord
        })
        const columnsData = [
            {
                field: 'id',
                label: 'ID',
                numeric: true
            },
            {
                field: 'day',
                label: 'Day',
                sortable: true,
                customSort: dayCustomSort
            },
            {
                field: 'fee',
                label: 'Fee',
                sortable: true,
                customSort: feeCustomSort
            }
        ]
        let columns: ITableColumn[]

        beforeEach(() => {
            wrapper = shallowMount(BTable, {
                props: {
                    columns: columnsData,
                    data,
                    sortMultiple: true
                }
            })
            columns = wrapper.vm.newColumns
        })

        afterEach(() => {
            dayCustomSort.mockClear()
            feeCustomSort.mockClear()
        })

        it('should be able to sort by Fee then Day with custom sort', () => {
            wrapper.vm.sort(columns[2])
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'fee', order: 'asc', customSort: feeCustomSort },
                { field: 'day', order: 'asc', customSort: dayCustomSort }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[0], data[6], data[1], data[2], data[3], data[4], data[5]
            ])
            expect(feeCustomSort).toHaveBeenCalled()
            expect(dayCustomSort).toHaveBeenCalled()
            // toggles fee
            wrapper.vm.sort(columns[2])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'fee', order: 'desc', customSort: feeCustomSort },
                { field: 'day', order: 'asc', customSort: dayCustomSort }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[1], data[2], data[3], data[4], data[5], data[0], data[6]
            ])
            // toggles day
            wrapper.vm.sort(columns[1])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'fee', order: 'desc', customSort: feeCustomSort },
                { field: 'day', order: 'desc', customSort: dayCustomSort }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[5], data[4], data[3], data[2], data[1], data[6], data[0]
            ])
        })

        it('should be able to remove column from sort (Fee+Day → Day)', () => {
            wrapper.vm.sort(columns[2])
            wrapper.vm.sort(columns[1])
            wrapper.vm.sort(columns[1]) // day → descending order
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'fee', order: 'asc', customSort: feeCustomSort },
                { field: 'day', order: 'desc', customSort: dayCustomSort }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[6], data[0], data[5], data[4], data[3], data[2], data[1]
            ])
            // removes fee
            wrapper.vm.removeSortingPriority(columns[2])
            expect(wrapper.vm.sortMultipleDataLocal).toEqual([
                { field: 'day', order: 'desc', customSort: dayCustomSort }
            ])
            expect(wrapper.vm.visibleData).toEqual([
                data[6], data[5], data[4], data[3], data[2], data[1], data[0]
            ])
        })
    })

    describe('with fallthrough attributes', () => {
        const data = [
            { id: 1, name: 'Jesse' },
            { id: 2, name: 'John' },
            { id: 3, name: 'Tina' },
            { id: 4, name: 'Anne' },
            { id: 5, name: 'Clarence' }
        ]
        const columns = [
            { label: 'ID', field: 'id' },
            { label: 'Name', field: 'name' }
        ]
        const attrs = {
            class: 'fallthrough-class',
            style: 'font-size: 2rem;',
            id: 'fallthrough-id'
        }

        it('should apply class, style, and id to the root <div> element if compatFallthrough is true (default)', () => {
            const wrapper = shallowMount(BTable, {
                attrs,
                props: {
                    paginated: true,
                    paginationPosition: 'both',
                    columns,
                    data
                }
            })

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

            const paginations = wrapper.findAllComponents(BTablePagination)
            // top
            expect(paginations[0].classes(attrs.class)).toBe(false)
            expect(paginations[0].attributes('style')).toBeUndefined()
            expect(paginations[0].attributes('id')).toBeUndefined()
            // bottom
            expect(paginations[1].classes(attrs.class)).toBe(false)
            expect(paginations[1].attributes('style')).toBeUndefined()
            expect(paginations[1].attributes('id')).toBeUndefined()
        })

        it('should apply class, style, and id to the underlying <b-table-pagination> components if compatFallthrough is false', () => {
            const wrapper = shallowMount(BTable, {
                attrs,
                props: {
                    compatFallthrough: false,
                    paginated: true,
                    paginationPosition: 'both',
                    columns,
                    data
                }
            })

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

            const paginations = wrapper.findAllComponents(BTablePagination)
            // top
            expect(paginations[0].classes(attrs.class)).toBe(true)
            expect(paginations[0].attributes('style')).toBe(attrs.style)
            expect(paginations[0].attributes('id')).toBe(attrs.id)
            // bottom
            expect(paginations[1].classes(attrs.class)).toBe(true)
            expect(paginations[1].attributes('style')).toBe(attrs.style)
            expect(paginations[1].attributes('id')).toBe(attrs.id)
        })
    })
})
