import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
    KviewBentoGrid,
    KviewBentoItem,
    KviewCombobox,
    KviewDatePicker,
    KviewMultiSelect,
} from '@kview/ui-core/vue';

const flush = async (): Promise<void> => {
    await nextTick();
    await new Promise((resolve) => setTimeout(resolve, 0));
    await nextTick();
};

const originalResizeObserver = window.ResizeObserver;
const originalPointerEvent = window.PointerEvent;

beforeEach(() => {
    class ResizeObserverStub {
        observe(): void {}

        unobserve(): void {}

        disconnect(): void {}
    }

    Object.defineProperty(window, 'ResizeObserver', {
        configurable: true,
        value: ResizeObserverStub,
    });
    Object.defineProperty(window, 'PointerEvent', {
        configurable: true,
        value: window.PointerEvent ?? MouseEvent,
    });

    Element.prototype.scrollIntoView = vi.fn();
    HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
    HTMLElement.prototype.releasePointerCapture = vi.fn();
});

afterEach(() => {
    document.body.innerHTML = '';
    Object.defineProperty(window, 'ResizeObserver', {
        configurable: true,
        value: originalResizeObserver,
    });
    Object.defineProperty(window, 'PointerEvent', {
        configurable: true,
        value: originalPointerEvent,
    });
    vi.restoreAllMocks();
});

describe('KviewCombobox', () => {
    it('keeps lookup fetching host-owned while preserving value, query, ARIA and Teleport', async () => {
        const wrapper = mount({
            components: { KviewCombobox },
            data: () => ({ value: '', query: '' }),
            template: `
                <form>
                    <KviewCombobox
                        id="fund_id"
                        v-model="value"
                        v-model:query="query"
                        name="fund_id"
                        label="Fund"
                        hint="Search by fund name."
                        error="Select a fund."
                        required
                        :options="[
                            { value: 'fund-1', label: 'Alpha Growth', meta: 'EUR' },
                            { value: 'fund-2', label: 'Beta Credit', meta: 'USD' },
                        ]"
                    />
                </form>
            `,
        }, { attachTo: document.body });

        const input = wrapper.get<HTMLInputElement>('#fund_id');

        expect(input.attributes('role')).toBe('combobox');
        expect(input.attributes('aria-describedby')).toBe('fund_id_hint fund_id_error');
        expect(input.attributes('aria-invalid')).toBe('true');
        expect(wrapper.get<HTMLInputElement>('input[type="hidden"][name="fund_id"]').element.value).toBe('');
        await flush();
        expect(input.element.checkValidity()).toBe(false);

        await input.setValue('Beta');
        await flush();

        const listbox = document.body.querySelector<HTMLElement>('[role="listbox"]');
        const option = Array.from(document.body.querySelectorAll<HTMLElement>('[role="option"]'))
            .find((item) => item.textContent?.includes('Beta Credit'));

        expect(listbox).not.toBeNull();
        expect(listbox?.closest('[data-reka-popper-content-wrapper]')?.parentElement).toBe(document.body);
        expect(option).toBeDefined();

        option?.dispatchEvent(new PointerEvent('pointerup', { bubbles: true }));
        option?.click();
        await flush();

        expect(wrapper.get<HTMLInputElement>('input[type="hidden"][name="fund_id"]').element.value).toBe('fund-2');
        expect(input.element.value).toBe('Beta Credit');
        expect((wrapper.vm.$data as { query: string }).query).toBe('Beta Credit');
        expect(input.element.checkValidity()).toBe(true);

        await input.setValue('Different');
        await flush();

        expect(wrapper.get<HTMLInputElement>('input[type="hidden"][name="fund_id"]').element.value).toBe('');
        expect(input.element.checkValidity()).toBe(false);
        wrapper.unmount();
    });

    it('keeps empty feedback inside the listbox accessibility contract', async () => {
        const wrapper = mount(KviewCombobox, {
            props: {
                id: 'investor_id',
                name: 'investor_id',
                label: 'Investor',
                options: [],
                noResultsLabel: 'No investors found',
            },
            attachTo: document.body,
        });
        const input = wrapper.get<HTMLInputElement>('#investor_id');

        await input.trigger('focus');
        await flush();

        const listbox = document.body.querySelector<HTMLElement>('[role="listbox"]');
        const emptyOption = listbox?.querySelector<HTMLElement>('[role="option"]');

        expect(listbox).not.toBeNull();
        expect(emptyOption?.textContent).toContain('No investors found');
        expect(emptyOption?.getAttribute('aria-disabled')).toBe('true');
        wrapper.unmount();
    });
});

