import { createHash } from "node:crypto";
import type { BrowserContext, ConsoleMessage, Page, Response } from "playwright";
import type { HarnessEvent } from "../../../core/src/classify/classifier.js";
import { matrixCellRef } from "../../../schema/src/records/context.js";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { AuthProof } from "../../../playwright/src/auth.js";
import {
  awaitSettled,
  readDocumentDirection,
  type CaptureOptions,
  type CellEvidence,
  type ConsoleEntry,
  type NetworkEntry,
  type PageProbe,
  type RunCellInput,
  type SettleOptions,
} from "../../../playwright/src/cell-runner.js";
import { captureCellScreenshots } from "../../../playwright/src/cell-original.js";
import type { RedactionRule } from "../../../playwright/src/redaction.js";
import {
  captureInteractionArtifacts,
  parseInteractionProbe,
  readInteractionProbe,
  runInteractionActions,
} from "./probe.js";
import type { CoupledNumericMeasurement, DiscoveredControl, InteractionCellEvidence, InteractionDiscovery } from "./types.js";
import {
  deriveDiscoveredControlOracle,
  type DiscoveredControlOracle,
  type DiscoveredControlOracleObservation,
} from "./oracles.js";
import type {
  DerivedPromiseCheck,
  JsonObservation,
  JourneyBrowserState,
} from "./types.js";
import { discoverInteractionControls } from "./discovery.js";
import type { CoverageEvent } from "../../../core/src/classify/classifier.js";
import type { PromiseContract } from "../../../feature/src/journey/schema.js";
import {
  parseUiJourneyPlan,
  type JourneyOracles,
  type UiJourneyPlan,
} from "../../../feature/src/journey/public.js";
import {
  runUiJourneyPlan,
  type JourneyOutcome,
} from "../../../feature/src/journey/runner.js";
import type { KernelStores } from "../../../core/src/classify/classifier.js";

export type { DerivedPromiseCheck, JourneyBrowserState } from "./types.js";
import { applyAuthAdapter } from "../cell-harness.js";

function compare(left: string, right: string): number {
  return left < right ? -1 : left > right ? 1 : 0;
}

export type InteractionCellResult =
  | { kind: "evidence"; evidence: InteractionCellEvidence; authProof?: AuthProof }
  | { kind: "coverage"; failedPrecondition: string }
  | { kind: "discovery"; discovery: InteractionDiscovery; observations: DiscoveredControlObservation[]; evidence: InteractionCellEvidence; authProof?: AuthProof }
  | {
      kind: "harness";
      outcome: HarnessEvent;
      partialEvidence?: InteractionCellEvidence;
    };

function seedToUint32(seed: string): number {
  const digest = createHash("sha256").update(seed).digest();
  return digest.readUInt32LE(0);
}

function buildDeterminismInitScript(clockStartMs: number, seed: string): string {
  const seedUint = seedToUint32(seed);
  return `(() => {
    const CLOCK_START_MS = ${String(clockStartMs)};
    const SEED = ${String(seedUint)};
    let tick = 0;
    const OriginalDate = Date;
    class PinnedDate extends OriginalDate {
      constructor(...args) {
        if (args.length === 0) {
          super(CLOCK_START_MS + tick);
          tick += 1;
          return;
        }
        super(...args);
      }
      static now() {
        const value = CLOCK_START_MS + tick;
        tick += 1;
        return value;
      }
    }
    PinnedDate.parse = OriginalDate.parse;
    PinnedDate.UTC = OriginalDate.UTC;
    PinnedDate.prototype = OriginalDate.prototype;
    Object.setPrototypeOf(PinnedDate, OriginalDate);
    globalThis.Date = PinnedDate;
    let state = SEED >>> 0;
    const nextRandom = () => {
      state = (state + 0x6d2b79f5) >>> 0;
      let t = Math.imul(state ^ (state >>> 15), state | 1);
      t = (t + Math.imul(t ^ (t >>> 7), t | 61)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
    Math.random = nextRandom;
  })();`;
}

function resolveTimezone(cell: MatrixCell): string {
  const fromState = cell.state.timezone;
  if (fromState !== undefined && fromState.trim().length > 0) {
    return fromState;
  }
  return "UTC";
}

function joinUrl(baseUrl: string, path: string): string {
  if (path.startsWith("http://") || path.startsWith("https://")) {
    return path;
  }
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
  return `${normalizedBase}${normalizedPath}`;
}

export type DiscoveredControlObservation = {
  control: DiscoveredControl;
  oracle: DiscoveredControlOracle;
  observation?: DiscoveredControlOracleObservation;
  numericObservations?: CoupledNumericMeasurement[];
};

