// Responsibility: Typed YAML profile and managed browser script for L1 native Web readiness.

export interface HwlabRuntimeWebProbeNativeReadinessProfileSpec {
  readonly application: "tasktree" | "workbench" | "hwpod" | "caserun";
  readonly authentication?: "required" | "none";
  readonly path: string;
  readonly navigationTimeoutMs: number;
  readonly settleMs: number;
  readonly commandTimeoutSeconds: number;
  readonly viewport: { readonly width: number; readonly height: number };
  readonly readySelectors: readonly string[];
  readonly minBodyTextLength: number;
  readonly contentReadiness?: {
    readonly settledSelector: string;
    readonly itemSelector: string;
    readonly selectedSelector: string;
    readonly contentSelector: string;
    readonly timeoutMs: number;
  };
  readonly geometryChecks?: readonly {
    readonly selector: string;
    readonly maxHeightPx: number;
    readonly required: boolean;
  }[];
  readonly interaction: {
    readonly selector: string;
    readonly observedSelector: string;
    readonly observedAttribute: string;
  };
  readonly failedResponseStatus: number;
  readonly criticalPathPrefixes: readonly string[];
  readonly screenshotName: string;
  readonly outputLimits: {
    readonly errors: number;
    readonly failedResponses: number;
    readonly textPreviewChars: number;
  };
  readonly layout?: {
    readonly requireNoDocumentOverflow: boolean;
    readonly requireNoMainContentOverflow: boolean;
    readonly mainContentSelector: string;
    readonly internalScrollSelectors: readonly string[];
  };
}

export function parseWebProbeNativeReadinessProfiles(
  value: unknown,
  path: string,
): Readonly<Record<string, HwlabRuntimeWebProbeNativeReadinessProfileSpec>> {
  const raw = object(value, path);
  const entries = Object.entries(raw);
  if (entries.length < 1 || entries.length > 20) throw new Error(`${path} must contain 1-20 profiles`);
  return Object.fromEntries(entries.map(([name, profile]) => {
    simpleId(name, `${path}.${name}`);
    return [name, nativeReadinessProfile(profile, `${path}.${name}`)];
  }));
}

