import { expect, test, type Page } from '@playwright/test';

const email = process.env.E2E_EMAIL;
const password = process.env.E2E_PASSWORD;

interface Box {
    x: number;
    y: number;
    width: number;
    height: number;
}

interface ShellGeometry {
    sidebar: Box;
    topbar: Box;
    logo: Box;
    search: Box;
    toggle: Box;
    account: Box;
    navigation: Box;
    navigationIcon: Box;
    weights: {
        brand: string;
        byline: string;
        navigation: string;
    };
}

const selectors = {
    blade: {
        sidebar: '#left-menu',
        topbar: '#top-menu',
        logo: '.ui-app-sidebar__mark img',
        search: '#top-menu .global-search',
        mobileSearch: '#top-menu .topbar-mobile-search',
        toggle: '#top-menu .topbar-sidebar-toggle',
        excel: '#excel-login-button',
        account: '.user-menu-trigger',
        navigation: '.ui-app-sidebar__tooltip > a',
        navigationIcon: '.ui-app-sidebar__tooltip > a .icon',
        brand: '.ui-app-sidebar__brand-copy strong',
        byline: '.ui-app-sidebar__brand-copy small',
        navigationCopy: '.ui-app-sidebar__tooltip > a .menu-text',
    },
    vue: {
        sidebar: '#application-navigation',
        topbar: '.application-shell__topbar',
        logo: '.application-shell__brand-mark img',
        search: '.application-shell__search',
        mobileSearch: '.application-shell__search',
        toggle: '.application-shell__navigation-toggle',
        excel: '.application-shell__excel-login',
        account: '.application-shell__account-trigger',
        navigation: '.application-shell__navigation a',
        navigationIcon: '.application-shell__navigation-icon',
        brand: '.application-shell__brand-copy strong',
        byline: '.application-shell__brand-copy small',
        navigationCopy: '.application-shell__navigation-copy',
    },
} as const;

const login = async (page: Page): Promise<void> => {
    await page.goto('/login');
    await page.getByRole('textbox', { name: 'Username', exact: true }).fill(email!);
    await page.getByLabel('Password').fill(password!);
    await page.getByRole('button', { name: 'Login' }).click();
    await expect(page).toHaveURL(/\/$/);
};

const box = async (page: Page, selector: string): Promise<Box> => (
    page.locator(selector).first().evaluate((element) => {
        const bounds = element.getBoundingClientRect();

        return {
            x: bounds.x,
            y: bounds.y,
            width: bounds.width,
            height: bounds.height,
        };
    })
);

const weight = async (page: Page, selector: string): Promise<string> => (
    page.locator(selector).first().evaluate((element) => getComputedStyle(element).fontWeight)
);

const collectGeometry = async (
    page: Page,
    owner: keyof typeof selectors,
    mobile: boolean,
): Promise<ShellGeometry> => {
    const target = selectors[owner];

    return {
        sidebar: await box(page, target.sidebar),
        topbar: await box(page, target.topbar),
        logo: await box(page, target.logo),
        search: await box(page, mobile ? target.mobileSearch : target.search),
        toggle: await box(page, target.toggle),
        account: await box(page, target.account),
        navigation: await box(page, target.navigation),
        navigationIcon: await box(page, target.navigationIcon),
        weights: {
            brand: await weight(page, target.brand),
            byline: await weight(page, target.byline),
            navigation: await weight(page, target.navigationCopy),
        },
    };
};

const expectNear = (actual: number, expected: number, tolerance = 1): void => {
    expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
};

const expectSameBox = (actual: Box, expected: Box, tolerance = 1): void => {
    expectNear(actual.x, expected.x, tolerance);
    expectNear(actual.y, expected.y, tolerance);
    expectNear(actual.width, expected.width, tolerance);
    expectNear(actual.height, expected.height, tolerance);
};