function controlPath(path: string): string[] | undefined {
  if (!path.startsWith("frame:0:")) return undefined;
  return path.slice("frame:0:".length).split("/");
}

async function observeDiscoveredControl(page: Page, control: DiscoveredControl): Promise<{ observation: DiscoveredControlOracleObservation; numericObservations: CoupledNumericMeasurement[] } | undefined> {
  const path = controlPath(control.path);
  if (path === undefined) return undefined;
  return page.evaluate(async (segments) => {
    let element: Element | undefined;
    let parent: Element | Document = document;
    for (const segment of segments) {
      const match = /^([a-z0-9-]+):(\d+)$/.exec(segment);
      if (match === null) return undefined;
      const index = Number(match[2]);
      const candidate: Element | undefined = Array.from(parent.children)[index];
      if (candidate !== undefined && candidate.tagName.toLowerCase() !== match[1]) return undefined;
      if (candidate === undefined) return undefined;
      element = candidate;
      parent = candidate;
    }
    if (element === undefined || !(element instanceof HTMLElement)) return undefined;
    const before = element.getAttribute("aria-pressed") ?? undefined;
    const pathFor = (candidate: Element): string => {
      const parts: string[] = [];
      let current: Element | null = candidate;
      while (current !== null) {
        const parent: Element | null = current.parentElement;
        parts.push(`${current.tagName.toLowerCase()}:${String(parent === null ? 0 : Array.from(parent.children).indexOf(current))}`);
        current = parent;
      }
      return `frame:0:${parts.reverse().join("/")}`;
    };
    const numericDisplays = (): Array<{ identity: string; value: number }> => Array.from(document.querySelectorAll<HTMLElement>("body *")).flatMap((candidate) => {
      if (candidate === element || element.contains(candidate) || candidate.contains(element)) return [];
      const text = (candidate.getAttribute("aria-valuenow") ?? candidate.textContent).trim();
      if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(text)) return [];
      const value = Number(text);
      return Number.isFinite(value) ? [{ identity: pathFor(candidate), value }] : [];
    });
    const baseline = numericDisplays();
    await new Promise<void>((resolve) => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => { resolve(); });
      });
    });
    const baselineAfter = new Map(numericDisplays().map((display) => [display.identity, display.value]));
    const formControl = element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement ? element : undefined;
    const form = formControl?.form;
    let invalidFired = false;
    let submitFired = false;
    const invalid = () => { invalidFired = true; };
    const submit = (event: SubmitEvent) => { submitFired = true; event.preventDefault(); };
    formControl?.addEventListener("invalid", invalid);
    form?.addEventListener("submit", submit);
    const beforeUrl = location.href;
    const activeBefore = document.activeElement;
    const mutationObserver = new MutationObserver(() => {});
    mutationObserver.observe(document.documentElement, { attributes: true, characterData: true, childList: true, subtree: true });
    mutationObserver.takeRecords();
    try {
      if (formControl !== undefined && form !== null && form !== undefined) form.requestSubmit();
      else element.click();
      const immediateActivationRecords = mutationObserver.takeRecords();
      const immediateActivationHref = location.href;
      const immediateActivationActive = document.activeElement;
      await new Promise<void>((resolve) => { requestAnimationFrame(() => { resolve(); }); });
      const postFrameActivationRecords = mutationObserver.takeRecords();
      const postFrameActivationHref = location.href;
      const postFrameActivationActive = document.activeElement;
      const afterFirst = new Map(numericDisplays().map((display) => [display.identity, display.value]));
      const modal = Array.from(document.querySelectorAll<HTMLElement>('[role="dialog"][aria-modal="true"]'))
        .find((candidate) => !candidate.hidden && getComputedStyle(candidate).display !== "none");
      const backgroundOperable = modal !== undefined && Array.from(document.querySelectorAll<HTMLElement>('button:not(:disabled), a[href], input:not(:disabled)')).some((candidate) => {
        if (modal.contains(candidate)) return false;
        const style = getComputedStyle(candidate);
        const rect = candidate.getBoundingClientRect();
        const hit = rect.width > 0 && rect.height > 0
          ? document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)
          : null;
        return candidate.closest("[inert]") === null
          && candidate.getAttribute("aria-disabled") !== "true"
          && style.display !== "none"
          && style.visibility !== "hidden"
          && style.pointerEvents !== "none"
          && (hit === candidate || candidate.contains(hit));
      });
      const expanded = element.getAttribute("aria-expanded");
      const controlledId = element.getAttribute("aria-controls");
      const controlled = controlledId === null ? null : document.getElementById(controlledId);
      const controlledHidden = controlled?.hasAttribute("hidden") === true || controlled?.getAttribute("aria-hidden") === "true";
      const afterPressed = element.isConnected ? element.getAttribute("aria-pressed") : null;
      const navigated = immediateActivationHref !== beforeUrl || postFrameActivationHref !== beforeUrl;
      const reacted = immediateActivationRecords.length > 0
        || postFrameActivationRecords.length > 0
        || immediateActivationHref !== beforeUrl
        || postFrameActivationHref !== beforeUrl
        || immediateActivationActive !== activeBefore
        || postFrameActivationActive !== activeBefore;
      const comparablePressedState = afterPressed !== null && (!navigated || before !== undefined && afterPressed !== before);
      const observation = {
        reacted,
        ...(before === undefined ? {} : { before }),
        ...(comparablePressedState ? { after: afterPressed as string } : {}),
        ...(formControl === undefined ? {} : { checkValidity: formControl.checkValidity(), invalidFired, nativeSubmissionSuppressed: !submitFired }),
        ...(expanded === null || controlled === null ? {} : { explicitStateContradiction: (expanded === "true" && controlledHidden) || (expanded === "false" && !controlledHidden) }),
        ...(modal === undefined ? {} : { ariaModal: true, backgroundOperable }),
        ...(navigated ? { navigated: true } : {}),
      };
      if (formControl !== undefined && form !== null && form !== undefined) form.requestSubmit();
      else element.click();
      await new Promise<void>((resolve) => { requestAnimationFrame(() => { resolve(); }); });
      const afterSecond = new Map(numericDisplays().map((display) => [display.identity, display.value]));
      return {
        observation,
        numericObservations: baseline.map((display): CoupledNumericMeasurement => {
          const baselineValue = baselineAfter.get(display.identity);
          const first = afterFirst.get(display.identity);
          const second = afterSecond.get(display.identity);
          if (baselineValue === undefined) {
            return { kind: "missing", controlIdentity: pathFor(element), displayIdentity: display.identity, missingPhase: "baselineAfter" };
          }
          if (first === undefined) {
            return { kind: "missing", controlIdentity: pathFor(element), displayIdentity: display.identity, missingPhase: "afterFirst" };
          }
          if (second === undefined) {
            return { kind: "missing", controlIdentity: pathFor(element), displayIdentity: display.identity, missingPhase: "afterSecond" };
          }
          return { kind: "measured", observation: { controlIdentity: pathFor(element), displayIdentity: display.identity, before: display.value, baselineAfter: baselineValue, afterFirst: first, afterSecond: second } };
        }),
      };
    } finally {
      mutationObserver.disconnect();
      formControl?.removeEventListener("invalid", invalid);
      form?.removeEventListener("submit", submit);
    }
  }, path);
}

