import { DOMWrapper, flushPromises, mount } from '@vue/test-utils';
import {
    defineComponent,
    h,
    nextTick,
    type PropType,
} from 'vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { KviewToastProvider } from '@kview/ui-core/vue';

const routerGet = vi.hoisted(() => vi.fn());
const routerPost = vi.hoisted(() => vi.fn());
const routerEvents = vi.hoisted(() => ({
    on: vi.fn(),
    listeners: {
        start: [],
        finish: [],
    } as Record<string, Array<(event: CustomEvent) => void>>,
}));

vi.mock('@inertiajs/vue3', async () => {
    const { defineComponent: defineVueComponent, h: createElement } = await import('vue');

    return {
        Link: defineVueComponent({
            name: 'InertiaLinkStub',
            inheritAttrs: false,
            props: {
                href: {
                    type: String,
                    required: true,
                },
            },
            emits: ['click'],
            setup(props, { attrs, emit, slots }) {
                return () => createElement('a', {
                    ...attrs,
                    href: props.href,
                    'data-inertia-link': 'true',
                    onClick: (event: MouseEvent) => {
                        event.preventDefault();
                        emit('click', event);
                    },
                }, slots.default?.());
            },
        }),
        router: {
            get: routerGet,
            post: routerPost,
            on: routerEvents.on,
        },
    };
});

import AppLink from '@/components/application/AppLink.vue';
import ApplicationShell from '@/components/application/ApplicationShell.vue';
import type {
    ApplicationNavigationProps,
    AuthProps,
    FlashProps,
} from '@/types/inertia';
import type { SettingsPayload } from '@/types/settings';

interface MatchMediaController {
    setMatches: (matches: boolean) => void;
}

const installMatchMedia = (initialMatches: boolean): MatchMediaController => {
    let matches = initialMatches;
    const listeners = new Set<(event: MediaQueryListEvent) => void>();
    const media = '(max-width: 767px)';
    const query = {
        get matches() {
            return matches;
        },
        media,
        onchange: null,
        addEventListener: (
            _type: string,
            listener: EventListenerOrEventListenerObject,
        ) => {
            if (typeof listener === 'function') {
                listeners.add(listener as (event: MediaQueryListEvent) => void);
            }
        },
        removeEventListener: (
            _type: string,
            listener: EventListenerOrEventListenerObject,
        ) => {
            if (typeof listener === 'function') {
                listeners.delete(listener as (event: MediaQueryListEvent) => void);
            }
        },
        addListener: (
            listener: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null,
        ) => {
            if (listener) {
                listeners.add(listener);
            }
        },
        removeListener: (
            listener: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null,
        ) => {
            if (listener) {
                listeners.delete(listener);
            }
        },
        dispatchEvent: (event: Event) => {
            listeners.forEach((listener) => listener(event as MediaQueryListEvent));

            return true;
        },
    } as MediaQueryList;

    Object.defineProperty(window, 'matchMedia', {
        configurable: true,
        writable: true,
        value: vi.fn(() => query),
    });

    return {
        setMatches(nextMatches: boolean) {
            matches = nextMatches;
            query.dispatchEvent({ matches, media } as MediaQueryListEvent);
        },
    };
};

const navigation: ApplicationNavigationProps = {
    brand: 'kView INVESTOR',
    byline: 'by kView',
    version: '3.5-test',
    logoUrl: '/images/logo.svg',
    homeUrl: '/home',
    searchUrl: '/search',
    searchSuggestUrl: '/search/suggest',
    searchInertia: true,
    searchPresets: [
        { label: 'Funds', query: 'fund' },
        { label: 'GPs', query: 'gp' },
        { label: 'Investors', query: 'investor' },
    ],
    searchDefaultActions: [
        {
            title: 'Create new fund',
            eyebrow: 'Action',
            kind: 'create',
            meta: 'Funds',
            icon: 'plus',
            url: '/funds/create',
            inertia: true,
        },
        {
            title: 'Create new GP',
            eyebrow: 'Action',
            kind: 'create',
            meta: 'General Partners',
            icon: 'building',
            url: '/gps/create',
            inertia: true,
        },
        {
            title: 'Create new investor',
            eyebrow: 'Action',
            kind: 'create',
            meta: 'Investors',
            icon: 'user',
            url: '/investors/create',
            inertia: true,
        },
    ],
    logoutUrl: '/logout',
    settings: {
        dataUrl: '/settings',
    },
    account: {
        workspaceLabel: 'kView INVESTOR by kView',
        userSettingsUrl: '/settings?tab=user',
        dataStatusUrl: '/settings?tab=system',
        workspaceUrl: '/settings?tab=workspace',
        excelTokenUrl: '/api/protocol-token/create',
    },
    items: [
        {
            id: 'home',
            route: 'home',
            href: '/home',
            label: 'Home',
            icon: 'home',
            section: 'Workspace',
            active: true,
            footer: false,
            modal: null,
            inertia: false,
        },
        {
            id: 'funds',
            route: 'funds.index',
            href: '/funds',
            label: 'Funds',
            icon: 'portfolio',
            section: 'Records',
            active: false,
            footer: false,
            modal: null,
            inertia: true,
        },
        {
            id: 'settings',
            route: 'settings.index',
            href: '/settings',
            label: 'Settings',
            icon: 'settings',
            section: null,
            active: false,
            footer: true,
            modal: 'settings',
            inertia: false,
        },
    ],
};