test('the Vue application shell preserves the historical geometry and uses a compact search command', async ({ page }, testInfo) => {
    test.skip(!email || !password, 'Set E2E_EMAIL and E2E_PASSWORD to validate application shell parity.');
    const mobile = testInfo.project.name === 'mobile-chromium';

    await login(page);
    // Import remains a standard historical Blade-shell reference. The seven
    // business indexes are now Inertia pages, while Home has a deliberately
    // distinct mobile presentation; using either would silently stop
    // protecting the generic shell parity contract.
    await page.goto('/imports');

    if (mobile) {
        const bladeToggle = page.locator(selectors.blade.toggle);

        // Login may intentionally leave the mobile drawer open. Normalize to
        // the open comparison state without clicking through the drawer that
        // is already covering the topbar toggle.
        if (await bladeToggle.getAttribute('aria-expanded') !== 'true') {
            await bladeToggle.click();
        }
        await expect(bladeToggle).toHaveAttribute('aria-expanded', 'true');
        await expect.poll(async () => Math.abs((await box(page, selectors.blade.sidebar)).x)).toBeLessThanOrEqual(1);
    }

    const blade = await collectGeometry(page, 'blade', mobile);
    const bladeExcelVisible = await page.locator(selectors.blade.excel).isVisible();

    if (mobile) {
        await page.keyboard.press('Escape');
    }

    await page.goto('/__kview/inertia/application-shell');
    await expect(page.getByRole('heading', { name: 'Layout applicatif persistant' })).toBeVisible();

    const demoCardInsets = await page.locator('.application-shell__demo-card').evaluateAll((cards) => {
        const formHeader = cards[0]?.querySelector('.ui-resource-header');
        const resultsHeader = cards[1]?.querySelector('.ui-section-header');

        if (!cards[0] || !cards[1] || !formHeader || !resultsHeader) {
            throw new Error('The application shell demo cards are incomplete.');
        }

        return [
            formHeader.getBoundingClientRect().x - cards[0].getBoundingClientRect().x,
            resultsHeader.getBoundingClientRect().x - cards[1].getBoundingClientRect().x,
        ];
    });

    expectNear(demoCardInsets[1], demoCardInsets[0]);

    await page.goto('/search');
    await expect(page.getByRole('heading', { name: 'Search' })).toBeVisible();
    await expect(page.locator('html')).toHaveAttribute('data-frontend-root', 'inertia');
    await expect(page.locator('#top-menu, #left-menu')).toHaveCount(0);

    const excelIconPath = page.locator(`${selectors.vue.excel} path`);

    await expect(excelIconPath).toHaveAttribute('fill', 'currentColor');
    await expect(excelIconPath).toHaveAttribute('stroke', 'none');
    await expect(excelIconPath).toHaveAttribute('transform', 'translate(3 0) scale(0.046875)');

    if (mobile) {
        const compactSearch = page.locator(selectors.vue.search);
        const searchBefore = await box(page, selectors.vue.search);
        const topbarBefore = await box(page, selectors.vue.topbar);

        expectSameBox(await box(page, selectors.vue.toggle), { x: 13.4, y: 7, width: 36, height: 36 }, 2);
        expectNear(searchBefore.width, 36);
        await compactSearch.click();
        await expect(compactSearch).toHaveAttribute('aria-expanded', 'true');
        await expect(page.getByRole('combobox', { name: 'Rechercher' })).toBeFocused();
        await expect(page.locator('.application-global-search__panel[role="dialog"]')).toBeVisible();
        expectSameBox(await box(page, selectors.vue.search), searchBefore);
        expectSameBox(await box(page, selectors.vue.topbar), topbarBefore);
        await page.keyboard.press('Escape');
        await expect(compactSearch).toHaveAttribute('aria-expanded', 'false');
        await expect(compactSearch).toBeFocused();

        await page.locator(selectors.vue.toggle).click();
        await expect(page.locator(selectors.vue.toggle)).toHaveAttribute('aria-expanded', 'true');
        await expect.poll(async () => Math.abs((await box(page, selectors.vue.sidebar)).x)).toBeLessThanOrEqual(1);
    }

    const vue = await collectGeometry(page, 'vue', mobile);
    const vueExcelVisible = await page.locator(selectors.vue.excel).isVisible();

    expectSameBox(vue.sidebar, blade.sidebar);
    expectSameBox(vue.topbar, blade.topbar);
    expectSameBox(vue.logo, blade.logo);
    if (mobile) {
        expectSameBox(vue.search, blade.search);
    } else {
        expectNear(vue.search.width, 36);
        expectNear(vue.search.height, 36);
    }
    expectSameBox(vue.toggle, blade.toggle);
    expectSameBox(vue.account, blade.account);
    expectNear(vue.navigation.width, blade.navigation.width);
    expectNear(vue.navigation.height, blade.navigation.height);
    expectSameBox(vue.navigationIcon, blade.navigationIcon);
    expect(vue.weights).toEqual(blade.weights);
    expect(vue.weights).toEqual({ brand: '750', byline: '550', navigation: '600' });
    expect(vueExcelVisible).toBe(bladeExcelVisible);
    expect(vueExcelVisible).toBe(!mobile);
    expect(await page.locator('.application-shell').evaluate((element) => (
        element.scrollWidth <= element.clientWidth + 1
    ))).toBe(true);

    if (mobile) {
        const viewportWidth = await page.evaluate(() => window.visualViewport?.width ?? window.innerWidth);

        expectNear(vue.sidebar.width, viewportWidth);
        await page.keyboard.press('Escape');
        await expect(page.locator(selectors.vue.toggle)).toHaveAttribute('aria-expanded', 'false');
    } else {
        await page.locator(selectors.vue.toggle).click();
        await expect(page.locator(selectors.vue.sidebar)).toHaveAttribute('aria-hidden', 'true');
        await expect.poll(async () => (await box(page, selectors.vue.sidebar)).width).toBeLessThanOrEqual(1);
        expectNear((await box(page, selectors.vue.topbar)).x, 3);
    }
});

