import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { beforeEach, describe, expect, it, vi } from 'vitest';

interface RouterSuccessEvent {
    detail: {
        page: {
            props: Record<string, unknown>;
        };
    };
}

type RouterSuccessListener = (event: RouterSuccessEvent) => void;

const inertia = vi.hoisted(() => ({
    get: vi.fn(),
    on: vi.fn(),
    successListeners: [] as RouterSuccessListener[],
}));

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

    return {
        Head: defineVueComponent({
            name: 'Head',
            props: { title: String },
            template: '<span data-head />',
        }),
        router: {
            get: inertia.get,
            on: inertia.on,
        },
    };
});

import SearchPage from '@/Pages/Search/Index.vue';
import type { SearchPageProps, SearchResultGroupData } from '@/types/search';

const emptyGroups: SearchResultGroupData[] = [
    {
        id: 'gps',
        title: 'General Partners',
        description: 'GP entities matching your query.',
        icon: 'building',
        results: [],
        emptyTitle: 'No General Partners found',
        emptyText: 'Try another name or alias.',
    },
    {
        id: 'funds',
        title: 'Funds',
        description: 'Funds matching your query.',
        icon: 'portfolio',
        results: [],
        emptyTitle: 'No funds found',
        emptyText: 'Try another name or alias.',
    },
    {
        id: 'investors',
        title: 'Investors',
        description: 'Investors matching your query.',
        icon: 'user',
        results: [],
        emptyTitle: 'No investors found',
        emptyText: 'Try another name or alias.',
    },
];

const initialProps: SearchPageProps = {
    query: '',
    searchUrl: '/search',
    groups: emptyGroups,
};

const mountPage = (props: SearchPageProps = initialProps) => mount(SearchPage, {
    attachTo: document.body,
    props,
});

beforeEach(() => {
    document.body.innerHTML = '';
    inertia.get.mockReset();
    inertia.successListeners.splice(0);
    inertia.on.mockReset().mockImplementation((event: string, listener: RouterSuccessListener) => {
        if (event === 'success') {
            inertia.successListeners.push(listener);
        }

        return vi.fn();
    });
});

describe('Search Inertia page', () => {
    it('renders the accessible initial state with the typed kView form', () => {
        const wrapper = mountPage();

        expect(wrapper.get('h1').text()).toBe('Search');
        expect(wrapper.get('form[role="search"]').attributes('action')).toBe('/search');
        expect(wrapper.get('input[name="q"]').attributes('type')).toBe('search');
        expect(wrapper.get('.search-page__initial-state').text()).toContain('Start a search');
        expect(wrapper.find('.search-page__groups').exists()).toBe(false);
        expect(wrapper.find('[data-ui-search]').exists()).toBe(false);
    });

    it('renders all result groups and keeps Blade destinations as native links', () => {
        const wrapper = mountPage({
            ...initialProps,
            query: 'growth',
            groups: [
                {
                    ...emptyGroups[0],
                    results: [{
                        id: 'gp-1',
                        title: 'Growth Partners',
                        href: '/gps/gp-1',
                        icon: 'building',
                        linkProps: { inertia: false },
                    }],
                },
                emptyGroups[1],
                emptyGroups[2],
            ],
        });

        expect(wrapper.findAll('.search-page__group')).toHaveLength(3);
        const link = wrapper.get('a[href="/gps/gp-1"]');
        expect(link.text()).toContain('Growth Partners');
        expect(link.attributes('data-inertia-link')).toBeUndefined();
        expect(wrapper.text()).toContain('No funds found');
        expect(wrapper.text()).toContain('No investors found');
    });

    it('trims a GET visit, exposes loading without blurring the field, and handles failures inline', async () => {
        const wrapper = mountPage({
            ...initialProps,
            query: 'old results',
            groups: emptyGroups,
        });
        const input = wrapper.get<HTMLInputElement>('input[name="q"]');

        await input.setValue('  new query  ');
        input.element.focus();
        await wrapper.get('form[role="search"]').trigger('submit');

        expect(inertia.get).toHaveBeenCalledOnce();
        expect(inertia.get).toHaveBeenCalledWith(
            '/search',
            { q: 'new query' },
            expect.objectContaining({
                preserveScroll: false,
                preserveState: true,
                replace: false,
                viewTransition: true,
            }),
        );
        expect(wrapper.get('form[role="search"]').attributes('aria-busy')).toBe('true');
        expect(wrapper.get('.search-page__status').text()).toContain('Searching all records');
        expect(input.attributes('disabled')).toBeUndefined();
        expect(document.activeElement).toBe(input.element);
        expect(wrapper.find('.search-page__groups').exists()).toBe(true);

        await wrapper.setProps({
            ...initialProps,
            query: 'new query',
            groups: [
                {
                    ...emptyGroups[0],
                    results: [{
                        id: 'gp-1',
                        title: 'New Query GP',
                        href: '/gps/gp-1',
                        icon: 'building',
                        linkProps: { inertia: false },
                    }],
                },
                emptyGroups[1],
                emptyGroups[2],
            ],
        } as never);
        await nextTick();

        expect(wrapper.get('.search-page__status').text()).toBe('1 result found for new query.');
        expect(document.activeElement).toBe(input.element);

        const options = inertia.get.mock.calls[0]?.[2] as {
            onHttpException: () => boolean;
            onNetworkError: () => boolean;
            onFinish: () => void;
        };
        expect(options.onHttpException()).toBe(false);
        expect(options.onNetworkError()).toBe(false);
        options.onFinish();
        await nextTick();

        expect(wrapper.get('[role="alert"]').text()).toContain('Search unavailable');
        expect(wrapper.get('[role="alert"]').text()).toContain('Check your connection');
        expect(wrapper.get('form[role="search"]').attributes('aria-busy')).toBeUndefined();
        expect(wrapper.get('.search-page__status').text()).toBe('');

        inertia.successListeners.forEach((listener) => listener({
            detail: {
                page: {
                    props: { query: 'successful query' },
                },
            },
        }));
        await nextTick();
        expect(wrapper.find('[role="alert"]').exists()).toBe(false);
        expect(input.element.value).toBe('successful query');
    });

    it('resynchronizes the field when browser history replaces the server query prop', async () => {
        const wrapper = mountPage({
            ...initialProps,
            query: 'current',
        });

        expect(wrapper.get<HTMLInputElement>('input[name="q"]').element.value).toBe('current');

        await wrapper.setProps({
            ...initialProps,
            query: 'previous',
        } as never);
        await nextTick();

        expect(wrapper.get<HTMLInputElement>('input[name="q"]').element.value).toBe('previous');
    });

    it('keeps the latest visit busy when an older visit finishes after resubmission', async () => {
        const wrapper = mountPage();
        const form = wrapper.get('form[role="search"]');
        const input = wrapper.get<HTMLInputElement>('input[name="q"]');

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

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

        firstOptions.onFinish();
        await nextTick();
        expect(form.attributes('aria-busy')).toBe('true');

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