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

/**
 * Phase 7 production cutover: functional smoke and behavior across all seven
 * real Vue crud-table indexes (identity, footer geometry, filters,
 * navigation, search/sort visits, financial-record origin, mobile). The
 * exhaustive pixel-parity pass (selection/actions/header/footer geometry
 * under a constrained desktop) has its own file:
 * phase7-production-indexes-parity.spec.ts — its single test is large enough
 * to stand alone. The local-only engine specimen lives in
 * phase7-crud-table-specimen.spec.ts.
 */
const email = process.env.E2E_EMAIL;
const password = process.env.E2E_PASSWORD;

const productionIndexes = [
    { route: '/gps', id: 'gps-crud-table', heading: 'General Partners' },
    { route: '/funds', id: 'funds-crud-table', heading: 'Funds' },
    { route: '/investors', id: 'investors-crud-table', heading: 'Investors' },
    { route: '/investments', id: 'investments-crud-table', heading: 'Investments' },
    { route: '/call-distribs', id: 'call-distribs-crud-table', heading: 'Calls & Distributions' },
    { route: '/capital-accounts', id: 'capital-accounts-crud-table', heading: 'Capital Accounts' },
    { route: '/exchange-rates', id: 'exchange-rates-crud-table', heading: 'Currencies (Exchange Rates)' },
] as const;

if (process.env.CI && (!email || !password)) {
    throw new Error('E2E_EMAIL and E2E_PASSWORD are required for the Phase 7 production-index characterization.');
}

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 markPersistentShell = async (page: Page): Promise<string> => page
    .locator('.application-shell')
    .evaluate((shell) => {
        const marker = crypto.randomUUID();

        (shell as HTMLElement & { __phaseSevenIndexMarker?: string }).__phaseSevenIndexMarker = marker;

        return marker;
    });

const expectShellMarker = async (page: Page, marker: string): Promise<void> => {
    await expect.poll(() => page.locator('.application-shell').evaluate((shell) => (
        shell as HTMLElement & { __phaseSevenIndexMarker?: string }
    ).__phaseSevenIndexMarker)).toBe(marker);
};

const expectProductionIndex = async (
    page: Page,
    surface: (typeof productionIndexes)[number],
): Promise<void> => {
    await expect(page).toHaveURL(new RegExp(`${surface.route.replace('/', '\\/')}(?:\\?.*)?$`));
    await expect(page.locator('html')).toHaveAttribute('data-frontend-root', 'inertia');
    await expect(page.locator('.application-shell')).toHaveCount(1);
    await expect(page.locator('#top-menu, #left-menu')).toHaveCount(0);
    await expect(page.getByRole('heading', { level: 1, name: surface.heading })).toBeVisible();
    await expect(page.locator(`#${surface.id}.erp-table-card--vue`)).toHaveCount(1);
    await expect(page.locator(`#${surface.id} .erp-table-search input`)).toBeVisible();
};

const queryValue = (page: Page, key: string): string | null => new URL(page.url()).searchParams.get(key);

