import { mount } from '@vue/test-utils';
import { h, nextTick } from 'vue';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
    KviewAvatar,
    KviewPopover,
} 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: () => ({}),
});

const flushFrame = async (): Promise<void> => {
    await nextTick();
    await new Promise<void>((resolve) => {
        window.requestAnimationFrame(() => resolve());
    });
    await nextTick();
};

const originalVisualViewport = window.visualViewport;
const originalResizeObserver = window.ResizeObserver;

beforeEach(() => {
    Object.defineProperty(document.documentElement, 'clientWidth', {
        configurable: true,
        value: 320,
    });
    Object.defineProperty(document.documentElement, 'clientHeight', {
        configurable: true,
        value: 480,
    });
});

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

describe('KviewPopover', () => {
    it('lets only the topmost open popover react to Escape and each outside pointer', async () => {
        const PopoverStackHarness = {
            components: { KviewPopover },
            template: `
                <div>
                    <KviewPopover
                        id="first-popover"
                        trigger-label="Open first popover"
                    >
                        <template #trigger>First</template>
                        <button type="button">First action</button>
                    </KviewPopover>
                    <KviewPopover
                        id="second-popover"
                        trigger-label="Open second popover"
                    >
                        <template #trigger>Second</template>
                        <button type="button">Second action</button>
                    </KviewPopover>
                </div>
            `,
        };
        const wrapper = mount(PopoverStackHarness, {
            attachTo: document.body,
        });
        const triggers = wrapper.findAll<HTMLButtonElement>('.ui-popover__trigger');
        const firstTrigger = triggers[0];
        const secondTrigger = triggers[1];
        const firstPanel = document.body.querySelector<HTMLElement>('#first-popover');
        const secondPanel = document.body.querySelector<HTMLElement>('#second-popover');

        if (!firstTrigger || !secondTrigger || !firstPanel || !secondPanel) {
            throw new Error('The two popovers were not rendered.');
        }

        await firstTrigger.trigger('click');
        await flushFrame();
        await secondTrigger.trigger('click');
        await flushFrame();

        expect(firstPanel.hidden).toBe(false);
        expect(secondPanel.hidden).toBe(false);

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

        expect(secondPanel.hidden).toBe(true);
        expect(firstPanel.hidden).toBe(false);
        expect(document.activeElement).toBe(secondTrigger.element);

        await secondTrigger.trigger('click');
        await flushFrame();

        const outside = document.createElement('button');
        outside.type = 'button';
        outside.textContent = 'Outside both popovers';
        document.body.appendChild(outside);
        outside.focus();
        outside.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
        await nextTick();

        expect(secondPanel.hidden).toBe(true);
        expect(firstPanel.hidden).toBe(false);
        expect(document.activeElement).toBe(outside);

        outside.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
        await nextTick();

        expect(firstPanel.hidden).toBe(true);
        expect(document.activeElement).toBe(outside);

        wrapper.unmount();
    });

    it('teleports, flips inside the viewport, restores focus on Escape and not on outside pointer', async () => {
        const wrapper = mount(KviewPopover, {
            attachTo: document.body,
            props: {
                id: 'account-popover',
                triggerLabel: 'Account menu for Ada',
                labelledby: 'account-popover-title',
                initialFocusSelector: '[data-close-account]',
            },
            slots: {
                trigger: ({ open }: { open: boolean }) => h('span', open ? 'Ada open' : 'Ada'),
                default: ({ close }: { close: (restoreFocus?: boolean) => void }) => [
                    h('h2', { id: 'account-popover-title' }, 'Ada Lovelace'),
                    h('button', {
                        type: 'button',
                        'data-close-account': '',
                        onClick: () => close(),
                    }, 'Close account'),
                    h('a', { href: '/settings' }, 'Settings'),
                ],
            },
        });
        const trigger = wrapper.get<HTMLButtonElement>('.ui-popover__trigger');
        const panel = document.body.querySelector<HTMLElement>('#account-popover');

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

        expect(panel.parentElement).toBe(document.body);
        expect(wrapper.element.contains(panel)).toBe(false);
        expect(panel.hidden).toBe(true);
        expect(panel.getAttribute('role')).toBe('dialog');
        expect(panel.getAttribute('aria-labelledby')).toBe('account-popover-title');
        expect(panel.getAttribute('aria-label')).toBeNull();
        expect(trigger.attributes('aria-haspopup')).toBe('dialog');
        expect(trigger.attributes('aria-controls')).toBe('account-popover');
        expect(trigger.attributes('aria-expanded')).toBe('false');

        trigger.element.getBoundingClientRect = () => rect(280, 430, 30, 30);
        panel.getBoundingClientRect = () => rect(0, 0, 180, 140);
        trigger.element.focus();
        await trigger.trigger('click');
        await flushFrame();

        expect(wrapper.emitted('update:open')?.[0]).toEqual([true]);
        expect(trigger.attributes('aria-expanded')).toBe('true');
        expect(panel.hidden).toBe(false);
        expect(panel.dataset.placement).toBe('top-end');
        expect(panel.style.top).toBe('282px');
        expect(panel.style.left).toBe('130px');
        expect(document.activeElement).toBe(panel.querySelector('[data-close-account]'));

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

        expect(trigger.attributes('aria-expanded')).toBe('false');
        expect(panel.hidden).toBe(true);
        expect(document.activeElement).toBe(trigger.element);

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

        const outside = document.createElement('button');
        outside.type = 'button';
        outside.textContent = 'Outside';
        document.body.appendChild(outside);
        outside.focus();
        outside.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
        await nextTick();

        expect(panel.hidden).toBe(true);
        expect(document.activeElement).toBe(outside);
        expect(document.activeElement).not.toBe(trigger.element);

        const exposed = wrapper.vm as unknown as {
            focusTrigger: () => void;
        };
        exposed.focusTrigger();
        expect(document.activeElement).toBe(trigger.element);
        expect(document.body.innerHTML).not.toContain('data-ui-');

        wrapper.unmount();
        expect(document.body.querySelector('#account-popover')).toBeNull();
    });

    it('cleans resize, scroll, visual viewport and observer resources on unmount', async () => {
        const visualAdd = vi.fn();
        const visualRemove = vi.fn();
        const observe = vi.fn();
        const disconnect = vi.fn();
        const windowAdd = vi.spyOn(window, 'addEventListener');
        const windowRemove = vi.spyOn(window, 'removeEventListener');

        Object.defineProperty(window, 'visualViewport', {
            configurable: true,
            value: {
                width: 300,
                height: 440,
                offsetLeft: 5,
                offsetTop: 7,
                addEventListener: visualAdd,
                removeEventListener: visualRemove,
            } as unknown as VisualViewport,
        });
        Object.defineProperty(window, 'ResizeObserver', {
            configurable: true,
            value: class {
                observe = observe;

                disconnect = disconnect;
            },
        });

        const wrapper = mount(KviewPopover, {
            attachTo: document.body,
            props: {
                open: true,
                triggerLabel: 'Open commands',
            },
            slots: {
                trigger: 'Commands',
                default: '<button type="button">First command</button>',
            },
        });

        await flushFrame();

        expect(observe).toHaveBeenCalledTimes(2);
        expect(visualAdd).toHaveBeenCalledWith('resize', expect.any(Function));
        expect(visualAdd).toHaveBeenCalledWith('scroll', expect.any(Function));
        expect(windowAdd).toHaveBeenCalledWith('resize', expect.any(Function));
        expect(windowAdd).toHaveBeenCalledWith('scroll', expect.any(Function), true);

        wrapper.unmount();

        expect(disconnect).toHaveBeenCalledOnce();
        expect(visualRemove).toHaveBeenCalledWith('resize', expect.any(Function));
        expect(visualRemove).toHaveBeenCalledWith('scroll', expect.any(Function));
        expect(windowRemove).toHaveBeenCalledWith('resize', expect.any(Function));
        expect(windowRemove).toHaveBeenCalledWith('scroll', expect.any(Function), true);
    });
});

