import AxeBuilder from '@axe-core/playwright';
import { expect, test, type Page, type TestInfo } from '@playwright/test';
import { writeFile } from 'node:fs/promises';

type BaselineMetrics = {
    domContentLoadedMs: number;
    loadMs: number;
    responseMs: number;
    transferredBytes: number;
    decodedBytes: number;
    longTaskCount: number;
    longTaskDurationMs: number;
    lcpMs: number | null;
    inpMs: number | null;
    inpStatus: 'unsupported' | 'below-16ms-threshold' | 'measured';
};

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

const installRuntimeObservers = async (page: Page): Promise<void> => {
    await page.addInitScript(() => {
        const state = {
            longTasks: [] as number[],
            lcpMs: null as number | null,
            interactions: {} as Record<string, number>,
            eventTimingSupported: false,
        };

        Object.defineProperty(window, '__kview_investorBaseline', {
            configurable: true,
            value: state,
        });

        try {
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries()) {
                    state.longTasks.push(entry.duration);
                }
            }).observe({ type: 'longtask', buffered: true });
        } catch {
            // Long-task observation is not supported by every browser engine.
        }

        try {
            new PerformanceObserver((list) => {
                const entries = list.getEntries();
                const latest = entries.at(-1);

                if (latest) {
                    state.lcpMs = latest.startTime;
                }
            }).observe({ type: 'largest-contentful-paint', buffered: true });
        } catch {
            // LCP observation is not supported by every browser engine.
        }

        try {
            new PerformanceObserver((list) => {
                for (const entry of list.getEntries() as PerformanceEventTiming[]) {
                    if (entry.interactionId <= 0) {
                        continue;
                    }

                    const interactionId = String(entry.interactionId);
                    state.interactions[interactionId] = Math.max(
                        state.interactions[interactionId] ?? 0,
                        entry.duration,
                    );
                }
            }).observe({
                type: 'event',
                buffered: true,
                durationThreshold: 16,
            } as PerformanceObserverInit & { durationThreshold: number });
            state.eventTimingSupported = true;
        } catch {
            // Event Timing observation is not supported by every browser engine.
        }
    });
};

const waitForPaintMetrics = async (page: Page): Promise<void> => {
    await page.evaluate(() => new Promise<void>((resolve) => {
        requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
    }));
};

const collectMetrics = async (page: Page): Promise<BaselineMetrics> => page.evaluate(() => {
    const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
    const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
    const state = (window as typeof window & {
        __kview_investorBaseline?: {
            longTasks: number[];
            lcpMs: number | null;
            interactions: Record<string, number>;
            eventTimingSupported: boolean;
        };
    }).__kview_investorBaseline;
    const longTasks = state?.longTasks ?? [];
    const interactionDurations = Object.values(state?.interactions ?? {}).sort((a, b) => b - a);
    const inpIndex = Math.min(Math.floor(interactionDurations.length / 50), interactionDurations.length - 1);
    const inpStatus = !state?.eventTimingSupported
        ? 'unsupported'
        : inpIndex < 0
            ? 'below-16ms-threshold'
            : 'measured';

    return {
        domContentLoadedMs: Math.round(navigation.domContentLoadedEventEnd),
        loadMs: Math.round(navigation.loadEventEnd),
        responseMs: Math.round(navigation.responseEnd),
        transferredBytes: Math.round(resources.reduce((total, entry) => total + entry.transferSize, 0)),
        decodedBytes: Math.round(resources.reduce((total, entry) => total + entry.decodedBodySize, 0)),
        longTaskCount: longTasks.length,
        longTaskDurationMs: Math.round(longTasks.reduce((total, duration) => total + duration, 0)),
        lcpMs: state?.lcpMs === null || state?.lcpMs === undefined ? null : Math.round(state.lcpMs),
        inpMs: inpIndex < 0 ? null : Math.round(interactionDurations[inpIndex]),
        inpStatus,
    };
});

const attachJson = async (testInfo: TestInfo, name: string, value: unknown): Promise<void> => {
    const path = testInfo.outputPath(`${name}.json`);
    const body = JSON.stringify(value, null, 2);

    await writeFile(path, body);
    await testInfo.attach(name, {
        path,
        contentType: 'application/json',
    });
};

