/**
 * Accessibility helpers for WCAG 2.2 AA testing
 */

import type { Locator, Page } from "@playwright/test";

/**
 * WCAG 2.2 AA contrast ratio requirements
 */
export const WCAG_CONTRAST = {
	normalText: 4.5,
	largeText: 3,
	uiComponents: 3,
} as const;

/**
 * Check if an element has visible focus indicator
 */
export async function hasFocusVisible(
	page: Page,
	locator: Locator,
): Promise<boolean> {
	return page.evaluate(
		(element: Element) => {
			const style = window.getComputedStyle(element);
			const outline = style.outlineWidth;
			const boxShadow = style.boxShadow;

			// Check for outline or box-shadow focus indicator
			return (
				(outline && outline !== "0px" && outline !== "none") ||
				(boxShadow && boxShadow !== "none")
			);
		},
		(await locator.elementHandle()) as Element,
	);
}

/**
 * Get computed color values for an element
 */
export async function getComputedColors(
	page: Page,
	locator: Locator,
): Promise<{
	color: string;
	backgroundColor: string;
}> {
	return page.evaluate(
		(element: Element) => {
			const style = window.getComputedStyle(element);
			return {
				color: style.color,
				backgroundColor: style.backgroundColor,
			};
		},
		(await locator.elementHandle()) as Element,
	);
}

/**
 * Calculate relative luminance of a color
 * https://www.w3.org/TR/WCAG20/#relativeluminancedef
 */
export function getLuminance(r: number, g: number, b: number): number {
	const [rs, gs, bs] = [r, g, b].map((val) => {
		const sRGB = val / 255;
		return sRGB <= 0.03928 ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
	});

	return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

/**
 * Parse RGB color string
 */
export function parseRGB(
	color: string,
): { r: number; g: number; b: number } | null {
	const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
	if (!match) return null;

	return {
		r: parseInt(match[1], 10),
		g: parseInt(match[2], 10),
		b: parseInt(match[3], 10),
	};
}

/**
 * Parse RGBA color string
 */
export function parseRGBA(
	color: string,
): { r: number; g: number; b: number; a: number } | null {
	const match = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
	if (!match) return null;

	return {
		r: parseInt(match[1], 10),
		g: parseInt(match[2], 10),
		b: parseInt(match[3], 10),
		a: parseFloat(match[4]),
	};
}

/**
 * Calculate contrast ratio between two luminances
 * https://www.w3.org/TR/WCAG20/#contrast-ratiodef
 */
export function getContrastRatio(lum1: number, lum2: number): number {
	const lighter = Math.max(lum1, lum2);
	const darker = Math.min(lum1, lum2);
	return (lighter + 0.05) / (darker + 0.05);
}

/**
 * Check contrast ratio between two colors
 */
export function checkContrast(color1: string, color2: string): number | null {
	const rgb1 = parseRGB(color1) || parseRGBA(color1);
	const rgb2 = parseRGB(color2) || parseRGBA(color2);

	if (!rgb1 || !rgb2) return null;

	const lum1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
	const lum2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);

	return getContrastRatio(lum1, lum2);
}

/**
 * Assert element meets WCAG AA contrast requirements
 */
export async function assertContrast(
	page: Page,
	locator: Locator,
	minRatio: number = WCAG_CONTRAST.normalText,
): Promise<void> {
	const colors = await getComputedColors(page, locator);
	const ratio = checkContrast(colors.color, colors.backgroundColor);

	if (ratio === null) {
		throw new Error("Could not calculate contrast ratio");
	}

	if (ratio < minRatio) {
		throw new Error(
			`Contrast ratio ${ratio.toFixed(2)} is below required ${minRatio} ` +
				`(color: ${colors.color}, background: ${colors.backgroundColor})`,
		);
	}
}

/**
 * Check if element has accessible name
 */
export async function hasAccessibleName(locator: Locator): Promise<boolean> {
	const name = await locator.getAttribute("aria-label");
	const labelledBy = await locator.getAttribute("aria-labelledby");
	const title = await locator.getAttribute("title");

	// Check for text content if it's a button or link
	const tagName = await locator.evaluate((el) => el.tagName.toLowerCase());
	let textContent = "";
	if (tagName === "button" || tagName === "a") {
		textContent = (await locator.textContent()) || "";
	}

	return !!(name || labelledBy || title || textContent.trim());
}

/**
 * Get accessible name for an element
 */
export async function getAccessibleName(
	page: Page,
	locator: Locator,
): Promise<string | null> {
	return page.evaluate(
		(element: Element) => {
			// Check aria-label
			const ariaLabel = element.getAttribute("aria-label");
			if (ariaLabel) return ariaLabel;

			// Check aria-labelledby
			const labelledBy = element.getAttribute("aria-labelledby");
			if (labelledBy) {
				const labelElement = document.getElementById(labelledBy);
				if (labelElement) return labelElement.textContent;
			}

			// Check title attribute
			const title = element.getAttribute("title");
			if (title) return title;

			// Check text content for interactive elements
			const tagName = element.tagName.toLowerCase();
			if (tagName === "button" || tagName === "a") {
				return element.textContent?.trim() || null;
			}

			return null;
		},
		(await locator.elementHandle()) as Element,
	);
}

/**
 * Test keyboard navigation through a set of elements
 */
export async function testKeyboardNavigation(
	page: Page,
	selectors: string[],
): Promise<void> {
	// Focus first element
	await page.focus(selectors[0]);

	for (let i = 0; i < selectors.length; i++) {
		const activeElement = await page.evaluate(
			() =>
				document.activeElement?.getAttribute("data-testid") ||
				document.activeElement?.getAttribute("aria-label") ||
				document.activeElement?.textContent?.trim() ||
				document.activeElement?.tagName,
		);

		// Check focus is visible
		const hasFocus = await page.evaluate(() => {
			const active = document.activeElement;
			if (!active || active === document.body) return false;

			const style = window.getComputedStyle(active);
			return (
				(style.outlineWidth && style.outlineWidth !== "0px") ||
				(style.boxShadow && style.boxShadow !== "none")
			);
		});

		if (!hasFocus) {
			throw new Error(`Focus not visible on element: ${activeElement}`);
		}

		// Press Tab to move to next element
		if (i < selectors.length - 1) {
			await page.keyboard.press("Tab");
		}
	}
}

/**
 * Assert focus is contained within a modal
 */
export async function assertFocusContained(
	page: Page,
	modalSelector: string,
): Promise<void> {
	const isContained = await page.evaluate((selector: string) => {
		const modal = document.querySelector(selector);
		if (!modal) return false;

		const activeElement = document.activeElement;
		if (!activeElement) return false;

		return modal.contains(activeElement);
	}, modalSelector);

	if (!isContained) {
		throw new Error("Focus is not contained within modal");
	}
}