export function nodeWebProbeNativeReadinessScript(
  profileName: string,
  profile: HwlabRuntimeWebProbeNativeReadinessProfileSpec,
  configRef: string,
): string {
  const config = { profileName, configRef, ...profile };
  return String.raw`const config = ${JSON.stringify(config)};

export default async function nativeReadiness({ page, goto, wait, screenshot, jsonArtifact, recordStep }) {
  const consoleErrors = [];
  const pageErrors = [];
  const failedResponses = [];
  let consoleErrorCount = 0;
  let pageErrorCount = 0;
  let failedResponseCount = 0;
  const short = (value, limit = 300) => String(value || "").slice(0, limit);
  const criticalPath = (url) => {
    try {
      const path = new URL(url).pathname;
      return config.criticalPathPrefixes.some((prefix) => path.startsWith(prefix));
    } catch {
      return false;
    }
  };
  page.on("console", (message) => {
    if (message.type() !== "error") return;
    consoleErrorCount += 1;
    if (consoleErrors.length < config.outputLimits.errors) {
      const location = message.location();
      consoleErrors.push({ message: short(message.text()), url: short(location?.url, 500) || null });
    }
  });
  page.on("pageerror", (error) => {
    pageErrorCount += 1;
    if (pageErrors.length < config.outputLimits.errors) pageErrors.push(short(error?.stack || error?.message || error));
  });
  page.on("requestfailed", (request) => {
    if (!criticalPath(request.url())) return;
    failedResponseCount += 1;
    if (failedResponses.length < config.outputLimits.failedResponses) {
      failedResponses.push({ method: request.method(), path: new URL(request.url()).pathname, failure: short(request.failure()?.errorText, 160) });
    }
  });
  page.on("response", (response) => {
    if (response.status() < config.failedResponseStatus || !criticalPath(response.url())) return;
    failedResponseCount += 1;
    if (failedResponses.length < config.outputLimits.failedResponses) {
      failedResponses.push({ method: response.request().method(), path: new URL(response.url()).pathname, status: response.status() });
    }
  });

  await page.setViewportSize(config.viewport);
  await goto(config.path, { selectors: config.readySelectors, readinessTimeoutMs: config.navigationTimeoutMs, attempts: 2 });
  let contentReadiness = null;
  if (config.contentReadiness) {
    const content = config.contentReadiness;
    await page.locator(content.settledSelector).waitFor({ state: "visible", timeout: content.timeoutMs });
    const item = page.locator(content.itemSelector).first();
    const itemCount = await page.locator(content.itemSelector).count();
    if (itemCount > 0) {
      await item.click({ timeout: content.timeoutMs });
      await page.locator(content.selectedSelector).first().waitFor({ state: "visible", timeout: content.timeoutMs });
      await page.locator(content.contentSelector).first().waitFor({ state: "visible", timeout: content.timeoutMs });
    }
    contentReadiness = { settled: true, itemCount, selected: itemCount > 0 };
  }
  if (config.settleMs > 0) await wait(config.settleMs);
  const before = await page.locator(config.interaction.observedSelector).getAttribute(config.interaction.observedAttribute);
  const mounted = await page.evaluate(({ selectors, minBodyTextLength, textPreviewChars, layout }) => {
    const visible = (selector) => {
      const element = document.querySelector(selector);
      const rect = element?.getBoundingClientRect();
      const style = element ? getComputedStyle(element) : null;
      return Boolean(rect && rect.width > 0 && rect.height > 0 && style?.display !== "none" && style?.visibility !== "hidden");
    };
    const bodyText = (document.body?.textContent || "").replace(/\s+/gu, " ").trim();
    const overflow = (element) => element === null ? null : ({
      clientWidth: element.clientWidth,
      clientHeight: element.clientHeight,
      scrollWidth: element.scrollWidth,
      scrollHeight: element.scrollHeight,
      horizontal: element.scrollWidth > element.clientWidth,
      vertical: element.scrollHeight > element.clientHeight,
    });
    const documentOverflow = overflow(document.documentElement);
    const bodyOverflow = overflow(document.body);
    const mainContentOverflow = layout ? overflow(document.querySelector(layout.mainContentSelector)) : null;
    const internalScrollPanes = layout ? layout.internalScrollSelectors.map((selector) => {
      const element = document.querySelector(selector);
      const style = element ? getComputedStyle(element) : null;
      return { selector, found: element !== null, overflow: overflow(element), overflowX: style?.overflowX ?? null, overflowY: style?.overflowY ?? null };
    }) : [];
    const noDocumentOverflow = !layout?.requireNoDocumentOverflow || Boolean(documentOverflow && bodyOverflow
      && !documentOverflow.horizontal && !documentOverflow.vertical && !bodyOverflow.horizontal && !bodyOverflow.vertical);
    const noMainContentOverflow = !layout?.requireNoMainContentOverflow || Boolean(mainContentOverflow
      && !mainContentOverflow.horizontal && !mainContentOverflow.vertical);
    const internalScrollPanesReady = !layout || internalScrollPanes.every((item) => item.found && ["auto", "scroll"].includes(item.overflowY));
    return {
      readyState: document.readyState,
      selectorsVisible: selectors.map((selector) => ({ selector, visible: visible(selector) })),
      bodyTextLength: bodyText.length,
      bodyTextPreview: bodyText.slice(0, textPreviewChars),
      bodyTextSufficient: bodyText.length >= minBodyTextLength,
      finalPath: location.pathname + location.search,
      title: document.title,
      layout: { documentOverflow, bodyOverflow, mainContentOverflow, internalScrollPanes, noDocumentOverflow, noMainContentOverflow, internalScrollPanesReady },
    };
  }, { selectors: config.readySelectors, minBodyTextLength: config.minBodyTextLength, textPreviewChars: config.outputLimits.textPreviewChars, layout: config.layout });
  await page.locator(config.interaction.selector).click({ timeout: config.navigationTimeoutMs });
  if (config.settleMs > 0) await wait(config.settleMs);
  const after = await page.locator(config.interaction.observedSelector).getAttribute(config.interaction.observedAttribute);
  const interactionChanged = before !== after;
  const geometryChecks = await page.evaluate((checks) => checks.map((check) => {
    const heights = [...document.querySelectorAll(check.selector)].map((element) => element.getBoundingClientRect().height);
    const maxHeight = heights.length > 0 ? Math.max(...heights) : null;
    return {
      selector: check.selector,
      required: check.required,
      matched: heights.length,
      maxHeight,
      maxHeightPx: check.maxHeightPx,
      ok: heights.length === 0 ? !check.required : maxHeight <= check.maxHeightPx,
    };
  }), config.geometryChecks || []);
  const geometryReady = geometryChecks.every((check) => check.ok);
  const screenshotArtifact = await screenshot(config.screenshotName);
  const selectorsReady = mounted.selectorsVisible.every((item) => item.visible);
  const layoutReady = mounted.layout.noDocumentOverflow && mounted.layout.noMainContentOverflow && mounted.layout.internalScrollPanesReady;
  const ok = selectorsReady && mounted.bodyTextSufficient && interactionChanged && layoutReady && geometryReady
    && consoleErrorCount === 0 && pageErrorCount === 0 && failedResponseCount === 0;
  const evidence = {
    ok,
    status: ok ? "pass" : "blocked",
    profileName: config.profileName,
    configRef: config.configRef,
    mounted,
    contentReadiness,
    interaction: { selector: config.interaction.selector, observedSelector: config.interaction.observedSelector, observedAttribute: config.interaction.observedAttribute, before, after, changed: interactionChanged },
    geometryChecks,
    consoleErrorCount,
    consoleErrors,
    pageErrorCount,
    pageErrors,
    failedResponseCount,
    failedResponses,
    screenshot: screenshotArtifact,
    valuesRedacted: true,
  };
  const report = await jsonArtifact("native-web-readiness.json", evidence);
  recordStep("native-web-readiness", { ok, selectorsReady, bodyTextSufficient: mounted.bodyTextSufficient, contentReadiness, interactionChanged, layoutReady, geometryReady, consoleErrorCount, pageErrorCount, failedResponseCount });
  return { ...evidence, report };
}
`;
}