const auth: AuthProps = {
    user: {
        id: 7,
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        avatarUrl: '/settings/avatar?v=7',
    },
    permissions: {
        admin: true,
    },
};

const flash: FlashProps = {
    id: 'flash-shell-1',
    success: 'Navigation prête.',
    error: null,
    warning: null,
    info: null,
    status: null,
    toasts: [],
};

const settingsPayload: SettingsPayload = {
    activeTab: 'user',
    tabs: [
        { id: 'user', label: 'Account', icon: 'user', group: 'Personal' },
        { id: 'preferences', label: 'Preferences', icon: 'sliders', group: 'Personal' },
        { id: 'workspace', label: 'Workspace', icon: 'building', group: 'Workspace' },
        { id: 'integrations', label: 'Integrations', icon: 'spreadsheet', group: 'Workspace' },
        { id: 'system', label: 'System', icon: 'status', group: 'Operations' },
    ],
    returnUrl: '/',
    dataUrl: '/settings',
    account: {
        name: 'Ada Lovelace',
        email: 'ada@example.com',
        avatarUrl: '/settings/avatar?v=7',
        hasAvatar: true,
        profileUrl: '/settings/profile',
        passwordUrl: '/settings/password',
    },
    preferences: {
        updateUrl: '/preferences',
        values: {
            targetCurrency: 'EUR',
            itemsPerPage: '25',
            amountScaling: 'unit',
            decimalSeparator: '.',
            dateFormatView: 'Y-m-d',
            dateFormatList: 'Y-m-d',
        },
        options: {
            targetCurrencies: [{ value: 'EUR', label: 'EUR' }],
            itemsPerPage: [{ value: '25', label: '25' }],
            amountScalings: [{ value: 'unit', label: 'Unit' }],
            decimalSeparators: [{ value: '.', label: '.' }],
            dateFormats: [{ value: 'Y-m-d', label: 'Y-m-d' }],
        },
    },
    workspace: {
        label: 'kView INVESTOR by kView',
        signedInAs: 'ada@example.com',
    },
    integrations: {
        excelTokenUrl: '/api/protocol-token/create',
    },
    system: {
        application: [{ label: 'Application', value: 'kView INVESTOR' }],
        database: [{ label: 'PostgreSQL', value: '16' }],
    },
};

const ShellHarness = defineComponent({
    name: 'ShellHarness',
    props: {
        searchQuery: { type: String, default: '' },
        searchUsesInertia: { type: Boolean, default: true },
        flash: {
            type: Object as PropType<FlashProps>,
            default: () => flash,
        },
    },
    setup(harnessProps) {
        return () => h(KviewToastProvider, null, {
            default: () => h(ApplicationShell, {
                navigation,
                auth,
                flash: harnessProps.flash,
                searchQuery: harnessProps.searchQuery,
                searchUsesInertia: harnessProps.searchUsesInertia,
            }, {
                default: () => h('section', { 'data-page-content': '' }, 'Page content'),
            }),
        });
    },
});

const mountShell = (
    searchQuery = '',
    searchUsesInertia = true,
    shellFlash: FlashProps = flash,
) => mount(ShellHarness, {
    attachTo: document.body,
    props: {
        searchQuery,
        searchUsesInertia,
        flash: shellFlash,
    },
});

const searchInput = (): DOMWrapper<HTMLInputElement> => {
    const element = document.body.querySelector<HTMLInputElement>('#application-search');

    if (!element) {
        throw new Error('Expected the application search input to be teleported inside its dialog.');
    }

    return new DOMWrapper(element);
};

const searchForm = (): DOMWrapper<HTMLFormElement> => {
    const element = document.body.querySelector<HTMLFormElement>('.application-global-search__panel form[role="search"]');

    if (!element) {
        throw new Error('Expected the application search form to be teleported inside its dialog.');
    }

    return new DOMWrapper(element);
};