describe('KviewMultiSelect', () => {
    it('keeps the checkbox menu open, searches, submits an array and clears to the all state', async () => {
        const wrapper = mount({
            components: { KviewMultiSelect },
            data: () => ({ values: [] as string[] }),
            template: `
                <form>
                    <KviewMultiSelect
                        id="fund_id"
                        v-model="values"
                        name="fund_id"
                        label="Fund"
                        placeholder="All funds"
                        searchable
                        search-placeholder="Search funds"
                        selected-count-label=":count funds"
                        clear-label="Show all funds"
                        :options="[
                            { value: 'fund-1', label: 'Alpha Growth' },
                            { value: 'fund-2', label: 'Beta Credit' },
                        ]"
                    />
                </form>
            `,
        }, { attachTo: document.body });
        const trigger = wrapper.get<HTMLButtonElement>('#fund_id');

        expect(trigger.text()).toContain('All funds');
        await trigger.trigger('click');
        await flush();

        const dialog = document.body.querySelector<HTMLElement>('#fund_id_popover');
        const checkboxes = Array.from(dialog?.querySelectorAll<HTMLButtonElement>('[role="checkbox"]') ?? []);

        expect(dialog?.hidden).toBe(false);
        expect(checkboxes).toHaveLength(2);
        checkboxes[0]?.click();
        await flush();

        expect(dialog?.hidden).toBe(false);
        expect(wrapper.findAll<HTMLInputElement>('input[type="hidden"][name="fund_id[]"]')).toHaveLength(1);
        expect(wrapper.get<HTMLInputElement>('input[type="hidden"][name="fund_id[]"]').element.value).toBe('fund-1');

        checkboxes[1]?.click();
        await flush();

        expect(trigger.text()).toContain('2 funds');
        expect(wrapper.findAll('input[type="hidden"][name="fund_id[]"]')).toHaveLength(2);

        const search = dialog?.querySelector<HTMLInputElement>('.ui-multiselect-search');
        search!.value = 'Beta';
        search!.dispatchEvent(new Event('input', { bubbles: true }));
        await flush();

        expect(dialog?.querySelector('.ui-multiselect-options')?.textContent).toContain('Beta Credit');
        expect(dialog?.querySelector('.ui-multiselect-options')?.textContent).not.toContain('Alpha Growth');

        const clear = Array.from(dialog?.querySelectorAll<HTMLButtonElement>('button') ?? [])
            .find((button) => button.textContent?.includes('Show all funds'));
        clear?.click();
        await flush();

        expect(wrapper.findAll('input[type="hidden"][name="fund_id[]"]')).toHaveLength(0);
        expect(trigger.text()).toContain('All funds');
        expect(dialog?.hidden).toBe(false);
        wrapper.unmount();
    });

    it('honors required, disabled and explicit accessible-name form contracts', async () => {
        const wrapper = mount({
            components: { KviewMultiSelect },
            data: () => ({ values: [] as string[], disabled: false }),
            template: `
                <form>
                    <KviewMultiSelect
                        id="required_funds"
                        v-model="values"
                        name="funds"
                        aria-label="Permitted funds"
                        required
                        :disabled="disabled"
                        :options="[
                            { value: 'locked', label: 'Locked fund', disabled: true },
                            { value: 'alpha', label: 'Alpha Growth' },
                        ]"
                    />
                </form>
            `,
        }, { attachTo: document.body });
        const form = wrapper.get<HTMLFormElement>('form').element;
        const trigger = wrapper.get<HTMLButtonElement>('#required_funds');

        expect(trigger.attributes('aria-label')).toBe('Permitted funds');
        expect(form.checkValidity()).toBe(false);
        expect(document.activeElement).toBe(trigger.element);

        await trigger.trigger('click');
        await flush();

        const dialog = document.body.querySelector<HTMLElement>('#required_funds_popover');
        const enabledCheckbox = dialog?.querySelector<HTMLButtonElement>('[role="checkbox"]:not([disabled])');

        expect(document.activeElement).toBe(enabledCheckbox);
        enabledCheckbox?.click();
        await flush();

        expect(form.checkValidity()).toBe(true);
        await wrapper.setData({ disabled: true });
        await flush();

        expect(Array.from(new FormData(form).getAll('funds[]'))).toEqual([]);
        wrapper.unmount();
    });

    it('keeps selected disabled options when the clear action resets editable choices', async () => {
        const wrapper = mount({
            components: { KviewMultiSelect },
            data: () => ({ values: ['locked', 'alpha'] as string[] }),
            template: `
                <KviewMultiSelect
                    id="locked_funds"
                    v-model="values"
                    name="funds"
                    label="Funds"
                    :options="[
                        { value: 'locked', label: 'Locked fund', disabled: true },
                        { value: 'alpha', label: 'Alpha Growth' },
                    ]"
                />
            `,
        }, { attachTo: document.body });

        await wrapper.get<HTMLButtonElement>('#locked_funds').trigger('click');
        await flush();

        const dialog = document.body.querySelector<HTMLElement>('#locked_funds_popover');
        const clear = Array.from(dialog?.querySelectorAll<HTMLButtonElement>('button') ?? [])
            .find((button) => button.textContent?.includes('Clear selection'));
        clear?.click();
        await flush();

        expect(wrapper.findAll<HTMLInputElement>('input[type="hidden"][name="funds[]"]')
            .map((input) => input.element.value)).toEqual(['locked']);
        wrapper.unmount();
    });

    it('surfaces and clears selected values that are no longer present in the option list', async () => {
        const wrapper = mount({
            components: { KviewMultiSelect },
            data: () => ({ values: ['retired-fund'] as string[] }),
            template: `
                <KviewMultiSelect
                    id="orphaned_funds"
                    v-model="values"
                    name="funds"
                    label="Funds"
                    placeholder="All funds"
                    :options="[
                        { value: 'alpha', label: 'Alpha Growth' },
                    ]"
                />
            `,
        }, { attachTo: document.body });
        const trigger = wrapper.get<HTMLButtonElement>('#orphaned_funds');

        expect(trigger.text()).toContain('1 selected');
        expect(wrapper.get<HTMLInputElement>('input[type="hidden"][name="funds[]"]').element.value)
            .toBe('retired-fund');

        await trigger.trigger('click');
        await flush();

        const dialog = document.body.querySelector<HTMLElement>('#orphaned_funds_popover');
        const clear = Array.from(dialog?.querySelectorAll<HTMLButtonElement>('button') ?? [])
            .find((button) => button.textContent?.includes('Clear selection'));

        expect(clear?.disabled).toBe(false);
        clear?.click();
        await flush();

        expect(wrapper.findAll('input[type="hidden"][name="funds[]"]')).toHaveLength(0);
        expect(trigger.text()).toContain('All funds');
        wrapper.unmount();
    });
});