async function observeDiscoveredControls(input: {
  browser: RunCellInput["browser"];
  cell: MatrixCell;
  clockStartMs: number;
  controls: readonly DiscoveredControl[];
  options: { commandControls?: boolean; searchSubmission?: boolean } | undefined;
  seed: string;
  storageState: Awaited<ReturnType<BrowserContext["storageState"]>>;
  targetUrl: string;
  settle: SettleOptions;
}): Promise<DiscoveredControlObservation[]> {
  const observations: DiscoveredControlObservation[] = [];
  for (const control of input.controls) {
    const oracle = deriveDiscoveredControlOracle(control, input.options);
    if (oracle.kind === "coverage-gap" || controlPath(control.path) === undefined) {
      observations.push({ control, oracle });
      continue;
    }
    let observed: { observation: DiscoveredControlOracleObservation; numericObservations: CoupledNumericMeasurement[] } | undefined;
    try {
      const context = await input.browser.newContext({
        locale: input.cell.locale,
        viewport: {
          width: input.cell.viewport.width,
          height: input.cell.viewport.height,
        },
        deviceScaleFactor: input.cell.viewport.deviceScaleFactor,
        timezoneId: resolveTimezone(input.cell),
        storageState: input.storageState,
      });
      try {
        await context.addInitScript({ content: buildDeterminismInitScript(input.clockStartMs, input.seed) });
        const page = await context.newPage();
        await page.goto(input.targetUrl, { waitUntil: "domcontentloaded" });
        const settled = await awaitSettled(page, input.settle);
        if (!settled.timedOut) {
          observed = await observeDiscoveredControl(page, control);
        }
      } finally {
        await context.close();
      }
    } catch {
      observed = undefined;
    }
    observations.push({ control, oracle, ...(observed === undefined ? {} : { observation: observed.observation, numericObservations: observed.numericObservations }) });
  }
  return observations;
}