const emitRouterEvent = (
    event: 'start' | 'finish',
    visit: Record<string, unknown>,
): void => {
    const routerEvent = new CustomEvent(event, { detail: { visit } });

    routerEvents.listeners[event]?.forEach((listener) => listener(routerEvent));
};

beforeEach(() => {
    document.body.innerHTML = '';
    document.documentElement.classList.remove('app-sidebar-hidden');
    window.localStorage.clear();
    routerGet.mockReset();
    routerPost.mockReset();
    routerEvents.listeners.start.splice(0);
    routerEvents.listeners.finish.splice(0);
    routerEvents.on.mockReset().mockImplementation((event: string, listener: (event: CustomEvent) => void) => {
        routerEvents.listeners[event]?.push(listener);

        return vi.fn(() => {
            const listeners = routerEvents.listeners[event] ?? [];
            const index = listeners.indexOf(listener);

            if (index >= 0) {
                listeners.splice(index, 1);
            }
        });
    });
});

describe('AppLink', () => {
    it('uses a native anchor by default and forwards accessible link state', async () => {
        const wrapper = mount(AppLink, {
            props: {
                href: '#native',
                accessibleLabel: 'Ouvrir les funds',
                current: 'page',
            },
            attrs: {
                class: 'consumer-link',
            },
            slots: {
                default: 'Funds',
            },
        });
        const link = wrapper.get('a');

        expect(link.attributes('href')).toBe('#native');
        expect(link.attributes('aria-label')).toBe('Ouvrir les funds');
        expect(link.attributes('aria-current')).toBe('page');
        expect(link.classes()).toContain('consumer-link');
        expect(link.attributes('data-inertia-link')).toBeUndefined();

        await link.trigger('click');

        expect(wrapper.emitted('click')).toHaveLength(1);
    });

    it('uses the explicit Inertia seam without changing anchor semantics', async () => {
        const wrapper = mount(AppLink, {
            props: {
                href: '/funds',
                inertia: true,
                accessibleLabel: 'Ouvrir les funds avec Inertia',
            },
            slots: {
                default: 'Funds',
            },
        });
        const link = wrapper.get('a');

        expect(link.attributes('href')).toBe('/funds');
        expect(link.attributes('data-inertia-link')).toBe('true');
        expect(link.attributes('aria-label')).toBe('Ouvrir les funds avec Inertia');

        await link.trigger('click');

        expect(wrapper.emitted('click')).toHaveLength(1);
    });
});