const attachScreenshot = async (page: Page, testInfo: TestInfo, name: string): Promise<void> => {
    const path = testInfo.outputPath(`${name}.png`);

    await page.screenshot({ path });
    await testInfo.attach(name, {
        path,
        contentType: 'image/png',
    });
};

const blockingAxeViolations = async (page: Page, testInfo: TestInfo) => {
    const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
        .analyze();

    await attachJson(testInfo, 'axe-results', results);

    return results.violations.filter((violation) => violation.impact === 'critical');
};

const assertNoHorizontalPageOverflow = async (page: Page): Promise<void> => {
    const overflow = await page.evaluate(() => ({
        clientWidth: document.documentElement.clientWidth,
        scrollWidth: document.documentElement.scrollWidth,
    }));

    expect(overflow.scrollWidth, JSON.stringify(overflow)).toBeLessThanOrEqual(overflow.clientWidth + 1);
};

test('login renders, validates credentials, and has no critical accessibility violation', async ({ page }, testInfo) => {
    const usernameInput = page.getByRole('textbox', { name: /^Username/ });

    await installRuntimeObservers(page);
    await page.goto('/login');

    await expect(page).toHaveTitle('Login · kView INVESTOR');
    await expect(page.getByRole('heading', { name: 'kView INVESTOR by kView' })).toBeVisible();
    await expect(usernameInput).toBeFocused();
    await assertNoHorizontalPageOverflow(page);

    await waitForPaintMetrics(page);
    const metrics = await collectMetrics(page);
    await attachJson(testInfo, 'login-performance', metrics);

    await usernameInput.fill('missing-e2e-user@kview-investor.local');
    await page.getByLabel('Password').fill('invalid-password');
    await page.getByRole('button', { name: 'Login' }).click();

    await expect(page).toHaveURL(/\/login$/);
    await expect(page.getByText('These credentials do not match our records.')).toBeVisible();
    await expect(usernameInput).toHaveValue('missing-e2e-user@kview-investor.local');

    await attachScreenshot(page, testInfo, 'login-screenshot');

    const criticalViolations = await blockingAxeViolations(page, testInfo);
    expect(criticalViolations).toEqual([]);

    console.log(`BASELINE login ${testInfo.project.name} ${JSON.stringify(metrics)}`);
});