function harnessOutcome(input: {
  phase: HarnessEvent["phase"];
  cell: MatrixCell;
  code: string;
  message: string;
  retryable: boolean;
}): HarnessEvent {
  return {
    phase: input.phase,
    scope: { id: input.cell.id },
    plannedContext: {
      kind: "browser",
      cell: matrixCellRef(input.cell),
    },
    cause: {
      code: input.code,
      message: input.message,
      retryable: input.retryable,
    },
    artifactRefs: [],
  };
}

export function attachInteractionCaptureListeners(
  page: Page,
  capture: CaptureOptions,
  buckets: { console: ConsoleEntry[]; network: NetworkEntry[] },
): () => void {
  const cleanup: Array<() => void> = [];
  if (capture.console) {
    const consoleListener = (message: ConsoleMessage) => {
      const location = message.location();
      buckets.console.push({
        type: message.type(),
        text: message.text(),
        location: {
          url: location.url,
          lineNumber: location.lineNumber,
          columnNumber: location.columnNumber,
        },
      });
    };
    const pageErrorListener = (error: Error) => {
      buckets.console.push({
        type: "error",
        text: error.message,
      });
    };
    page.on("console", consoleListener);
    page.on("pageerror", pageErrorListener);
    cleanup.push(() => page.off("console", consoleListener));
    cleanup.push(() => page.off("pageerror", pageErrorListener));
  }

  if (capture.network) {
    const responseListener = (response: Response) => {
      const request = response.request();
      buckets.network.push({
        url: request.url(),
        method: request.method(),
        status: response.status(),
        resourceType: request.resourceType(),
      });
    };
    page.on("response", responseListener);
    cleanup.push(() => page.off("response", responseListener));
  }

  return () => {
    for (const detach of cleanup) {
      detach();
    }
  };
}

async function readPageProbe(page: Page): Promise<PageProbe | undefined> {
  return page.evaluate(() => {
    const globalWindow = window as typeof window & {
      __probe?: { now: number; random: number; tz: string };
      __rand?: number;
      __stored?: string | null;
    };

    const probe: PageProbe = {};
    if (globalWindow.__probe !== undefined) {
      probe.now = globalWindow.__probe.now;
      probe.random = globalWindow.__probe.random;
      probe.tz = globalWindow.__probe.tz;
    }
    if (globalWindow.__rand !== undefined) {
      probe.random = globalWindow.__rand;
    }
    if (globalWindow.__stored !== undefined) {
      probe.stored = globalWindow.__stored;
    }

    return Object.keys(probe).length === 0 ? undefined : probe;
  });
}

function buildInteractionEvidence(input: {
  cell: MatrixCell;
  documentDirection?: string;
  settled: CellEvidence["settled"];
  console: ConsoleEntry[];
  network: NetworkEntry[];
  pageProbe?: PageProbe;
  interactionProbe?: InteractionCellEvidence["interactionProbe"];
  journeyEvidence: NonNullable<InteractionCellEvidence["journeyEvidence"]>;
  interactionArtifacts: InteractionCellEvidence["interactionArtifacts"];
  screenshots?: CellEvidence["screenshots"];
}): InteractionCellEvidence {
  const evidence: InteractionCellEvidence = {
    cell: input.cell,
    console: input.console,
    network: input.network,
    performance: { layoutShifts: [] },
    settled: input.settled,
    journeyEvidence: input.journeyEvidence,
    interactionArtifacts: input.interactionArtifacts,
  };
  if (input.documentDirection !== undefined) {
    evidence.documentDirection = input.documentDirection;
  }
  if (input.interactionProbe !== undefined) {
    evidence.interactionProbe = input.interactionProbe;
  }
  if (input.pageProbe !== undefined) {
    evidence.pageProbe = input.pageProbe;
  }
  if (input.screenshots !== undefined) {
    evidence.screenshots = input.screenshots;
  }
  return evidence;
}

function decodePointer(path: string): string[] {
  return path.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
}

type PointerResult = { found: boolean; value?: JsonObservation };

function pointerValue(value: JsonObservation, path: string): PointerResult {
  let current: JsonObservation | undefined = value;
  for (const segment of decodePointer(path)) {
    if (Array.isArray(current)) {
      if (!/^(0|[1-9][0-9]*)$/.test(segment) || !Object.hasOwn(current, segment)) return { found: false };
      current = current[Number(segment)];
      continue;
    }
    if (typeof current !== "object" || current === null || !Object.hasOwn(current, segment)) return { found: false };
    current = current[segment];
  }
  return { found: true, value: current as JsonObservation };
}