describe('ApplicationShell', () => {
    it('shows a delayed content skeleton only for the latest cross-path Inertia visit', async () => {
        vi.useFakeTimers();
        installMatchMedia(false);
        const wrapper = mountShell();

        try {
            const main = wrapper.get('#application-content');
            expect(main.attributes('scroll-region')).toBe('');
            const firstVisit = {
                id: 'visit-a',
                url: new URL('/search', window.location.href),
                prefetch: false,
                showProgress: true,
            };
            const secondVisit = {
                ...firstVisit,
                id: 'visit-b',
                url: new URL('/__kview/inertia/application-shell/secondary', window.location.href),
            };

            emitRouterEvent('start', firstVisit);
            await nextTick();

            expect(main.attributes('aria-busy')).toBe('true');
            expect(wrapper.find('.application-shell__navigation-skeleton').exists()).toBe(false);

            await vi.advanceTimersByTimeAsync(119);
            expect(wrapper.find('.application-shell__navigation-skeleton').exists()).toBe(false);

            await vi.advanceTimersByTimeAsync(1);
            await nextTick();
            expect(wrapper.get('[role="status"][aria-label="Chargement de la page"]')).toBeTruthy();

            emitRouterEvent('start', secondVisit);
            emitRouterEvent('finish', firstVisit);
            await nextTick();

            expect(main.attributes('aria-busy')).toBe('true');

            await vi.advanceTimersByTimeAsync(120);
            emitRouterEvent('finish', secondVisit);
            await nextTick();

            expect(main.attributes('aria-busy')).toBeUndefined();
            expect(wrapper.find('.application-shell__navigation-skeleton').exists()).toBe(false);
        } finally {
            wrapper.unmount();
            vi.useRealTimers();
        }

        expect(routerEvents.listeners.start).toHaveLength(0);
        expect(routerEvents.listeners.finish).toHaveLength(0);
    });

    it('keeps same-path visits local and ignores prefetch or progress-free work', async () => {
        vi.useFakeTimers();
        installMatchMedia(false);
        const wrapper = mountShell();

        try {
            const main = wrapper.get('#application-content');
            const samePathVisit = {
                id: 'visit-query',
                url: new URL(`${window.location.pathname}?q=demo`, window.location.href),
                prefetch: false,
                showProgress: true,
            };

            emitRouterEvent('start', samePathVisit);
            await vi.advanceTimersByTimeAsync(200);
            await nextTick();

            expect(main.attributes('aria-busy')).toBe('true');
            expect(wrapper.find('.application-shell__navigation-skeleton').exists()).toBe(false);

            emitRouterEvent('finish', samePathVisit);
            emitRouterEvent('start', {
                ...samePathVisit,
                id: 'visit-prefetch',
                prefetch: true,
            });
            emitRouterEvent('start', {
                ...samePathVisit,
                id: 'visit-background',
                showProgress: false,
            });
            await vi.advanceTimersByTimeAsync(200);
            await nextTick();

            expect(main.attributes('aria-busy')).toBeUndefined();
            expect(wrapper.find('.application-shell__navigation-skeleton').exists()).toBe(false);
        } finally {
            wrapper.unmount();
            vi.useRealTimers();
        }
    });

    it('renders the saved desktop sidebar state on its first Vue commit', () => {
        installMatchMedia(false);
        window.localStorage.setItem('kview_investor.sidebar.hidden', 'true');
        const wrapper = mountShell();

        try {
            const shell = wrapper.get('.application-shell');
            const sidebar = wrapper.get('#application-navigation');
            const toggle = wrapper.get<HTMLButtonElement>('button[aria-controls="application-navigation"]');

            expect(shell.classes()).toContain('is-sidebar-collapsed');
            expect(document.documentElement.classList).toContain('app-sidebar-hidden');
            expect(sidebar.attributes('aria-hidden')).toBe('true');
            expect(sidebar.element.hasAttribute('inert')).toBe(true);
            expect(toggle.attributes('aria-expanded')).toBe('false');
        } finally {
            wrapper.unmount();
        }
    });

    it('keeps the mobile drawer transient and restores the saved desktop state at the breakpoint', async () => {
        const media = installMatchMedia(true);
        window.localStorage.setItem('kview_investor.sidebar.hidden', 'true');
        const wrapper = mountShell();

        try {
            const shell = wrapper.get('.application-shell');
            const sidebar = wrapper.get('#application-navigation');
            const toggle = wrapper.get<HTMLButtonElement>('button[aria-controls="application-navigation"]');

            expect(shell.classes()).not.toContain('is-sidebar-collapsed');
            expect(sidebar.attributes('aria-hidden')).toBe('true');
            expect(toggle.attributes('aria-expanded')).toBe('false');

            await toggle.trigger('click');
            await nextTick();

            expect(shell.classes()).toContain('is-mobile-navigation-open');
            expect(window.localStorage.getItem('kview_investor.sidebar.hidden')).toBe('true');

            media.setMatches(false);
            await nextTick();

            expect(shell.classes()).toContain('is-sidebar-collapsed');
            expect(sidebar.attributes('aria-hidden')).toBe('true');
            expect(toggle.attributes('aria-expanded')).toBe('false');
        } finally {
            wrapper.unmount();
        }
    });

    it('deduplicates flash ids and renders every flash variant plus structured toasts', async () => {
        installMatchMedia(false);
        const firstFlash: FlashProps = {
            id: 'flash-batch-a',
            success: 'Success A',
            error: 'Error A',
            warning: 'Warning A',
            info: 'Info A',
            status: 'Status A',
            toasts: [{
                id: 'structured-a',
                type: 'warning',
                title: 'Structured title A',
                message: 'Structured message A',
                icon: 'settings',
                duration: 0,
            }],
        };
        const wrapper = mountShell('', true, firstFlash);

        try {
            await nextTick();

            const initialToasts = Array.from(
                document.body.querySelectorAll<HTMLElement>('.ui-toast'),
            );

            expect(initialToasts).toHaveLength(6);
            expect(document.body.querySelector('.ui-toast--success')?.textContent).toContain('Success A');
            expect(document.body.querySelector('.ui-toast--error[role="alert"]')?.textContent).toContain('Error A');
            expect(document.body.querySelector('.ui-toast--warning')?.textContent).toContain('Warning A');
            expect(initialToasts.some((toast) => toast.textContent?.includes('Info A'))).toBe(true);
            expect(initialToasts.some((toast) => toast.textContent?.includes('Status A'))).toBe(true);

            const structuredA = initialToasts.find((toast) => (
                toast.textContent?.includes('Structured title A')
            ));
            expect(structuredA?.classList.contains('ui-toast--warning')).toBe(true);
            expect(structuredA?.textContent).toContain('Structured message A');
            expect(structuredA?.style.getPropertyValue('--toast-duration')).toBe('0ms');

            await wrapper.setProps({
                flash: {
                    ...firstFlash,
                    success: 'Duplicate success must not render',
                    toasts: [{
                        id: 'structured-duplicate',
                        message: 'Duplicate structured toast must not render',
                        duration: 0,
                    }],
                },
            });
            await nextTick();

            expect(document.body.querySelectorAll('.ui-toast')).toHaveLength(6);
            expect(document.body.textContent).not.toContain('Duplicate success must not render');
            expect(document.body.textContent).not.toContain('Duplicate structured toast must not render');

            await wrapper.setProps({
                flash: {
                    id: 'flash-batch-b',
                    success: 'Success B',
                    error: 'Error B',
                    warning: 'Warning B',
                    info: 'Info B',
                    status: 'Status B',
                    toasts: [{
                        id: 'structured-b',
                        type: 'success',
                        title: 'Structured title B',
                        message: 'Structured message B',
                        duration: 0,
                    }],
                },
            });
            await nextTick();

            const updatedToasts = Array.from(
                document.body.querySelectorAll<HTMLElement>('.ui-toast'),
            );

            expect(updatedToasts).toHaveLength(12);
            expect(updatedToasts.some((toast) => toast.textContent?.includes('Success B'))).toBe(true);
            expect(updatedToasts.some((toast) => toast.textContent?.includes('Error B'))).toBe(true);
            expect(updatedToasts.some((toast) => toast.textContent?.includes('Warning B'))).toBe(true);
            expect(updatedToasts.some((toast) => toast.textContent?.includes('Info B'))).toBe(true);
            expect(updatedToasts.some((toast) => toast.textContent?.includes('Status B'))).toBe(true);
            expect(updatedToasts.some((toast) => (
                toast.textContent?.includes('Structured title B')
                && toast.textContent.includes('Structured message B')
                && toast.classList.contains('ui-toast--success')
            ))).toBe(true);
        } finally {
            wrapper.unmount();
        }
    });

    it('submits the migrated global search with Inertia and returns focus to its launcher', async () => {
        installMatchMedia(false);
        const wrapper = mountShell('  Existing query  ');

        try {
            await nextTick();

            const trigger = wrapper.get<HTMLButtonElement>('.application-shell__search');
            await trigger.trigger('click');
            const input = searchInput();
            await vi.waitFor(() => {
                expect(document.activeElement).toBe(input.element);
            });

            expect(input.element.value).toBe('  Existing query  ');

            await searchForm().trigger('submit');

            expect(routerGet).toHaveBeenCalledOnce();
            expect(routerGet).toHaveBeenCalledWith(
                '/search',
                { q: 'Existing query' },
                expect.objectContaining({
                    preserveScroll: false,
                    preserveState: true,
                }),
            );
            expect(input.attributes('aria-busy')).toBe('true');
            expect(input.attributes('disabled')).toBeUndefined();
            expect(document.activeElement).toBe(trigger.element);

            const options = routerGet.mock.calls[0]?.[2] as { onFinish: () => void };
            options.onFinish();
            await nextTick();

            expect(input.attributes('aria-busy')).toBeUndefined();

            await wrapper.setProps({ searchQuery: 'History query' });
            await nextTick();

            expect(input.element.value).toBe('History query');
        } finally {
            wrapper.unmount();
        }
    });

    it('keeps the global search pending until the latest visit finishes', async () => {
        installMatchMedia(false);
        const wrapper = mountShell();

        try {
            await nextTick();

            const trigger = wrapper.get<HTMLButtonElement>('.application-shell__search');
            const form = searchForm();
            const input = searchInput();

            await trigger.trigger('click');
            await input.setValue('first');
            await form.trigger('submit');
            await trigger.trigger('click');
            await input.setValue('second');
            await form.trigger('submit');

            const firstOptions = routerGet.mock.calls[0]?.[2] as {
                onFinish: () => void;
                onNetworkError: () => boolean;
            };
            const secondOptions = routerGet.mock.calls[1]?.[2] as {
                onFinish: () => void;
                onNetworkError: () => boolean;
            };

            expect(firstOptions.onNetworkError()).toBe(false);
            firstOptions.onFinish();
            await nextTick();
            expect(input.attributes('aria-busy')).toBe('true');
            expect(document.body.querySelector('[role="alert"]')).toBeNull();

            expect(secondOptions.onNetworkError()).toBe(false);
            await nextTick();
            expect(document.body.querySelector('[role="alert"]')?.textContent).toContain('Search unavailable');
            expect(document.body.querySelector('[role="alert"]')?.textContent).toContain('Check your connection');

            secondOptions.onFinish();
            await nextTick();
            expect(input.attributes('aria-busy')).toBeUndefined();

            expect(input.element.value).toBe('second');
        } finally {
            wrapper.unmount();
        }
    });

    it('keeps search navigation native until the destination shares the persistent root', async () => {
        installMatchMedia(false);
        const wrapper = mountShell('demo', false);

        try {
            await nextTick();

            const form = searchForm();
            expect(form.attributes('method')).toBe('get');
            expect(form.attributes('action')).toBe('/search');

            await form.trigger('submit');

            expect(routerGet).not.toHaveBeenCalled();
        } finally {
            wrapper.unmount();
        }
    });

    it('renders grouped desktop navigation, native/Inertia links, flash and account details', async () => {
        installMatchMedia(false);
        const wrapper = mountShell();

        try {
            await nextTick();

            expect(wrapper.get('.application-shell__skip-link').attributes('href')).toBe('#application-content');
            expect(wrapper.get('#application-content').text()).toContain('Page content');
            expect(wrapper.findAll('.application-shell__navigation-group')).toHaveLength(2);
            expect(wrapper.get('.application-shell__navigation-label').text()).toBe('Workspace');
            const brandLogo = wrapper.get<HTMLImageElement>('.application-shell__brand-mark img');
            expect(brandLogo.attributes('src')).toBe(navigation.logoUrl);
            expect(brandLogo.attributes('width')).toBe('28');
            expect(brandLogo.attributes('height')).toBe('28');
            expect(wrapper.get('.application-shell__navigation a[href="/home"]').attributes('aria-current')).toBe('page');
            expect(wrapper.get('a[href="/funds"]').attributes('data-inertia-link')).toBe('true');
            expect(wrapper.get('a[href="/settings"]').attributes('data-inertia-link')).toBeUndefined();
            const globalSearchTrigger = wrapper.get('.application-shell__search');
            const globalSearchPanel = document.body.querySelector<HTMLElement>('.application-global-search__panel');
            expect(globalSearchTrigger.attributes('aria-haspopup')).toBe('dialog');
            expect(globalSearchTrigger.attributes('aria-controls')).toBe(globalSearchPanel?.id);
            expect(globalSearchTrigger.text()).toBe('');
            expect(searchForm().attributes('action')).toBe('/search');
            expect(document.body.querySelector('.application-shell__search-label')).toBeNull();
            expect(document.body.querySelector('.application-shell__search-shortcut')).toBeNull();
            expect(wrapper.get('.application-shell__excel-login .kv-icon').attributes('aria-hidden')).toBe('true');
            expect(document.body.querySelector('.ui-toast--success')?.textContent).toContain('Navigation prête.');
            expect(wrapper.get('.application-shell__version').text()).toBe('3.5-test');

            const toggle = wrapper.get<HTMLButtonElement>('button[aria-controls="application-navigation"]');
            const sidebar = wrapper.get('#application-navigation');
            expect(toggle.attributes('aria-expanded')).toBe('true');
            expect(toggle.attributes('aria-describedby')).toMatch(/^kview-tooltip-/);

            await toggle.trigger('click');

            expect(wrapper.get('.application-shell').classes()).toContain('is-sidebar-collapsed');
            expect(toggle.attributes('aria-expanded')).toBe('false');
            expect(window.localStorage.getItem('kview_investor.sidebar.hidden')).toBe('true');
            expect(sidebar.attributes('aria-hidden')).toBe('true');
            expect(sidebar.element.hasAttribute('inert')).toBe(true);

            await toggle.trigger('click');

            expect(wrapper.get('.application-shell').classes()).not.toContain('is-sidebar-collapsed');
            expect(toggle.attributes('aria-expanded')).toBe('true');
            expect(window.localStorage.getItem('kview_investor.sidebar.hidden')).toBe('false');
            expect(sidebar.attributes('aria-hidden')).toBeUndefined();
            expect(sidebar.element.hasAttribute('inert')).toBe(false);

            const accountTrigger = wrapper.get<HTMLButtonElement>('.application-shell__account-trigger');
            expect(accountTrigger.attributes('aria-haspopup')).toBe('dialog');
            await accountTrigger.trigger('click');
            await nextTick();

            const accountMenu = document.querySelector<HTMLElement>('#application-account-menu');
            const closeAccount = accountMenu?.querySelector<HTMLButtonElement>('.application-shell__account-close');
            const logout = accountMenu?.querySelector<HTMLButtonElement>('.application-shell__logout');

            expect(accountMenu?.getAttribute('role')).toBe('dialog');
            expect(accountMenu?.parentElement).toBe(document.body);
            expect(accountMenu?.textContent).toContain('Ada Lovelace');
            expect(accountMenu?.textContent).toContain('ada@example.com');
            expect(accountMenu?.textContent).toContain('User Settings');
            expect(accountMenu?.textContent).toContain('Data Status');
            expect(accountMenu?.textContent).toContain('Workspace');
            expect(accountMenu?.textContent).toContain('Excel');
            expect(accountMenu?.querySelector('a[href="/settings?tab=user"]')).not.toBeNull();
            expect(accountMenu?.querySelector('a[href="/settings?tab=system"]')).not.toBeNull();
            expect(accountMenu?.querySelector('a[href="/settings?tab=workspace"]')).not.toBeNull();
            expect(accountMenu?.querySelector('a[href="/settings?tab=user"]')?.getAttribute('aria-expanded')).toBe('false');
            expect(accountMenu?.querySelector('img')?.getAttribute('src')).toBe('/settings/avatar?v=7');
            expect(logout?.textContent).toContain('Sign out');
            expect(document.activeElement).toBe(closeAccount);

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

            expect(accountMenu?.hidden).toBe(true);
            expect(document.activeElement).toBe(accountTrigger.element);

            await accountTrigger.trigger('click');
            await nextTick();
            document.dispatchEvent(new KeyboardEvent('keydown', {
                key: 'k',
                ctrlKey: true,
                bubbles: true,
                cancelable: true,
            }));
            await vi.waitFor(() => {
                expect(document.activeElement).toBe(searchInput().element);
            });

            expect(accountMenu?.hidden).toBe(true);
            const globalSearchInput = searchInput();
            expect(document.activeElement).toBe(globalSearchInput.element);
            expect(globalSearchInput.attributes('aria-expanded')).toBe('true');
            expect(document.body.querySelector('.application-global-search__panel')?.getAttribute('aria-hidden')).toBe('false');
        } finally {
            wrapper.unmount();
        }
    });

    it('opens Settings in Vue via a same-shell fetch and closes back to its trigger', async () => {
        installMatchMedia(false);
        const fetchMock = vi.fn().mockResolvedValue({
            ok: true,
            status: 200,
            json: async () => settingsPayload,
        } as Response);
        vi.stubGlobal('fetch', fetchMock);
        const wrapper = mountShell();

        try {
            await nextTick();

            const settingsLink = wrapper.get<HTMLAnchorElement>('a[href="/settings"]');
            settingsLink.element.focus();
            await settingsLink.trigger('click');
            await flushPromises();
            await nextTick();

            const dialog = document.body.querySelector<HTMLElement>('#application-settings-dialog');

            expect(fetchMock).toHaveBeenCalledOnce();
            expect(fetchMock).toHaveBeenCalledWith(
                expect.stringMatching(/\/settings\?tab=user$/),
                expect.objectContaining({
                    headers: expect.objectContaining({
                        Accept: 'application/json',
                        'X-KView-Investor-Settings-Modal': 'true',
                    }),
                }),
            );
            expect(dialog?.getAttribute('role')).toBe('dialog');
            expect(settingsLink.attributes('href')).toBe('/settings');

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

            expect(document.body.querySelector('#application-settings-dialog')).not.toBeNull();
            expect(wrapper.get('.application-shell__search').attributes('aria-expanded')).toBe('false');
            expect(searchInput().attributes('aria-expanded')).toBe('false');
            expect(document.activeElement).not.toBe(searchInput().element);

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

            expect(document.body.querySelector('#application-settings-dialog')).toBeNull();
            expect(document.activeElement).toBe(settingsLink.element);
        } finally {
            wrapper.unmount();
            vi.unstubAllGlobals();
        }
    });

    it('posts logout once and exposes its pending state', async () => {
        installMatchMedia(false);
        const wrapper = mountShell();

        try {
            await nextTick();
            await wrapper.get('.application-shell__account-trigger').trigger('click');
            await nextTick();

            const logout = document.querySelector<HTMLButtonElement>('.application-shell__logout');
            expect(logout).not.toBeNull();
            logout?.click();
            logout?.click();
            await nextTick();

            expect(routerPost).toHaveBeenCalledOnce();
            expect(routerPost).toHaveBeenCalledWith(
                '/logout',
                {},
                expect.objectContaining({ preserveScroll: false }),
            );
            expect(logout?.disabled).toBe(true);
            expect(logout?.getAttribute('aria-busy')).toBe('true');
            expect(logout?.textContent).toContain('Signing out');

            const options = routerPost.mock.calls[0]?.[2] as { onFinish: () => void };
            options.onFinish();
            await nextTick();

            expect(logout?.disabled).toBe(false);
            expect(logout?.getAttribute('aria-busy')).toBeNull();
        } finally {
            wrapper.unmount();
        }
    });

    it('opens mobile navigation, focuses its close command and restores the toggle on Escape', async () => {
        const media = installMatchMedia(true);
        const wrapper = mountShell();

        try {
            await nextTick();

            const sidebar = wrapper.get('#application-navigation');
            const toggle = wrapper.get<HTMLButtonElement>('button[aria-controls="application-navigation"]');

            expect(sidebar.attributes('aria-hidden')).toBe('true');
            expect(toggle.attributes('aria-expanded')).toBe('false');
            expect(toggle.attributes('aria-describedby')).toMatch(/^kview-tooltip-/);
            expect(wrapper.find('.application-shell__overlay').exists()).toBe(false);

            await toggle.trigger('click');
            await nextTick();

            const close = wrapper.get<HTMLButtonElement>('button.application-shell__mobile-close');
            expect(wrapper.get('.application-shell').classes()).toContain('is-mobile-navigation-open');
            expect(sidebar.attributes('aria-hidden')).toBeUndefined();
            expect(sidebar.attributes('role')).toBe('dialog');
            expect(sidebar.attributes('aria-modal')).toBe('true');
            expect(toggle.attributes('aria-expanded')).toBe('true');
            expect(wrapper.find('.application-shell__overlay').exists()).toBe(true);
            expect(wrapper.get('.application-shell__topbar').element.hasAttribute('inert')).toBe(true);
            expect(wrapper.get('#application-content').element.hasAttribute('inert')).toBe(true);
            expect(document.activeElement).toBe(close.element);

            const navigationLinks = sidebar.findAll<HTMLAnchorElement>('a');
            const firstLink = navigationLinks[0];
            const lastLink = navigationLinks.at(-1) ?? firstLink;

            lastLink.element.focus();
            document.dispatchEvent(new KeyboardEvent('keydown', {
                key: 'Tab',
                bubbles: true,
                cancelable: true,
            }));
            expect(document.activeElement).toBe(firstLink.element);

            firstLink.element.focus();
            document.dispatchEvent(new KeyboardEvent('keydown', {
                key: 'Tab',
                shiftKey: true,
                bubbles: true,
                cancelable: true,
            }));
            expect(document.activeElement).toBe(lastLink.element);

            await wrapper.get('a[href="/funds"]').trigger('click');
            await nextTick();
            expect(wrapper.get('#application-navigation').attributes('aria-hidden')).toBe('true');
            expect(document.activeElement).toBe(toggle.element);

            await toggle.trigger('click');
            await nextTick();
            expect(document.activeElement).toBe(close.element);

            document.dispatchEvent(new KeyboardEvent('keydown', {
                key: 'k',
                ctrlKey: true,
                bubbles: true,
                cancelable: true,
            }));
            await vi.waitFor(() => {
                expect(document.activeElement).toBe(searchInput().element);
            });

            expect(wrapper.get('#application-navigation').attributes('aria-hidden')).toBe('true');
            const globalSearchInput = searchInput();
            expect(document.activeElement).toBe(globalSearchInput.element);
            expect(globalSearchInput.attributes('aria-expanded')).toBe('true');

            await toggle.trigger('click');
            await nextTick();
            expect(document.activeElement).toBe(close.element);

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

            expect(wrapper.get('#application-navigation').attributes('aria-hidden')).toBe('true');
            expect(wrapper.find('.application-shell__overlay').exists()).toBe(false);
            expect(wrapper.get('.application-shell__topbar').element.hasAttribute('inert')).toBe(false);
            expect(wrapper.get('#application-content').element.hasAttribute('inert')).toBe(false);
            expect(document.activeElement).toBe(toggle.element);
            expect(toggle.attributes('aria-describedby')).toMatch(/^kview-tooltip-/);
            expect(document.body.querySelector('.ui-tooltip__content.is-open')).toBeNull();

            await toggle.trigger('click');
            await nextTick();
            expect(document.activeElement).toBe(close.element);

            media.setMatches(false);
            await nextTick();

            expect(wrapper.get('#application-navigation').attributes('aria-hidden')).toBeUndefined();
            expect(toggle.attributes('aria-expanded')).toBe('true');
            expect(document.activeElement).toBe(toggle.element);
        } finally {
            wrapper.unmount();
        }
    });
});
