import { mount } from '@vue/test-utils';
import { h, nextTick } from 'vue';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
    KviewBreadcrumbs,
    KviewCard,
    KviewFormActions,
    KviewMoneyInput,
    KviewPageHeader,
    KviewPercentInput,
    KviewResourceHeader,
    KviewSectionHeader,
    KviewTextarea,
    KviewTooltip,
    KviewUiProvider,
} from '@kview/ui-core/vue';

const rect = (
    left: number,
    top: number,
    width: number,
    height: number,
): DOMRect => ({
    x: left,
    y: top,
    left,
    top,
    width,
    height,
    right: left + width,
    bottom: top + height,
    toJSON: () => ({}),
});

afterEach(() => {
    document.body.innerHTML = '';
});

describe('KviewTooltip', () => {
    it('merges the focusable control description and opens from focus and pointer', async () => {
        const wrapper = mount(KviewTooltip, {
            attachTo: document.body,
            props: {
                id: 'portfolio-help',
                text: 'Afficher le portefeuille',
            },
            slots: {
                default: '<button type="button" aria-describedby="server-help">Portefeuille</button>',
            },
        });
        const button = wrapper.get<HTMLButtonElement>('button');

        await nextTick();

        expect(button.attributes('aria-describedby')).toBe('server-help portfolio-help');
        expect(wrapper.get('.ui-tooltip__trigger').attributes('tabindex')).toBeUndefined();

        button.element.focus();
        await nextTick();

        const tooltip = document.body.querySelector<HTMLElement>('#portfolio-help');
        expect(tooltip?.getAttribute('role')).toBe('tooltip');
        expect(tooltip?.getAttribute('aria-hidden')).toBe('false');
        expect(tooltip?.classList.contains('is-open')).toBe(true);

        await button.trigger('keydown', { key: 'Escape' });
        expect(tooltip?.getAttribute('aria-hidden')).toBe('true');
        expect(document.activeElement).toBe(button.element);

        await wrapper.get('.ui-tooltip').trigger('pointerenter', { pointerType: 'mouse' });
        expect(tooltip?.getAttribute('aria-hidden')).toBe('false');

        await wrapper.get('.ui-tooltip').trigger('pointerleave');
        expect(tooltip?.getAttribute('aria-hidden')).toBe('true');

        await wrapper.setProps({ id: 'portfolio-help-next' });
        await nextTick();

        expect(button.attributes('aria-describedby')).toBe('server-help portfolio-help-next');
        expect(document.body.querySelector('#portfolio-help')).toBeNull();
        expect(document.body.querySelector('#portfolio-help-next')).not.toBeNull();

        const detachedButton = button.element;
        wrapper.unmount();

        expect(detachedButton.getAttribute('aria-describedby')).toBe('server-help');
        expect(document.body.querySelector('#portfolio-help-next')).toBeNull();
    });

    it('ignores touch and pointer-restored focus while keeping real mouse hover', async () => {
        const wrapper = mount(KviewTooltip, {
            attachTo: document.body,
            props: {
                id: 'navigation-help',
                text: 'Afficher la navigation',
            },
            slots: {
                default: '<button type="button">Navigation</button>',
            },
        });
        const root = wrapper.get<HTMLElement>('.ui-tooltip');
        const button = wrapper.get<HTMLButtonElement>('button');
        const tooltip = document.body.querySelector<HTMLElement>('#navigation-help');
        const matches = vi.spyOn(button.element, 'matches').mockImplementation((selector) => (
            selector === ':focus-visible'
                ? false
                : Element.prototype.matches.call(button.element, selector)
        ));

        await root.trigger('pointerenter', { pointerType: 'touch' });
        expect(tooltip?.getAttribute('aria-hidden')).toBe('true');

        button.element.focus();
        await nextTick();
        expect(document.activeElement).toBe(button.element);
        expect(tooltip?.getAttribute('aria-hidden')).toBe('true');

        button.element.blur();
        matches.mockRestore();
        await root.trigger('pointerenter', { pointerType: 'mouse' });
        expect(tooltip?.getAttribute('aria-hidden')).toBe('false');

        await root.trigger('pointerleave');
        expect(tooltip?.getAttribute('aria-hidden')).toBe('true');
        wrapper.unmount();
    });

    it('flips a top tooltip below its trigger when it would leave the viewport', async () => {
        Object.defineProperty(document.documentElement, 'clientWidth', {
            configurable: true,
            value: 320,
        });
        Object.defineProperty(document.documentElement, 'clientHeight', {
            configurable: true,
            value: 480,
        });

        const wrapper = mount(KviewTooltip, {
            attachTo: document.body,
            props: {
                id: 'collision-help',
                text: 'Collision contrôlée',
                position: 'top',
            },
            slots: {
                default: 'Aide contextuelle',
            },
        });
        const trigger = wrapper.get<HTMLElement>('.ui-tooltip__trigger').element;
        const tooltip = document.body.querySelector<HTMLElement>('#collision-help');

        trigger.getBoundingClientRect = () => rect(100, 2, 40, 20);

        if (!tooltip) {
            throw new Error('The teleported tooltip was not rendered.');
        }

        tooltip.getBoundingClientRect = () => rect(0, 0, 120, 32);

        await wrapper.get('.ui-tooltip').trigger('pointerenter');
        await nextTick();

        expect(tooltip.dataset.placement).toBe('bottom');
        expect(tooltip.style.top).toBe('30px');
        expect(Number.parseFloat(tooltip.style.left)).toBeGreaterThanOrEqual(8);
        expect(Number.parseFloat(tooltip.style.left)).toBeLessThanOrEqual(192);

        document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
        await nextTick();
        expect(tooltip.getAttribute('aria-hidden')).toBe('true');

        await wrapper.get('.ui-tooltip').trigger('pointerenter');
        await wrapper.setProps({ disabled: true });
        await nextTick();
        expect(document.body.querySelector('#collision-help')).toBeNull();

        await wrapper.setProps({ disabled: false });
        await nextTick();
        const restoredTooltip = document.body.querySelector<HTMLElement>('#collision-help');
        expect(restoredTooltip?.getAttribute('aria-hidden')).toBe('true');
        expect(wrapper.get('.ui-tooltip__trigger').attributes('tabindex')).toBe('0');

        await wrapper.get('.ui-tooltip').trigger('pointerenter');
        await wrapper.setProps({ text: null });
        await nextTick();
        expect(restoredTooltip?.getAttribute('aria-hidden')).toBe('true');
        expect(wrapper.get('.ui-tooltip__trigger').attributes('tabindex')).toBeUndefined();

        wrapper.unmount();
    });
});