function dimensionUnavailable(value: JsonObservation): boolean {
  return typeof value === "object" && value !== null && !Array.isArray(value) && value.unavailable === true;
}

function equalJson(left: JsonObservation | undefined, right: JsonObservation | undefined): boolean {
  return JSON.stringify(left) === JSON.stringify(right);
}

export function derivePromiseChecks(
  before: JourneyBrowserState,
  after: JourneyBrowserState,
  promise: PromiseContract,
): DerivedPromiseCheck[] {
  return promise.expectations.map((expectation) => {
    const beforeValue = pointerValue(before[expectation.dimension], expectation.path);
    const afterDimension = after[expectation.dimension];
    const afterValue = pointerValue(afterDimension, expectation.path);
    const available = !dimensionUnavailable(afterDimension);
    const passed = available && (expectation.operator === "exists"
      ? afterValue.found
      : expectation.operator === "absent"
        ? !afterValue.found
        : afterValue.found && equalJson(afterValue.value, expectation.value));
    return {
      id: expectation.id,
      dimension: expectation.dimension,
      path: expectation.path,
      operator: expectation.operator,
      passed,
      before: beforeValue.value,
      after: afterValue.value,
    };
  });
}

export function aggregateJourneyCoverage(input: {
  cell: { id: string };
  journeyId: string;
  unproven: string[];
}): CoverageEvent {
  const scopeId = `journey:${input.journeyId}:${input.cell.id}`;
  return {
    scope: { id: scopeId, detectorId: "interaction-journey", surfaceId: input.journeyId },
    context: { kind: "browser", cell: { id: input.cell.id } },
    reason: "unproven-precondition",
    witnessRefs: [...new Set(input.unproven)].map((id) => ({ id: `${scopeId}:${id}` })),
  };
}