test('the desktop sidebar preference is applied before the first Vue shell commit', async ({ page }, testInfo) => {
    test.skip(testInfo.project.name !== 'desktop-chromium', 'The desktop pre-paint contract only needs one browser project.');
    test.skip(!email || !password, 'Set E2E_EMAIL and E2E_PASSWORD to validate the sidebar pre-paint contract.');

    await login(page);
    await page.evaluate(() => window.localStorage.setItem('kview_investor.sidebar.hidden', 'true'));
    await page.addInitScript(() => {
        const targetWindow = window as typeof window & {
            __kview_investorFirstShellCommit?: {
                className: string;
                htmlSidebarHidden: boolean;
            } | null;
        };
        targetWindow.__kview_investorFirstShellCommit = null;
        let observer: MutationObserver | null = null;

        const captureShell = (): void => {
            const shell = document.querySelector<HTMLElement>('.application-shell');

            if (!shell || targetWindow.__kview_investorFirstShellCommit) {
                return;
            }

            targetWindow.__kview_investorFirstShellCommit = {
                className: shell.className,
                htmlSidebarHidden: document.documentElement.classList.contains('app-sidebar-hidden'),
            };
            observer?.disconnect();
        };
        observer = new MutationObserver(captureShell);

        observer.observe(document, { childList: true, subtree: true });
        window.addEventListener('DOMContentLoaded', captureShell, { once: true });
    });

    await page.goto('/search');
    await expect(page.getByRole('heading', { name: 'Search' })).toBeVisible();
    await expect(page.locator('.application-shell')).toHaveClass(/is-sidebar-collapsed/);
    await expect(page.locator('#application-navigation')).toHaveAttribute('aria-hidden', 'true');
    await expect.poll(() => page.evaluate(() => (
        window as typeof window & {
            __kview_investorFirstShellCommit?: {
                className: string;
                htmlSidebarHidden: boolean;
            } | null;
        }
    ).__kview_investorFirstShellCommit)).toMatchObject({
        className: expect.stringContaining('is-sidebar-collapsed'),
        htmlSidebarHidden: true,
    });
});