function nativeReadinessProfile(value: unknown, path: string): HwlabRuntimeWebProbeNativeReadinessProfileSpec {
  const raw = object(value, path);
  onlyKeys(raw, ["application", "authentication", "path", "navigationTimeoutMs", "settleMs", "commandTimeoutSeconds", "viewport", "readySelectors", "minBodyTextLength", "contentReadiness", "geometryChecks", "interaction", "failedResponseStatus", "criticalPathPrefixes", "screenshotName", "outputLimits", "layout"], path);
  const viewport = object(raw.viewport, `${path}.viewport`);
  onlyKeys(viewport, ["width", "height"], `${path}.viewport`);
  const interaction = object(raw.interaction, `${path}.interaction`);
  onlyKeys(interaction, ["selector", "observedSelector", "observedAttribute"], `${path}.interaction`);
  const limits = object(raw.outputLimits, `${path}.outputLimits`);
  onlyKeys(limits, ["errors", "failedResponses", "textPreviewChars"], `${path}.outputLimits`);
  const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120);
  const contentReadiness = raw.contentReadiness === undefined ? undefined : nativeReadinessContent(raw.contentReadiness, `${path}.contentReadiness`);
  const geometryChecks = raw.geometryChecks === undefined ? undefined : nativeReadinessGeometryChecks(raw.geometryChecks, `${path}.geometryChecks`);
  const layout = raw.layout === undefined ? undefined : nativeReadinessLayout(raw.layout, `${path}.layout`);
  if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`);
  return {
    application: enumText(raw.application, `${path}.application`, ["tasktree", "workbench", "hwpod", "caserun"]),
    ...(raw.authentication === undefined ? {} : { authentication: enumText(raw.authentication, `${path}.authentication`, ["required", "none"]) }),
    path: absolutePath(raw.path, `${path}.path`),
    navigationTimeoutMs: integer(raw.navigationTimeoutMs, `${path}.navigationTimeoutMs`, 1000, 120_000),
    settleMs: integer(raw.settleMs, `${path}.settleMs`, 0, 30_000),
    commandTimeoutSeconds: integer(raw.commandTimeoutSeconds, `${path}.commandTimeoutSeconds`, 60, 3600),
    viewport: { width: integer(viewport.width, `${path}.viewport.width`, 240, 7680), height: integer(viewport.height, `${path}.viewport.height`, 240, 4320) },
    readySelectors: textArray(raw.readySelectors, `${path}.readySelectors`, 1, 20, 500),
    minBodyTextLength: integer(raw.minBodyTextLength, `${path}.minBodyTextLength`, 1, 100_000),
    ...(contentReadiness === undefined ? {} : { contentReadiness }),
    ...(geometryChecks === undefined ? {} : { geometryChecks }),
    interaction: {
      selector: boundedText(interaction.selector, `${path}.interaction.selector`, 500),
      observedSelector: boundedText(interaction.observedSelector, `${path}.interaction.observedSelector`, 500),
      observedAttribute: boundedText(interaction.observedAttribute, `${path}.interaction.observedAttribute`, 100),
    },
    failedResponseStatus: integer(raw.failedResponseStatus, `${path}.failedResponseStatus`, 400, 599),
    criticalPathPrefixes: textArray(raw.criticalPathPrefixes, `${path}.criticalPathPrefixes`, 1, 20, 300).map((item, index) => absolutePath(item, `${path}.criticalPathPrefixes[${index}]`)),
    screenshotName,
    outputLimits: { errors: integer(limits.errors, `${path}.outputLimits.errors`, 1, 100), failedResponses: integer(limits.failedResponses, `${path}.outputLimits.failedResponses`, 1, 100), textPreviewChars: integer(limits.textPreviewChars, `${path}.outputLimits.textPreviewChars`, 20, 2000) },
    layout,
  };
}

function nativeReadinessContent(value: unknown, path: string): NonNullable<HwlabRuntimeWebProbeNativeReadinessProfileSpec["contentReadiness"]> {
  const raw = object(value, path);
  onlyKeys(raw, ["settledSelector", "itemSelector", "selectedSelector", "contentSelector", "timeoutMs"], path);
  return {
    settledSelector: boundedText(raw.settledSelector, `${path}.settledSelector`, 500),
    itemSelector: boundedText(raw.itemSelector, `${path}.itemSelector`, 500),
    selectedSelector: boundedText(raw.selectedSelector, `${path}.selectedSelector`, 500),
    contentSelector: boundedText(raw.contentSelector, `${path}.contentSelector`, 500),
    timeoutMs: integer(raw.timeoutMs, `${path}.timeoutMs`, 1000, 120_000),
  };
}

function nativeReadinessGeometryChecks(value: unknown, path: string): NonNullable<HwlabRuntimeWebProbeNativeReadinessProfileSpec["geometryChecks"]> {
  if (!Array.isArray(value) || value.length < 1 || value.length > 20) throw new Error(`${path} must contain 1-20 items`);
  return value.map((item, index) => {
    const itemPath = `${path}[${index}]`;
    const raw = object(item, itemPath);
    onlyKeys(raw, ["selector", "maxHeightPx", "required"], itemPath);
    return {
      selector: boundedText(raw.selector, `${itemPath}.selector`, 500),
      maxHeightPx: integer(raw.maxHeightPx, `${itemPath}.maxHeightPx`, 1, 4320),
      required: boolean(raw.required, `${itemPath}.required`),
    };
  });
}

function nativeReadinessLayout(value: unknown, path: string): NonNullable<HwlabRuntimeWebProbeNativeReadinessProfileSpec["layout"]> {
  const raw = object(value, path);
  onlyKeys(raw, ["requireNoDocumentOverflow", "requireNoMainContentOverflow", "mainContentSelector", "internalScrollSelectors"], path);
  return {
    requireNoDocumentOverflow: boolean(raw.requireNoDocumentOverflow, `${path}.requireNoDocumentOverflow`),
    requireNoMainContentOverflow: boolean(raw.requireNoMainContentOverflow, `${path}.requireNoMainContentOverflow`),
    mainContentSelector: boundedText(raw.mainContentSelector, `${path}.mainContentSelector`, 500),
    internalScrollSelectors: textArray(raw.internalScrollSelectors, `${path}.internalScrollSelectors`, 1, 20, 500),
  };
}

function object(value: unknown, path: string): Record<string, unknown> { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record<string, unknown>; }
function onlyKeys(value: Record<string, unknown>, allowed: readonly string[], path: string): void { const extra = Object.keys(value).filter((key) => !allowed.includes(key)); if (extra.length > 0) throw new Error(`${path} unsupported fields: ${extra.sort().join(", ")}`); }
function boundedText(value: unknown, path: string, max: number): string { if (typeof value !== "string" || value.length < 1 || value.length > max) throw new Error(`${path} must be a 1-${max} character string`); return value; }
function simpleId(value: string, path: string): string { if (!/^[a-z][a-z0-9-]*$/u.test(value)) throw new Error(`${path} must be a lower-case id`); return value; }
function integer(value: unknown, path: string, min: number, max: number): number { if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) throw new Error(`${path} must be an integer from ${min} to ${max}`); return Number(value); }
function boolean(value: unknown, path: string): boolean { if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`); return value; }
function enumText<T extends string>(value: unknown, path: string, allowed: readonly T[]): T { if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`${path} must be one of ${allowed.join(", ")}`); return value as T; }
function absolutePath(value: unknown, path: string): string { const text = boundedText(value, path, 1000); if (!text.startsWith("/")) throw new Error(`${path} must start with /`); return text; }
function textArray(value: unknown, path: string, min: number, max: number, itemMax: number): string[] { if (!Array.isArray(value) || value.length < min || value.length > max) throw new Error(`${path} must contain ${min}-${max} items`); return value.map((item, index) => boundedText(item, `${path}[${index}]`, itemMax)); }