export async function captureJourneyState(
  page: Page,
  network: NetworkEntry[] = [],
): Promise<JourneyBrowserState> {
  const browserState = await page.evaluate(() => {
    const compare = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0;
    const elementSummary = (element: Element | null) =>
      element === null
        ? null
        : {
            tagName: element.tagName.toLowerCase(),
            id: element.id,
            role: element.getAttribute("role"),
            ariaLabel: element.getAttribute("aria-label"),
            text: element.textContent.trim(),
          };
    const observedElements = Array.from(document.querySelectorAll("[id],[role],[aria-label],button,input,select,textarea,a"))
      .map((element) => {
        const summary = elementSummary(element);
        if (summary === null) return null;
        const style = element instanceof HTMLElement ? getComputedStyle(element) : null;
        const rect = element.getBoundingClientRect();
        const hit = rect.width > 0 && rect.height > 0
          ? document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)
          : null;
        return {
          ...summary,
          attributes: Array.from(element.attributes)
            .map((attribute) => ({ name: attribute.name, value: attribute.value }))
            .sort((left, right) => compare(left.name, right.name))
            .reduce<Record<string, string>>((attributes, attribute) => {
              attributes[attribute.name] = attribute.value;
              return attributes;
            }, {}),
          styles: style === null ? null : { display: style.display, visibility: style.visibility, opacity: style.opacity, pointerEvents: style.pointerEvents },
          hit: elementSummary(hit),
        };
      })
      .filter((element): element is NonNullable<typeof element> => element !== null)
      .sort((left, right) => compare(JSON.stringify(left), JSON.stringify(right)));
    const duplicateId = observedElements
      .filter(({ id }) => id.length > 0)
      .map(({ id }) => id)
      .find((id, index, ids) => ids.indexOf(id) !== index);
    if (duplicateId !== undefined) {
      throw new Error(`duplicate DOM id in journey evidence: ${duplicateId}`);
    }
    const active = document.activeElement;
    const style = active instanceof HTMLElement ? getComputedStyle(active) : null;
    const storage = (store: Storage): Record<string, string | null> => {
      const result: Record<string, string | null> = {};
      for (let index = 0; index < store.length; index += 1) {
        const key = store.key(index);
        if (key !== null) {
          result[key] = store.getItem(key);
        }
      }
      return result;
    };
    const storageState = (() => {
      try {
        return { local: storage(localStorage), session: storage(sessionStorage) };
      } catch {
        return { unavailable: true };
      }
    })();
    const rect = active instanceof HTMLElement ? active.getBoundingClientRect() : null;
    const hit = rect === null ? null : document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
    const accessibleElementsById = Object.fromEntries(
      observedElements
        .filter(({ id }) => id.length > 0)
        .map(({ tagName, id, role, ariaLabel, text, attributes }) => [
          id,
          { tagName, id, role, ariaLabel, text, attributes },
        ]),
    );
    const domElementsById = Object.fromEntries(
      observedElements
        .filter(({ id }) => id.length > 0)
        .map(({ attributes, tagName, id, text }) => [id, { attributes, tagName, id, text }]),
    );
    const styleElementsById = Object.fromEntries(
      observedElements
        .filter(({ id }) => id.length > 0)
        .map(({ styles: elementStyles, id, tagName }) => [id, { id, tagName, styles: elementStyles }]),
    );
    const hitTestElementsById = Object.fromEntries(
      observedElements
        .filter(({ id }) => id.length > 0)
        .map(({ hit: elementHit, id, tagName }) => [id, { id, tagName, hit: elementHit }]),
    );
    return {
      accessible: {
        observation: "accessibility-attribute-and-role-summary",
        document: {
          title: document.title,
          activeElement: elementSummary(active),
          elements: observedElements.map(({ tagName, id, role, ariaLabel, text, attributes }) => ({
            tagName,
            id,
            role,
            ariaLabel,
            text,
            attributes,
          })),
          elementsById: accessibleElementsById,
        },
      },
      dom: {
        document: {
          title: document.title,
          language: document.documentElement.lang,
          elements: observedElements.map(({ attributes, tagName, id, text }) => ({ attributes, tagName, id, text })),
          elementsById: domElementsById,
        },
      },
      styles: {
        activeElement: style === null ? null : { display: style.display, visibility: style.visibility, opacity: style.opacity, pointerEvents: style.pointerEvents },
        elements: observedElements.map(({ styles: elementStyles, id, tagName }) => ({ id, tagName, styles: elementStyles })),
        elementsById: styleElementsById,
      },
      url: {
        href: location.href,
        pathname: location.pathname,
        search: location.search,
        searchParams: Object.fromEntries([...new URLSearchParams(location.search).entries()].sort(([left], [right]) => compare(left, right))),
      },
      storage: storageState,
      focus: { activeElement: elementSummary(active) },
      hitTest: {
        activeElement: elementSummary(hit),
        elements: observedElements.map(({ hit: elementHit, id, tagName }) => ({ id, tagName, hit: elementHit })),
        elementsById: hitTestElementsById,
      },
    };
  });
  const url = new URL(browserState.url.href);
  return structuredClone({
    accessible: browserState.accessible,
    dom: browserState.dom,
    styles: browserState.styles,
    url: {
      ...browserState.url,
      pathname: url.pathname,
      search: url.search,
      searchParams: Object.fromEntries([...url.searchParams.entries()].sort(([left], [right]) => compare(left, right))),
    },
    storage: browserState.storage,
    network,
    focus: browserState.focus,
    hitTest: browserState.hitTest,
  });
}

export type DeclaredJourneyResult = {
  evidence: {
    before: JourneyBrowserState;
    after: JourneyBrowserState;
    promiseChecks: DerivedPromiseCheck[];
  };
  journeyOutcome?: JourneyOutcome;
  coverage?: CoverageEvent;
};

export async function captureDeclaredJourneyEvidence(input: {
  page: Page;
  cell: MatrixCell;
  settled: CellEvidence["settled"];
  console: ConsoleEntry[];
  network: NetworkEntry[];
  journeyEvidence: DeclaredJourneyResult["evidence"];
  capture: CaptureOptions;
  artifactRunDir?: string;
  sourceRunId?: string;
  redactionRules?: RedactionRule[];
}): Promise<InteractionCellEvidence> {
  const [pageProbe, interactionArtifacts, documentDirection] = await Promise.all([
    readPageProbe(input.page),
    captureInteractionArtifacts(input.page, input.cell.id),
    readDocumentDirection(input.page),
  ]);
  const screenshots = await captureCellScreenshots(input.page, {
    cell: input.cell,
    capture: input.capture,
    ...(input.artifactRunDir === undefined ? {} : { artifactRunDir: input.artifactRunDir }),
    ...(input.sourceRunId === undefined ? {} : { sourceRunId: input.sourceRunId }),
    ...(input.redactionRules === undefined ? {} : { redactionRules: input.redactionRules }),
  });
  return buildInteractionEvidence({
    cell: input.cell,
    settled: input.settled,
    documentDirection,
    console: input.console,
    network: input.network,
    journeyEvidence: input.journeyEvidence,
    interactionArtifacts,
    ...(pageProbe === undefined ? {} : { pageProbe }),
    ...(screenshots === undefined ? {} : { screenshots }),
  });
}