describe('phase 3 Vue navigation and header adapters', () => {
    it('shares one compact card surface and the record-detail header variant', () => {
        const wrapper = mount(KviewCard, {
            props: {
                compact: true,
                bodyClass: 'fund-card-body',
                bodyAttrs: {
                    'aria-busy': 'true',
                },
            },
            slots: {
                header: () => h(KviewPageHeader, {
                    title: 'EUR / USD',
                    backHref: '/exchange-rates',
                    variant: 'detail',
                }),
                default: '<p>Exchange-rate detail</p>',
            },
        });

        expect(wrapper.classes()).toContain('ui-card--compact');
        expect(wrapper.get('.ui-page-header').classes()).toContain('ui-page-header--detail');
        expect(wrapper.get('.ui-card__body').classes()).toContain('fund-card-body');
        expect(wrapper.get('.ui-card__body').attributes('aria-busy')).toBe('true');
        expect(wrapper.get('h1').text()).toBe('EUR / USD');
        expect(wrapper.text()).toContain('Exchange-rate detail');
        expect(wrapper.html()).not.toContain('data-ui-');
    });

    it('renders localized breadcrumbs with native links and a current page', () => {
        const wrapper = mount(KviewUiProvider, {
            props: {
                messages: {
                    navigation: { breadcrumb: "Fil d’Ariane" },
                },
            },
            slots: {
                default: () => h(KviewBreadcrumbs, {
                    items: [
                        { label: 'Accueil', href: '/home', icon: 'home' },
                        { label: 'Funds', href: '/funds' },
                        { label: 'North Atlantic', href: '/funds/7', current: true },
                    ],
                }),
            },
        });
        const breadcrumb = wrapper.get('nav');

        expect(breadcrumb.attributes('aria-label')).toBe("Fil d’Ariane");
        expect(breadcrumb.findAll('li')).toHaveLength(3);
        expect(breadcrumb.findAll('a').map((link) => link.attributes('href'))).toEqual([
            '/home',
            '/funds',
        ]);
        expect(breadcrumb.get('[aria-current="page"]').text()).toBe('North Atlantic');
        expect(breadcrumb.findAll('.ui-breadcrumb-separator')).toHaveLength(2);
    });

    it('keeps page, resource and section heading levels semantic and resolves resource icons', () => {
        const wrapper = mount(KviewUiProvider, {
            props: {
                resourceIcons: {
                    fund: 'portfolio',
                },
            },
            slots: {
                default: () => [
                    h(KviewPageHeader, {
                        resource: 'fund',
                        title: 'Fund workspace',
                        backHref: '/funds',
                    }),
                    h(KviewResourceHeader, {
                        resource: 'fund',
                        title: 'Investments',
                        description: 'Portfolio positions.',
                        compact: true,
                    }, {
                        actions: () => h('button', { type: 'button' }, 'Ajouter'),
                    }),
                    h(KviewSectionHeader, {
                        title: 'Capital accounts',
                        headingLevel: 4,
                        icon: 'chart',
                    }),
                ],
            },
        });

        expect(wrapper.get('.ui-page-header h1').text()).toBe('Fund workspace');
        expect(wrapper.get('.ui-page-header__back').attributes('href')).toBe('/funds');
        expect(wrapper.get('.ui-resource-header h2').text()).toBe('Investments');
        expect(wrapper.get('.ui-resource-header').classes()).toContain('ui-resource-header--compact');
        expect(wrapper.get('.ui-resource-header__actions button').text()).toBe('Ajouter');
        expect(wrapper.get('.ui-section-header h4').text()).toBe('Capital accounts');
        expect(wrapper.findAll('.ui-resource-heading__icon')).toHaveLength(3);
        expect(wrapper.html()).not.toContain('data-ui-');
    });
});

