import AxeBuilder from '@axe-core/playwright';
import { expect, test, type Page } from '@playwright/test';

/**
 * Phase 7 crud-table engine: the local-only specimen route
 * (`/__kview/inertia/crud-table`) remains the most concentrated place to
 * exercise the portable engine itself — accessibility, keyboard column
 * reorder/resize, import/export, row actions, the persistent horizontal
 * scrollbar, mobile behavior, and nested-popover focus. The production
 * cutover across all seven real indexes has its own spec files:
 * phase7-production-indexes.spec.ts and
 * phase7-production-indexes-parity.spec.ts.
 */
const email = process.env.E2E_EMAIL;
const password = process.env.E2E_PASSWORD;

if (process.env.CI && (!email || !password)) {
    throw new Error('E2E_EMAIL and E2E_PASSWORD are required for the Phase 7 crud-table 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 expectNoSeriousA11yViolations = async (page: Page): Promise<void> => {
    const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
        .analyze();
    const violations = results.violations.filter((violation) => (
        violation.impact === 'serious' || violation.impact === 'critical'
    ));

    expect(violations, JSON.stringify(violations, null, 2)).toEqual([]);
};

test.describe('Phase 7 crud-table engine specimen', () => {
    test.skip(!email || !password, 'Set E2E_EMAIL and E2E_PASSWORD to validate the Phase 7 crud-table.');

    test('exposes a page heading and passes axe with the column panel open', async ({ page }) => {
        await login(page);
        await page.goto('/__kview/inertia/crud-table');
        await expect(page.locator('.erp-table-card--vue')).toHaveCount(1);

        await expect(page.getByRole('heading', { level: 1, name: 'General Partners' })).toBeVisible();
        await expectNoSeriousA11yViolations(page);

        await page.locator('.erp-table-settings-trigger').click();
        await expect(page.locator('.erp-column-manager')).toBeVisible();
        await expectNoSeriousA11yViolations(page);
    });

    test('reorders and resizes columns from the keyboard', async ({ page }) => {
        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        await page.locator('.erp-table-settings-trigger').click();
        const before = await page.$$eval(
            '.erp-column-manager-toggle span',
            (els) => els.map((element) => element.textContent),
        );
        await page.locator('.erp-column-manager-item:first-child .erp-drag-handle').focus();
        await page.keyboard.press('Alt+ArrowDown');
        const after = await page.$$eval(
            '.erp-column-manager-toggle span',
            (els) => els.map((element) => element.textContent),
        );
        expect(after[0]).toBe(before[1]);
        expect(after[1]).toBe(before[0]);
        await page.keyboard.press('Escape');

        const separator = page.locator('.erp-column-resize[role="separator"]').first();
        const widthBefore = Number(await separator.getAttribute('aria-valuenow'));
        await separator.focus();
        await page.keyboard.press('ArrowRight');
        const widthAfter = Number(await separator.getAttribute('aria-valuenow'));
        expect(widthAfter).toBe(widthBefore + 12);
    });

    test('import/export keeps the secondary-button outline and opens its panel', async ({ page }) => {
        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        const trigger = page.locator('.erp-table-import-export');
        const outline = await trigger.evaluate((element) => {
            const style = getComputedStyle(element);

            return {
                width: Number.parseFloat(style.borderTopWidth),
                style: style.borderTopStyle,
            };
        });

        expect(outline.width).toBeGreaterThanOrEqual(1);
        expect(outline.style).toBe('solid');

        await trigger.click();
        const panel = page.getByRole('dialog', { name: 'Import / Export' });
        await expect(panel).toBeVisible();
        await page.keyboard.press('Escape');
        await expect(panel).toBeHidden();
        await expect(trigger).toBeFocused();
    });

    test('row actions open the preview drawer on desktop', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Row-action buttons are pointer-only; mobile uses row tap to detail.');

        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        await page.locator('tbody tr').first().hover();
        await page.locator('.erp-table-actions-cell [aria-label="Preview GP"]').first().click();
        await expect(page.locator('[id$="_preview_drawer"]')).toBeVisible();
        await page.keyboard.press('Escape');
    });

    test('desktop: the persistent horizontal scrollbar stays inside the table above its footer and remains synchronized', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Desktop persistent-scrollbar contract.');

        await page.setViewportSize({ width: 1205, height: 979 });
        await login(page);
        await page.goto('/funds?items_per_page=50');

        const table = page.locator('#funds-crud-table');
        const content = page.locator('#application-content');
        const mainScroller = table.locator('.erp-table-main-scroll');
        const persistentScroller = table.locator('.erp-table-persistent-horizontal-scroll');

        await expect(table).toBeVisible();
        await expect(persistentScroller).toBeVisible();
        await expectNoSeriousA11yViolations(page);

        const geometry = await table.evaluate((tableElement) => {
            const contentElement = document.querySelector<HTMLElement>('#application-content');
            const shell = tableElement.querySelector<HTMLElement>('.erp-table-shell');
            const main = tableElement.querySelector<HTMLElement>('.erp-table-main-scroll');
            const persistent = tableElement.querySelector<HTMLElement>('.erp-table-persistent-horizontal-scroll');
            const track = tableElement.querySelector<HTMLElement>('.erp-table-persistent-horizontal-scroll__track');
            const footer = tableElement.querySelector<HTMLElement>('.erp-table-footer');

            if (!contentElement || !shell || !main || !persistent || !track || !footer) {
                throw new Error('Expected the persistent horizontal-scrollbar geometry.');
            }

            const contentRect = contentElement.getBoundingClientRect();
            const shellRect = shell.getBoundingClientRect();
            const mainRect = main.getBoundingClientRect();
            const persistentRect = persistent.getBoundingClientRect();

            return {
                visibleBottomDelta: Math.abs(Math.min(contentRect.bottom, shellRect.bottom) - persistentRect.bottom),
                leftDelta: Math.abs(mainRect.left - persistentRect.left),
                widthDelta: Math.abs(mainRect.width - persistentRect.width),
                trackDelta: Math.abs(main.scrollWidth - track.getBoundingClientRect().width),
                overflowing: main.scrollWidth > main.clientWidth + 1,
                parentIsShell: persistent.parentElement === shell,
                immediatelyBeforeFooter: persistent.nextElementSibling === footer,
                proxyCount: tableElement.querySelectorAll('.erp-table-persistent-horizontal-scroll').length,
                insideShell: persistentRect.top >= shellRect.top - 1
                    && persistentRect.bottom <= shellRect.bottom + 1,
            };
        });

        expect(geometry.overflowing).toBe(true);
        expect(geometry.visibleBottomDelta).toBeLessThanOrEqual(2);
        expect(geometry.leftDelta).toBeLessThanOrEqual(2);
        expect(geometry.widthDelta).toBeLessThanOrEqual(2);
        expect(geometry.trackDelta).toBeLessThanOrEqual(2);
        expect(geometry.parentIsShell).toBe(true);
        expect(geometry.immediatelyBeforeFooter).toBe(true);
        expect(geometry.proxyCount).toBe(1);
        expect(geometry.insideShell).toBe(true);

        const scrollProgress = async (selector: string): Promise<number> => page.locator(selector).evaluate((element) => {
            const maximum = element.scrollWidth - element.clientWidth;

            return maximum > 0 ? element.scrollLeft / maximum : 0;
        });

        await mainScroller.evaluate((element) => {
            element.scrollLeft = (element.scrollWidth - element.clientWidth) * 0.7;
            element.dispatchEvent(new Event('scroll'));
        });
        await expect.poll(async () => Math.abs(
            await scrollProgress('#funds-crud-table .erp-table-main-scroll')
            - await scrollProgress('#funds-crud-table .erp-table-persistent-horizontal-scroll'),
        )).toBeLessThan(0.02);

        await persistentScroller.evaluate((element) => {
            element.scrollLeft = (element.scrollWidth - element.clientWidth) * 0.35;
            element.dispatchEvent(new Event('scroll'));
        });
        await expect.poll(async () => Math.abs(
            await scrollProgress('#funds-crud-table .erp-table-main-scroll')
            - await scrollProgress('#funds-crud-table .erp-table-persistent-horizontal-scroll'),
        )).toBeLessThan(0.02);

        await persistentScroller.focus();
        const beforeKeyboard = await scrollProgress('#funds-crud-table .erp-table-persistent-horizontal-scroll');
        await page.keyboard.press('ArrowLeft');
        await expect.poll(() => scrollProgress('#funds-crud-table .erp-table-persistent-horizontal-scroll'))
            .toBeLessThan(beforeKeyboard);
        await expect(persistentScroller).toBeFocused();

        await content.evaluate((element) => {
            element.scrollTop = element.scrollHeight;
        });
        await expect.poll(() => table.evaluate((tableElement) => {
            const persistent = tableElement.querySelector<HTMLElement>('.erp-table-persistent-horizontal-scroll');
            const footer = tableElement.querySelector<HTMLElement>('.erp-table-footer');

            if (!persistent || !footer) return Number.POSITIVE_INFINITY;

            return Math.abs(
                footer.getBoundingClientRect().top
                - persistent.getBoundingClientRect().bottom,
            );
        })).toBeLessThanOrEqual(2);
    });

    // Mobile contract: the trailing action-button column is hidden on touch
    // (tiny targets in a horizontally-scrolling table are unreliable and
    // redundant); tapping the full-width row navigates to the detail page
    // where every action lives. The data region still scrolls horizontally.
    test('mobile: actions column is hidden and the row tap navigates', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'mobile-chromium', 'Mobile-only characterization.');

        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        const scrollState = await page.locator('.erp-table-card--vue .erp-table-main-scroll').evaluate((el) => ({
            overflowX: getComputedStyle(el).overflowX,
            scrollable: el.scrollWidth > el.clientWidth,
        }));
        expect(scrollState.overflowX).toBe('auto');
        expect(scrollState.scrollable).toBe(true);
        await expect(page.locator('.erp-table-persistent-horizontal-scroll')).toBeHidden();

        await expect(page.locator('.erp-table-actions-cell').first()).toBeHidden();
        const mobileScroller = page.locator('.erp-table-card--vue .erp-table-main-scroll');
        const mobileScrollShell = page.locator('.erp-table-card--vue .erp-table-scroll');
        const mobileSelectionCell = page.locator('.erp-table-select-cell').first();
        const mobileCheckbox = mobileSelectionCell.locator('input[type="checkbox"]');

        await expect.poll(() => mobileCheckbox.evaluate((element) => getComputedStyle(element).opacity)).toBe('1');
        await expect.poll(() => mobileCheckbox.evaluate((element) => getComputedStyle(element).pointerEvents)).toBe('auto');
        await expect.poll(() => mobileSelectionCell.evaluate((element) => (
            getComputedStyle(element).backgroundColor
        ))).not.toBe('rgba(0, 0, 0, 0)');
        await expect.poll(() => mobileSelectionCell.evaluate((element) => (
            getComputedStyle(element).boxShadow
        ))).toBe('none');

        await mobileScroller.evaluate((element) => {
            element.scrollLeft = Math.min(80, element.scrollWidth - element.clientWidth);
        });
        await expect(mobileScrollShell).toHaveClass(/is-scrolled-x/);
        await expect.poll(() => mobileSelectionCell.evaluate((element) => (
            getComputedStyle(element).boxShadow
        ))).not.toBe('none');

        await mobileScroller.evaluate((element) => {
            element.scrollLeft = 0;
        });

        await page.locator('tbody tr').first().locator('td').nth(1).tap();
        await expect(page).toHaveURL(/\/gps\/[^/]+$/);
    });

    test('nested filter operator popover keeps the filter open and closes on a double Escape', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'desktop-chromium', 'Pointer/keyboard interaction.');

        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        const filter = page.locator('#gps-crud-table_filter_name');
        await page.locator('.erp-column-filter-trigger').first().click();
        await expect(filter).toBeVisible();

        const filterGeometry = await filter.evaluate((panel) => {
            const head = panel.querySelector<HTMLElement>('.erp-column-filter-head');
            const actions = panel.querySelector<HTMLElement>('.erp-column-filter-actions');

            if (!head || !actions) throw new Error('Expected filter header and actions.');

            const panelRect = panel.getBoundingClientRect();
            const headRect = head.getBoundingClientRect();
            const actionsRect = actions.getBoundingClientRect();

            return {
                padding: getComputedStyle(panel).padding,
                headLeftInset: Math.abs(headRect.left - panelRect.left),
                headRightInset: Math.abs(panelRect.right - headRect.right),
                actionsLeftInset: Math.abs(actionsRect.left - panelRect.left),
                actionsRightInset: Math.abs(panelRect.right - actionsRect.right),
            };
        });

        expect(filterGeometry.padding).toBe('0px');
        expect(filterGeometry.headLeftInset).toBeLessThanOrEqual(2);
        expect(filterGeometry.headRightInset).toBeLessThanOrEqual(2);
        expect(filterGeometry.actionsLeftInset).toBeLessThanOrEqual(2);
        expect(filterGeometry.actionsRightInset).toBeLessThanOrEqual(2);

        await filter.locator('.ui-dropdown-trigger').click();
        await page.getByRole('option', { name: 'Equals' }).click();
        // Choosing an operator must NOT dismiss the parent filter popover.
        await expect(filter).toBeVisible();

        await filter.locator('.ui-dropdown-trigger').click();
        await expect(page.locator('.ui-dropdown-menu--reka')).toBeVisible();
        await page.keyboard.press('Escape');
        await expect(page.locator('.ui-dropdown-menu--reka')).toBeHidden();
        await expect(filter).toBeVisible();
        await page.keyboard.press('Escape');
        await expect(filter).toBeHidden();
    });

    test('select-all exposes the mixed tri-state and the toolbar never overlaps', async ({ page }, testInfo) => {
        test.skip(testInfo.project.name !== 'mobile-chromium', 'Mobile toolbar characterization.');

        await login(page);
        await page.goto('/__kview/inertia/crud-table');

        // Partial selection -> indeterminate / aria-checked mixed.
        await page.locator('.erp-table-select-cell input[type="checkbox"]').first().check();
        const state = await page.locator('.erp-table-select-head input').evaluate((el: HTMLInputElement) => ({
            indeterminate: el.indeterminate,
            ariaChecked: el.getAttribute('aria-checked'),
        }));
        expect(state.indeterminate).toBe(true);
        expect(state.ariaChecked).toBe('mixed');

        // Select all -> toolbar commands wrap without overlapping.
        await page.locator('input[aria-label="Select all visible rows"]').check();
        const overlaps = await page.locator('.erp-table-card--vue .erp-table-toolbar').evaluate((tb) => {
            const kids = [...tb.querySelectorAll(
                '.erp-table-toolbar-actions button, '
                + '.erp-table-toolbar-actions a, '
                + '.erp-table-toolbar-actions .erp-selection-count',
            )]
                .filter((e) => e.getBoundingClientRect().width > 0);
            const rects = kids.map((e) => e.getBoundingClientRect());
            let count = 0;
            for (let i = 0; i < rects.length; i += 1) {
                for (let j = i + 1; j < rects.length; j += 1) {
                    const a = rects[i];
                    const b = rects[j];
                    if (a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom) count += 1;
                }
            }
            return count;
        });
        expect(overlaps).toBe(0);
    });
});