function missingJourneyOracles(plan: UiJourneyPlan, oracles: JourneyOracles): string[] {
  return plan.oracleRefs.flatMap((oracle) => {
    const candidates = oracle.kind === "persistence" ? oracles.persistence : oracle.kind === "side-effect" ? oracles.sideEffect : undefined;
    if (candidates?.some(({ ref }) => ref.kind === oracle.kind && ref.id === oracle.id && ref.version === oracle.version)) {
      return [];
    }
    return [`oracle:${oracle.kind}:${oracle.id}${oracle.version === undefined ? "" : `:${oracle.version}`}`];
  });
}

function unavailableJourneyDimensions(state: JourneyBrowserState): string[] {
  return (Object.keys(state) as Array<keyof JourneyBrowserState>).flatMap((dimension) =>
    dimensionUnavailable(state[dimension]) ? [`dimension:${dimension}`] : [],
  );
}

export class DeclaredJourneyRunError extends Error {
  constructor(readonly boundary: "action" | "oracle", cause: unknown) {
    super(cause instanceof Error ? cause.message : String(cause));
    this.name = "DeclaredJourneyRunError";
  }
}

export async function runDeclaredJourney(input: {
  page: Page;
  cell: MatrixCell;
  plan: UiJourneyPlan;
  network: NetworkEntry[];
  oracles: JourneyOracles;
  stores?: KernelStores;
  runId: string;
  unproven?: string[];
}): Promise<DeclaredJourneyResult> {
  const plan = parseUiJourneyPlan(input.plan);
  const before = await captureJourneyState(input.page, input.network);
  const unprovenBefore = [
    ...(input.unproven ?? []),
    ...unavailableJourneyDimensions(before),
    ...missingJourneyOracles(plan, input.oracles),
  ];
  if (unprovenBefore.length > 0) {
    try {
      await runInteractionActions(input.page, { ...input.cell, actions: plan.semanticActions });
    } catch (error) {
      throw new DeclaredJourneyRunError("action", error);
    }
    const after = await captureJourneyState(input.page, input.network);
    const unproven = [...unprovenBefore, ...unavailableJourneyDimensions(after)];
    return {
      evidence: {
        before,
        after,
        promiseChecks: derivePromiseChecks(before, after, plan.promise),
      },
      coverage: aggregateJourneyCoverage({
        cell: input.cell,
        journeyId: plan.journey.id,
        unproven,
      }),
    };
  }
  let evidence: DeclaredJourneyResult["evidence"] | undefined;
  let unprovenAfter: string[] = [];
  let journeyOutcome: Awaited<ReturnType<typeof runUiJourneyPlan>>;
  try {
    journeyOutcome = await runUiJourneyPlan(plan, {
      trigger: async () => {
        try {
          await runInteractionActions(input.page, { ...input.cell, actions: plan.semanticActions });
        } catch (error) {
          throw new DeclaredJourneyRunError("action", error);
        }
        const after = await captureJourneyState(input.page, input.network);
        evidence = {
          before,
          after,
          promiseChecks: derivePromiseChecks(before, after, plan.promise),
        };
        unprovenAfter = unavailableJourneyDimensions(after);
        return { uiSuccess: evidence.promiseChecks.every((check) => check.passed) };
      },
      browserPromiseChecks: () => evidence?.promiseChecks ?? [],
      certificationAllowed: () => unprovenAfter.length === 0,
      oracles: input.oracles,
      ...(input.stores === undefined ? {} : { stores: input.stores }),
      runId: input.runId,
    });
  } catch (error) {
    if (error instanceof DeclaredJourneyRunError) throw error;
    throw new DeclaredJourneyRunError("oracle", error);
  }
  if (evidence === undefined) {
    throw new Error(`declared journey ${plan.journey.id} did not execute actions`);
  }
  if (unprovenAfter.length > 0) {
    return {
      evidence,
      coverage: aggregateJourneyCoverage({
        cell: input.cell,
        journeyId: plan.journey.id,
        unproven: unprovenAfter,
      }),
    };
  }
  return { evidence, journeyOutcome };
}