describe('phase 3 Vue form adapters', () => {
    it('renders localized default actions and a disabled loading submit state', () => {
        const wrapper = mount(KviewUiProvider, {
            props: {
                messages: {
                    common: {
                        cancel: 'Annuler',
                        save: 'Enregistrer',
                        saving: 'Enregistrement...',
                    },
                },
            },
            slots: {
                default: () => h(KviewFormActions, {
                    cancelHref: '/funds',
                    loading: true,
                    align: 'end',
                    sticky: true,
                }),
            },
        });
        const actions = wrapper.get('.ui-form-actions');
        const submit = actions.get('button[type="submit"]');

        expect(actions.classes()).toEqual(expect.arrayContaining([
            'ui-form-actions--end',
            'ui-form-actions--sticky',
        ]));
        expect(submit.text()).toBe('Enregistrement...');
        expect(submit.attributes('disabled')).toBeDefined();
        expect(submit.attributes('aria-busy')).toBe('true');
        expect(actions.get('a').attributes('href')).toBe('/funds');
        expect(actions.get('a').text()).toBe('Annuler');
    });

    it('connects textarea hint, error and external descriptions without losing v-model', async () => {
        const wrapper = mount(KviewTextarea, {
            props: {
                name: 'investment_notes',
                label: 'Notes',
                modelValue: 'Initial',
                hint: 'Visible by the investment team.',
                error: 'Notes are required.',
                required: true,
                rows: 6,
            },
            attrs: {
                'aria-describedby': 'server-help investment_notes_hint server-help',
                'aria-invalid': true,
                'aria-busy': true,
            },
        });
        const textarea = wrapper.get<HTMLTextAreaElement>('textarea');

        expect(textarea.attributes('rows')).toBe('6');
        expect(textarea.attributes('aria-describedby')).toBe(
            'investment_notes_hint investment_notes_error server-help',
        );
        expect(textarea.attributes('aria-invalid')).toBe('true');
        expect(textarea.attributes('aria-busy')).toBe('true');
        expect(textarea.attributes('required')).toBeDefined();

        await wrapper.setProps({ error: null });

        expect(textarea.attributes('aria-invalid')).toBe('true');

        await textarea.setValue('Updated without transformation');

        expect(wrapper.emitted('update:modelValue')).toEqual([['Updated without transformation']]);

        await wrapper.setProps({ loading: true });

        expect(textarea.attributes('disabled')).toBeDefined();
        expect(textarea.attributes('aria-busy')).toBe('true');
    });

    it('keeps money values unscaled while exposing currency and scaling metadata', async () => {
        const wrapper = mount(KviewMoneyInput, {
            props: {
                name: 'commitment',
                label: 'Commitment',
                modelValue: '1250000.50',
                currency: 'EUR',
                scaling: 'million',
            },
        });
        const input = wrapper.get<HTMLInputElement>('input');

        expect(wrapper.get('.ui-input-adornment--prefix').text()).toBe('EUR');
        expect(input.attributes('inputmode')).toBe('decimal');
        expect(input.attributes('data-scaling')).toBe('million');
        expect(input.element.value).toBe('1250000.50');

        await input.setValue('2500000.75');

        expect(wrapper.emitted('update:modelValue')).toEqual([['2500000.75']]);
    });

    it('keeps percent values unchanged and renders a decorative percent suffix', async () => {
        const wrapper = mount(KviewPercentInput, {
            props: {
                name: 'irr',
                label: 'IRR',
                modelValue: '18.75',
            },
        });
        const input = wrapper.get<HTMLInputElement>('input');
        const suffix = wrapper.get('.ui-input-adornment--suffix');

        expect(input.attributes('inputmode')).toBe('decimal');
        expect(input.element.value).toBe('18.75');
        expect(suffix.text()).toBe('%');
        expect(suffix.attributes('aria-hidden')).toBe('true');

        await input.setValue('19.125');

        expect(wrapper.emitted('update:modelValue')).toEqual([['19.125']]);
    });
});