test('authenticated navigation reaches Home and Funds without runtime errors', async ({ page }, testInfo) => {
    test.skip(!email || !password, 'Set E2E_EMAIL and E2E_PASSWORD to run the authenticated baseline.');

    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 installRuntimeObservers(page);
    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(/\/$/);

    if (testInfo.project.name === 'mobile-chromium') {
        // The mobile drawer starts closed (aria-hidden/inert); open it so the
        // nav and its Funds link are reachable, same as any real mobile user
        // navigating from Home to Funds.
        await page.getByRole('button', { name: 'Afficher la navigation' }).click();
    }

    const primaryNavigation = page.getByRole('navigation', { name: 'Navigation principale' });
    const fundsLink = primaryNavigation.getByRole('link', { name: 'Funds', exact: true });
    const brandLogo = page.locator('.application-shell__brand-mark img');

    await expect(brandLogo).toBeVisible();
    await expect.poll(() => brandLogo.evaluate((image: HTMLImageElement) => (
        image.complete && image.naturalWidth > 0 && image.naturalHeight > 0
    ))).toBe(true);

    if (testInfo.project.name === 'mobile-chromium') {
        await expect(primaryNavigation).toBeVisible();
        await expect(primaryNavigation.getByRole('link', { name: 'Home', exact: true })).toHaveAttribute('aria-current', 'page');
    } else {
        await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible();
    }

    await assertNoHorizontalPageOverflow(page);

    await waitForPaintMetrics(page);
    const homeMetrics = await collectMetrics(page);
    await attachScreenshot(page, testInfo, 'home-screenshot');
    // Use an epoch-based clock: Phase 8 moved Home into the persistent Inertia
    // shell, so Home -> Funds is now a same-document SPA visit and
    // `performance.now()` no longer resets here. The epoch clock still works
    // for the (now unused) hard-navigation case, so it is kept as the single
    // codepath rather than branching on shell state.
    const navigationStartedAt = await page.evaluate(() => performance.timeOrigin + performance.now());

    await fundsLink.click();

    await expect(page).toHaveURL(/\/funds$/);
    await expect(page.getByText('Funds', { exact: true }).first()).toBeVisible();
    await assertNoHorizontalPageOverflow(page);
    const navigationDurationMs = await page.evaluate(
        (startedAt) => Math.round((performance.timeOrigin + performance.now()) - startedAt),
        navigationStartedAt,
    );

    await attachScreenshot(page, testInfo, 'funds-screenshot');

    const fundsTable = page.locator('#funds-crud-table');
    const loadedRows = fundsTable.locator('tbody tr');

    if (await loadedRows.count()) {
        const search = fundsTable.locator('.erp-table-search input');

        await search.fill('__codex_no_matching_fund__');
        await search.press('Enter');

        const noResults = fundsTable.locator('.erp-table-empty-state');
        await expect(noResults).toBeVisible();

        const geometry = await fundsTable.evaluate((root) => {
            const mainElement = root.querySelector<HTMLElement>('.erp-table-main-scroll')!;
            const stateElement = root.querySelector<HTMLElement>('.erp-table-empty-state')!;
            const main = mainElement.getBoundingClientRect();
            const state = stateElement.getBoundingClientRect();

            return {
                leftInset: state.left - main.left,
                rightInset: main.right - state.right,
                borderBottomWidth: Number.parseFloat(getComputedStyle(stateElement).borderBottomWidth),
            };
        });

        expect(Math.abs(geometry.leftInset)).toBeLessThanOrEqual(1);
        expect(Math.abs(geometry.rightInset)).toBeLessThanOrEqual(1);
        expect(geometry.borderBottomWidth).toBeGreaterThanOrEqual(1);

        await search.fill('');
        await search.press('Enter');
        await expect(noResults).toHaveCount(0);
        await expect(loadedRows.first()).toBeVisible();
    } else {
        await expect(fundsTable.locator('.erp-table-empty-state')).toBeVisible();
        await expect(fundsTable.locator('.erp-table-select-head')).toHaveCount(0);
    }

    await waitForPaintMetrics(page);
    // Home -> Funds no longer crosses a document boundary, so the Navigation
    // Timing fields below (domContentLoadedMs, loadMs, responseMs,
    // transferredBytes, decodedBytes) still describe the original Home
    // document load, not the Funds transition specifically. LCP, long-task
    // and INP stay accurate: their PerformanceObservers were installed once
    // and keep observing across the same-document SPA visit. Only the INP
    // fields are asserted below; the duplicated document-level fields remain
    // for diagnostic context in the attached JSON artifact.
    const fundsMetrics = await collectMetrics(page);

    const criticalViolations = await blockingAxeViolations(page, testInfo);
    const relevantConsoleProblems = consoleProblems.filter((problem) => !problem.includes('Failed to load resource'));

    await attachJson(testInfo, 'home-performance', homeMetrics);
    await attachJson(testInfo, 'funds-performance', fundsMetrics);
    await attachJson(testInfo, 'navigation-performance', { navigationDurationMs });
    await attachJson(testInfo, 'console-problems', consoleProblems);

    expect(criticalViolations).toEqual([]);
    expect(relevantConsoleProblems).toEqual([]);
    expect(navigationDurationMs).toBeLessThanOrEqual(2500);
    expect(homeMetrics.lcpMs).not.toBeNull();
    expect(homeMetrics.lcpMs!).toBeLessThanOrEqual(2500);
    expect(fundsMetrics.inpStatus).not.toBe('unsupported');
    if (fundsMetrics.inpStatus === 'measured') {
        expect(fundsMetrics.inpMs).not.toBeNull();
        expect(fundsMetrics.inpMs ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(200);
    } else {
        expect(fundsMetrics.inpStatus).toBe('below-16ms-threshold');
        expect(fundsMetrics.inpMs).toBeNull();
    }

    console.log(`BASELINE home ${testInfo.project.name} ${JSON.stringify(homeMetrics)}`);
    console.log(`BASELINE funds ${testInfo.project.name} ${JSON.stringify(fundsMetrics)}`);
    console.log(`BASELINE funds-navigation ${testInfo.project.name} ${JSON.stringify({ navigationDurationMs })}`);
});