export async function runInteractionCell(input: RunCellInput & { interactionDiscovery?: { enabled: boolean; routeOptions?: Record<string, { commandControls?: boolean; searchSubmission?: boolean }> } }): Promise<InteractionCellResult> {
  const clockStartMs = Date.parse(input.clockStart);
  const timezoneId = resolveTimezone(input.cell);
  const buckets = { console: [] as ConsoleEntry[], network: [] as NetworkEntry[] };
  let detachCaptureListeners: (() => void) | undefined;

  const context = await input.browser.newContext({
    locale: input.cell.locale,
    viewport: {
      width: input.cell.viewport.width,
      height: input.cell.viewport.height,
    },
    deviceScaleFactor: input.cell.viewport.deviceScaleFactor,
    timezoneId,
  });

  try {
    const authResult = await applyAuthAdapter({
      adapter: input.authAdapter,
      context,
      cell: input.cell,
    });
    if (authResult.kind === "harness") {
      return {
        kind: "harness",
        outcome: harnessOutcome({ phase: "adapter", cell: input.cell, ...authResult }),
      };
    }
    const authProof = authResult.proof;

    await context.addInitScript({
      content: buildDeterminismInitScript(clockStartMs, input.seed),
    });

    const page = await context.newPage();
    detachCaptureListeners = attachInteractionCaptureListeners(page, input.capture, buckets);

    const targetUrl = joinUrl(input.baseUrl, input.cell.url);
    try {
      await page.goto(targetUrl, {
        timeout: input.timeouts.navigateMs,
        waitUntil: "domcontentloaded",
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return {
        kind: "harness",
        outcome: harnessOutcome({
          phase: "setup",
          cell: input.cell,
          code: "navigation-failed",
          message,
          retryable: true,
        }),
      };
    }

    const settleOptions: SettleOptions = {
      timeoutMs: input.timeouts.settleMs,
      ...input.capture.settle,
    };
    const settled = await awaitSettled(page, settleOptions);
    const documentDirection = await readDocumentDirection(page);
    const initialInteractionProbe = await readInteractionProbe(page);
    const discoveryEligible = input.interactionDiscovery?.enabled === true
      && (input.cell.journeys?.length ?? 0) === 0
      && input.cell.actions.length === 0
      && initialInteractionProbe === null;
    const before = await captureJourneyState(page, buckets.network);
    if (!discoveryEligible) {
      await runInteractionActions(page, input.cell);
    }
    const rawInteractionProbe = discoveryEligible ? initialInteractionProbe : await readInteractionProbe(page);
    const after = await captureJourneyState(page, buckets.network);
    const interactionProbe = rawInteractionProbe === null || rawInteractionProbe === undefined
      ? undefined
      : parseInteractionProbe(rawInteractionProbe);
    if (interactionProbe !== undefined && buckets.network.length > 0) {
      for (const check of interactionProbe.promiseChecks) {
        if (!check.dimensions.network.matches && check.dimensions.network.observed === null) {
          check.dimensions.network.observed = buckets.network;
        }
      }
    }

    const [pageProbe, interactionArtifacts] = await Promise.all([
      readPageProbe(page),
      captureInteractionArtifacts(page, input.cell.id),
    ]);

    const screenshots = await captureCellScreenshots(page, input);
    const evidence = buildInteractionEvidence({
      cell: input.cell,
      documentDirection,
      settled,
      console: buckets.console,
      network: buckets.network,
      interactionProbe,
      journeyEvidence: { before, after, promiseChecks: [] },
      interactionArtifacts,
      ...(pageProbe === undefined ? {} : { pageProbe }),
      ...(screenshots === undefined ? {} : { screenshots }),
    });

    if (settled.timedOut) {
      return {
        kind: "harness",
        outcome: harnessOutcome({
          phase: "infrastructure",
          cell: input.cell,
          code: "settle-timeout",
          message: "Page did not reach settled state before timeout",
          retryable: false,
        }),
        partialEvidence: evidence,
      };
    }

    if (discoveryEligible) {
      const discovery = await discoverInteractionControls(page, input.redactionRules);
      return {
        kind: "discovery",
        discovery,
        observations: await observeDiscoveredControls({
          browser: input.browser,
          cell: input.cell,
          clockStartMs,
          controls: discovery.controls,
          options: input.interactionDiscovery?.routeOptions?.[input.cell.routeId],
          seed: input.seed,
          storageState: await context.storageState(),
          targetUrl,
          settle: settleOptions,
        }),
        evidence,
        ...(authProof === undefined ? {} : { authProof }),
      };
    }

    return {
      kind: "evidence",
      evidence,
      ...(authProof === undefined ? {} : { authProof }),
    };
  } finally {
    detachCaptureListeners?.();
    await context.close();
  }
}