test.describe('Phase 7 production index cutover', () => {
    test.skip(!email || !password, 'Set E2E_EMAIL and E2E_PASSWORD to validate the Phase 7 production indexes.');

    test('all seven indexes render the Vue engine without a legacy Blade shell', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'The seven-route identity smoke runs once on desktop.');

        const consoleProblems: string[] = [];

        page.on('console', (message) => {
            if (message.type() === 'error' || message.type() === 'warning') {
                consoleProblems.push(`${message.type()}: ${message.text()}`);
            }
        });
        page.on('pageerror', (error) => consoleProblems.push(`pageerror: ${error.message}`));

        await login(page);

        for (const surface of productionIndexes) {
            await page.goto(surface.route);
            await expectProductionIndex(page, surface);
        }

        expect(consoleProblems).toEqual([]);
    });

    test('reported desktop width keeps the CRUD footer compact and readable', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'The reported footer geometry runs once on desktop.');

        await page.setViewportSize({ width: 1046, height: 942 });
        await login(page);
        await page.goto('/investors?items_per_page=25');

        const table = page.locator('#investors-crud-table');
        const footer = table.locator('.erp-table-footer');
        await expect(footer).toBeVisible();
        await expect(table.locator('.erp-table-pagination:not(:empty)')).toBeVisible();

        const geometry = await footer.evaluate((element) => {
            const selectors = ['.erp-table-footnote', '.erp-page-size-form', '.erp-table-pagination'];
            const rects = selectors.map((selector) => {
                const target = element.querySelector<HTMLElement>(selector);

                if (!target) throw new Error(`Missing footer group: ${selector}`);

                return target.getBoundingClientRect();
            });
            const footerRect = element.getBoundingClientRect();
            const overlaps = rects.some((rect, index) => rects.slice(index + 1).some((other) => (
                rect.left < other.right
                && other.left < rect.right
                && rect.top < other.bottom
                && other.top < rect.bottom
            )));
            const centers = rects.map((rect) => rect.top + rect.height / 2);

            return {
                centerSpread: Math.max(...centers) - Math.min(...centers),
                footerHeight: footerRect.height,
                overlaps,
                overflows: element.scrollWidth > element.clientWidth + 1,
            };
        });

        expect(geometry.centerSpread).toBeLessThanOrEqual(1);
        expect(geometry.footerHeight).toBeLessThanOrEqual(64);
        expect(geometry.overlaps).toBe(false);
        expect(geometry.overflows).toBe(false);

        const pageSizeTrigger = footer.locator('.erp-page-size-form .ui-dropdown-trigger');
        await pageSizeTrigger.click();
        const selectedPageSize = page.getByRole('option', { name: '25', exact: true });
        await expect(selectedPageSize).toHaveAttribute('aria-selected', 'true');
        await expect(selectedPageSize).toHaveClass(/is-selected/);
        await expect(selectedPageSize.locator('.ui-dropdown-option-check')).toHaveCount(0);
        await page.keyboard.press('Escape');
        await expect(pageSizeTrigger).toBeFocused();

        const boundaries = table.locator('.erp-pagination-boundary');
        await expect(boundaries).toHaveCount(2);
        await expect(boundaries.nth(0).locator('.ui-visually-hidden')).toHaveText('Previous page');
        await expect(boundaries.nth(1).locator('.ui-visually-hidden')).toHaveText('Next page');
        await expect(boundaries.nth(0).locator('.kv-icon')).toBeVisible();
        await expect(boundaries.nth(1).locator('.kv-icon')).toBeVisible();
    });

    test('funds keeps discrete columns readable without reserving a trailing action gutter', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'The reported 731px table geometry runs once on desktop.');

        await page.setViewportSize({ width: 731, height: 942 });
        await login(page);
        await page.goto('/funds?items_per_page=25');

        const table = page.locator('#funds-crud-table');
        const scroller = table.locator('.erp-table-main-scroll');
        const vintageHeader = table.locator('thead th.is-align-center').filter({ hasText: 'Vintage' });
        const firstRow = table.locator('tbody tr').first();
        const vintageCell = firstRow.locator('td.is-align-center').last();
        const actionsCell = firstRow.locator('.erp-table-actions-cell');
        const actions = actionsCell.locator('.erp-row-actions');
        const createCommand = table.locator('.erp-table-create');

        await expect(table).toBeVisible();
        await scroller.evaluate((element) => {
            element.scrollLeft = element.scrollWidth;
        });
        await expect(vintageHeader).toBeVisible();
        await expect(vintageCell).toContainText(/\d{4}/);
        await expect(vintageHeader.locator('.erp-column-filter-trigger')).toHaveCount(1);
        await expect(vintageHeader.locator('.erp-table-sort')).toHaveCount(1);

        const vintageGeometry = await vintageHeader.evaluate((header) => {
            const label = header.querySelector<HTMLElement>('.erp-table-sort > span:first-child');

            if (!label) throw new Error('Expected the Vintage sort label.');

            return {
                headerWidth: header.getBoundingClientRect().width,
                labelFits: label.scrollWidth <= label.clientWidth + 1,
                headerAlign: getComputedStyle(header).textAlign,
            };
        });
        expect(vintageGeometry.headerWidth).toBeGreaterThanOrEqual(144);
        expect(vintageGeometry.labelFits).toBe(true);
        expect(vintageGeometry.headerAlign).toBe('center');
        await expect.poll(() => vintageCell.evaluate((cell) => getComputedStyle(cell).textAlign)).toBe('center');

        const trailingGeometry = await firstRow.evaluate((row) => {
            const tableElement = row.closest('table');
            const actionCell = row.querySelector<HTMLElement>('.erp-table-actions-cell');
            const dataCells = Array.from(row.querySelectorAll<HTMLElement>(
                'td:not(.erp-table-select-cell):not(.erp-table-actions-cell)',
            ));
            const lastDataCell = dataCells.at(-1);

            if (!tableElement || !actionCell || !lastDataCell) throw new Error('Expected the trailing table edge.');

            return {
                actionCellWidth: actionCell.getBoundingClientRect().width,
                lastDataToTableEdge: Math.abs(
                    tableElement.getBoundingClientRect().right - lastDataCell.getBoundingClientRect().right,
                ),
            };
        });
        expect(trailingGeometry.actionCellWidth).toBeLessThanOrEqual(1);
        expect(trailingGeometry.lastDataToTableEdge).toBeLessThanOrEqual(1);

        await firstRow.hover({ position: { x: 22, y: 22 } });
        await expect.poll(() => actions.evaluate((element) => getComputedStyle(element).opacity)).toBe('1');
        const overlayGeometry = await Promise.all([
            scroller.boundingBox(),
            actions.boundingBox(),
        ]);
        expect(overlayGeometry[0]).not.toBeNull();
        expect(overlayGeometry[1]).not.toBeNull();
        expect(overlayGeometry[1]!.x).toBeGreaterThanOrEqual(overlayGeometry[0]!.x);
        expect(overlayGeometry[1]!.x + overlayGeometry[1]!.width).toBeLessThanOrEqual(
            overlayGeometry[0]!.x + overlayGeometry[0]!.width + 1,
        );
        await expect.poll(() => actionsCell.evaluate((element) => (
            getComputedStyle(element, '::before').opacity
        ))).toBe('1');

        await expect(createCommand).toHaveClass(/ui-icon-button--secondary/);
        const createBox = await createCommand.boundingBox();
        expect(createBox?.width).toBe(38);
        expect(createBox?.height).toBe(38);
    });

    test('constrained global search prioritizes records and keeps no-match actions compact', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'The constrained global-search geometry runs once on desktop.');

        await page.setViewportSize({ width: 562, height: 942 });
        await login(page);
        await page.goto('/investors?items_per_page=25');

        const trigger = page.locator('.application-shell__search');
        await trigger.click();

        const panel = page.getByRole('dialog', { name: 'Recherche globale' });
        const input = panel.getByRole('combobox', { name: 'Rechercher' });
        await expect(trigger).toHaveCSS('width', '36px');
        await expect(trigger).toHaveCSS('height', '36px');
        await expect(panel.getByText('Open full search', { exact: true })).toHaveCount(0);
        await expect(panel.locator('.application-global-search__item--action')).toHaveCount(3);
        await input.fill('Demo');
        await expect(panel.locator('.application-global-search__item-title').filter({ hasText: 'Demo' }).first()).toBeVisible();
        await expect(panel.getByText('Suggested searches')).toHaveCount(0);

        const sectionOrder = await panel.locator('.application-global-search__title').allTextContents();
        expect(sectionOrder.at(-1)).toBe('Actions');
        expect(sectionOrder.slice(0, -1)).toEqual(expect.arrayContaining([
            'General Partners',
            'Funds',
            'Investors',
        ]));

        const resultAndActionGeometry = await panel.evaluate((element) => {
            const firstResult = element.querySelector<HTMLElement>(
                '.application-global-search__group:not(.application-global-search__group--actions) .application-global-search__item',
            )?.getBoundingClientRect();
            const actions = element.querySelector<HTMLElement>('.application-global-search__group--actions')?.getBoundingClientRect();
            const panelRect = element.getBoundingClientRect();

            if (!firstResult || !actions) throw new Error('Missing search result or quick-action group.');

            return {
                recordsBeforeActions: firstResult.top < actions.top,
                actionHeight: actions.height,
                panelInsideViewport: panelRect.left >= 10
                    && panelRect.right <= window.innerWidth - 10 + 1,
            };
        });

        expect(resultAndActionGeometry.recordsBeforeActions).toBe(true);
        expect(resultAndActionGeometry.actionHeight).toBeLessThanOrEqual(132);
        expect(resultAndActionGeometry.panelInsideViewport).toBe(true);

        await input.fill('__phase7_no_global_match__');
        await expect(panel.getByText('No matching records. Try another name or alias.')).toBeVisible();
        await expect(panel.locator('.application-global-search__item--action')).toHaveCount(4);

        const noMatchGeometry = await panel.evaluate((element) => {
            const empty = element.querySelector<HTMLElement>('.application-global-search__empty')?.getBoundingClientRect();
            const actions = element.querySelector<HTMLElement>('.application-global-search__group--actions')?.getBoundingClientRect();
            const panelRect = element.getBoundingClientRect();

            if (!empty || !actions) throw new Error('Missing no-match copy or quick-action group.');

            return {
                messageBeforeActions: empty.bottom <= actions.top,
                panelHeight: panelRect.height,
                scrollWidth: element.scrollWidth,
                clientWidth: element.clientWidth,
            };
        });

        expect(noMatchGeometry.messageBeforeActions).toBe(true);
        expect(noMatchGeometry.panelHeight).toBeLessThanOrEqual(390);
        expect(noMatchGeometry.scrollWidth).toBeLessThanOrEqual(noMatchGeometry.clientWidth + 1);

        await input.press('ArrowDown');
        const activeId = await input.getAttribute('aria-activedescendant');
        expect(activeId).not.toBeNull();
        await expect(page.locator(`#${activeId}`)).toContainText('Search all records');

        await input.press('Escape');
        await expect(panel).toBeHidden();
        await expect(trigger).toBeFocused();
    });

    test('primary business navigation keeps the application shell mounted', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Persistent-shell navigation is covered once on desktop.');

        await login(page);
        await page.goto('/gps');
        await expectProductionIndex(page, productionIndexes[0]);
        const marker = await markPersistentShell(page);
        const navigation = page.locator('#application-navigation');

        for (const surface of productionIndexes.slice(1, 4)) {
            await navigation.getByRole('link', { name: surface.heading, exact: true }).click();
            await expectProductionIndex(page, surface);
            await expectShellMarker(page, marker);
        }
    });

    test('search and sorting are partial Inertia visits that preserve the shell', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Server interaction contract runs once on desktop.');

        await login(page);
        await page.goto('/gps');
        const table = page.locator('#gps-crud-table');
        const search = table.locator('.erp-table-search input');
        const marker = await markPersistentShell(page);

        await search.fill('__phase7_no_matching_gp__');
        await search.press('Enter');
        await expect.poll(() => queryValue(page, 'search')).toBe('__phase7_no_matching_gp__');
        await expect(table.locator('.erp-table-empty-state')).toBeVisible();
        await expectShellMarker(page, marker);

        await search.fill('');
        await search.press('Enter');
        await expect.poll(() => queryValue(page, 'search')).toBeNull();
        await expect(table.locator('tbody tr').first()).toBeVisible();
        await expectShellMarker(page, marker);

        const sortable = table.locator('.erp-table-sort').first();
        const key = await sortable.locator('span').first().textContent();

        await sortable.click();
        await expect.poll(() => queryValue(page, 'direction')).toBe('asc');
        await expect(sortable.locator('xpath=ancestor::th')).toHaveAttribute('aria-sort', 'ascending');
        await expectShellMarker(page, marker);
        expect(queryValue(page, 'sort')).not.toBeNull();
        expect(key?.trim()).not.toBe('');
    });

    test('multiselect and date filters keep their typed server contract', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Typed filter round-trips run once on desktop.');

        await login(page);
        await page.goto('/funds');
        const fundsMarker = await markPersistentShell(page);

        await page.getByRole('button', { name: 'Filter Asset class' }).click();
        await page.getByRole('checkbox', { name: 'Private equity' }).check();
        await page.getByRole('button', { name: 'Apply', exact: true }).click();
        await expect.poll(() => queryValue(page, 'filters[asset_class][operator]')).toBe('in');
        await expect.poll(() => queryValue(page, 'filters[asset_class][value]')).toBe('private_equity');
        await expect(page.getByRole('button', { name: 'Filter Asset class' })).toHaveClass(/is-active/);
        await expectShellMarker(page, fundsMarker);

        await page.goto('/call-distribs');
        const callsMarker = await markPersistentShell(page);

        await page.getByRole('button', { name: 'Filter Due date' }).click();
        const dateInput = page
            .locator('#call-distribs-crud-table_filter_due_date')
            .getByLabel('Filter value');

        await expect(dateInput).toHaveAttribute('type', 'date');
        await dateInput.fill('2025-01-01');
        await page.getByRole('button', { name: 'Apply', exact: true }).click();
        await expect.poll(() => queryValue(page, 'filters[due_date][operator]')).toBe('=');
        await expect.poll(() => queryValue(page, 'filters[due_date][value]')).toBe('2025-01-01');
        await expectShellMarker(page, callsMarker);
    });

    test('financial-record indexes keep their origin through row detail and Back', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Index-origin navigation runs once on desktop.');

        await login(page);

        const cases = [
            {
                index: '/investments',
                table: '#investments-crud-table',
                detailCell: 3,
                detail: /\/investments\/[0-9a-f-]+\?return_index=1$/,
                back: 'Back to investments',
            },
            {
                index: '/call-distribs',
                table: '#call-distribs-crud-table',
                detailCell: 3,
                detail: /\/call-distribs\/[0-9a-f-]+\?return_index=1$/,
                back: 'Back to calls & distributions',
            },
            {
                index: '/capital-accounts',
                table: '#capital-accounts-crud-table',
                detailCell: 2,
                detail: /\/capital-accounts\/[0-9a-f-]+\?return_index=1$/,
                back: 'Back to capital accounts',
            },
        ] as const;

        for (const surface of cases) {
            await page.goto(surface.index);
            const marker = await markPersistentShell(page);
            const row = page.locator(`${surface.table} tbody tr`).first();

            await expect(row).toBeVisible();
            await row.locator('td').nth(surface.detailCell).click();
            await expect(page).toHaveURL(surface.detail);
            await expectShellMarker(page, marker);

            await page.getByRole('link', { name: surface.back, exact: true }).click();
            await expect(page).toHaveURL(new RegExp(`${surface.index}$`));
            await expectShellMarker(page, marker);
        }
    });

    test('exchange-rate index keeps its specialized import and settings actions', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Specialized header actions run once on desktop.');

        await login(page);
        await page.goto('/exchange-rates');
        await expectProductionIndex(page, productionIndexes[6]);

        const period = page.locator('.exchange-rate-sync .ui-dropdown-trigger');

        await expect(period).toBeVisible();
        await period.click();
        await page.getByRole('option', { name: '1 month' }).click();
        await expect(page.locator('input[name="period"]')).toHaveValue('one_month');
        await expect(page.getByRole('button', { name: 'Import history' })).toBeVisible();
        await expect(page.getByRole('link', { name: 'Admin settings' })).toBeVisible();
    });

    test('mobile indexes stay within the viewport and investor row tap opens its own detail', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'mobile-chromium', 'Responsive production coverage runs in the mobile project.');

        await login(page);

        for (const surface of productionIndexes) {
            await page.goto(surface.route);
            await expectProductionIndex(page, surface);
            const widths = await page.evaluate(() => ({
                client: document.documentElement.clientWidth,
                scroll: document.documentElement.scrollWidth,
            }));

            expect(widths.scroll, `${surface.route} overflows the page viewport`).toBeLessThanOrEqual(widths.client + 1);
            await expect(page.locator(`#${surface.id} .erp-table-actions-cell`).first()).toBeHidden();
            const selectionCheckbox = page.locator(`#${surface.id} .erp-table-select-cell input`).first();

            if (await selectionCheckbox.count()) {
                await expect.poll(() => selectionCheckbox.evaluate((element) => (
                    getComputedStyle(element).opacity
                ))).toBe('1');
                await expect.poll(() => selectionCheckbox.evaluate((element) => (
                    getComputedStyle(element).pointerEvents
                ))).toBe('auto');
                await expect.poll(() => selectionCheckbox.locator('xpath=..').evaluate((element) => (
                    getComputedStyle(element).backgroundColor
                ))).not.toBe('rgba(0, 0, 0, 0)');
            }
        }

        await page.goto('/investors');
        const marker = await markPersistentShell(page);
        const row = page.locator('#investors-crud-table tbody tr').first();

        await expect(row).toBeVisible();
        await row.locator('td').nth(2).tap();
        await expect(page).toHaveURL(/\/investors\/[0-9a-f-]+$/);
        await expectShellMarker(page, marker);
    });
});