describe('KviewBentoGrid', () => {
    it('exposes responsive column, gap and item-span contracts without owning a surface', () => {
        const wrapper = mount({
            components: { KviewBentoGrid, KviewBentoItem },
            template: `
                <KviewBentoGrid :columns="3" gap="lg">
                    <KviewBentoItem :span="2"><article>Primary</article></KviewBentoItem>
                    <KviewBentoItem><article>Secondary</article></KviewBentoItem>
                </KviewBentoGrid>
            `,
        });

        expect(wrapper.get('.ui-bento-grid').classes()).toEqual(expect.arrayContaining([
            'ui-bento-grid--columns-3',
            'ui-bento-grid--gap-lg',
        ]));
        expect(wrapper.findAll('.ui-bento-item')).toHaveLength(2);
        expect(wrapper.get('.ui-bento-item').classes()).toContain('ui-bento-item--span-2');
        expect(wrapper.find('.ui-bento-item.ui-card').exists()).toBe(false);
    });
});

describe('KviewDatePicker', () => {
    it('normalizes Laravel timestamps and submits one native YYYY-MM-DD value', () => {
        const wrapper = mount(KviewDatePicker, {
            props: {
                id: 'notice_date',
                name: 'notice_date',
                label: 'Notice date',
                modelValue: '2026-02-03 00:00:00',
                locale: 'en-GB',
                error: 'The notice date is invalid.',
            },
        });
        const input = wrapper.get<HTMLInputElement>('input[type="date"]');
        const trigger = wrapper.get<HTMLButtonElement>('.ui-date-picker-trigger');

        expect(input.attributes('name')).toBe('notice_date');
        expect(input.element.value).toBe('2026-02-03');
        expect(input.attributes('aria-describedby')).toBe('notice_date_error');
        expect(input.attributes('aria-invalid')).toBe('true');
        expect(trigger.text()).toContain('03 Feb 2026');
        expect(wrapper.get('.ui-date-picker-trigger-label').attributes('title')).toBe('03 Feb 2026');
        expect(wrapper.get('.ui-date-picker-trigger-label-long').text()).toBe('03 Feb 2026');
        expect(wrapper.get('.ui-date-picker-trigger-label-short').text()).toBe('03/02/26');
        expect(trigger.attributes('aria-describedby')).toBe('notice_date_error');
    });

    it('teleports the calendar, supports grid keyboard movement and restores focus on Escape', async () => {
        const wrapper = mount({
            components: { KviewDatePicker },
            data: () => ({ value: '2026-02-03' }),
            template: `
                <KviewDatePicker
                    id="event_date"
                    v-model="value"
                    name="event_date"
                    label="Event date"
                    locale="en-GB"
                    min="2026-02-01"
                    max="2026-02-28"
                />
            `,
        }, { attachTo: document.body });
        const trigger = wrapper.get<HTMLButtonElement>('.ui-date-picker-trigger');

        trigger.element.focus();
        await trigger.trigger('click');
        await flush();

        const dialog = document.body.querySelector<HTMLElement>('#event_date_popover');
        const selected = dialog?.querySelector<HTMLButtonElement>('.ui-date-picker-day.is-selected');

        expect(dialog?.parentElement).toBe(document.body);
        expect(dialog?.getAttribute('role')).toBe('dialog');
        expect(document.activeElement).toBe(selected);

        selected?.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'ArrowRight',
            bubbles: true,
            cancelable: true,
        }));
        expect((document.activeElement as HTMLElement | null)?.textContent?.trim()).toBe('4');

        document.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Escape',
            bubbles: true,
            cancelable: true,
        }));
        await flush();

        expect(dialog?.hidden).toBe(true);
        expect(document.activeElement).toBe(trigger.element);

        await trigger.trigger('click');
        await flush();

        const tenth = Array.from(dialog?.querySelectorAll<HTMLButtonElement>('.ui-date-picker-day') ?? [])
            .find((day) => !day.classList.contains('is-outside') && day.textContent?.trim() === '10');
        tenth?.click();
        await flush();

        expect(wrapper.get<HTMLInputElement>('input[type="date"]').element.value).toBe('2026-02-10');
        wrapper.unmount();
    });
});