describe('KviewAvatar', () => {
    it('renders at most two initials and falls back when the image cannot load', async () => {
        const wrapper = mount(KviewAvatar, {
            props: {
                name: 'Ada Augusta Lovelace',
                size: 'small',
                label: 'Profile picture for Ada Augusta Lovelace',
            },
        });
        const root = wrapper.get('.ui-avatar');

        expect(root.text()).toBe('AA');
        expect(root.classes()).toContain('ui-avatar--small');
        expect(root.attributes('role')).toBe('img');
        expect(root.attributes('aria-label')).toBe('Profile picture for Ada Augusta Lovelace');
        expect(wrapper.find('img').exists()).toBe(false);

        await wrapper.setProps({ src: '/settings/avatar?v=1' });

        const image = wrapper.get('img');
        expect(image.attributes('src')).toBe('/settings/avatar?v=1');
        expect(image.attributes('alt')).toBe('');
        expect(image.attributes('loading')).toBe('lazy');
        expect(root.classes()).toContain('ui-avatar--image');

        await image.trigger('error');

        expect(wrapper.find('img').exists()).toBe(false);
        expect(root.text()).toBe('AA');
        expect(root.classes()).not.toContain('ui-avatar--image');

        await wrapper.setProps({ src: '/settings/avatar?v=2' });

        expect(wrapper.get('img').attributes('src')).toBe('/settings/avatar?v=2');
    });

    it('uses a stable fallback and remains decorative without an explicit label', () => {
        const wrapper = mount(KviewAvatar, {
            props: { name: ' ' },
        });

        expect(wrapper.get('.ui-avatar').text()).toBe('U');
        expect(wrapper.get('.ui-avatar').attributes('aria-hidden')).toBe('true');
        expect(wrapper.get('.ui-avatar').attributes('role')).toBeUndefined();
    });
});
