import type { CellEvidence } from "../../../playwright/src/cell-runner.js";
import type { CoverageOutcome } from "../../../schema/src/records/coverage.js";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { ContinuityScenario } from "../../../universal/fixtures/continuity/types.js";

export const INTERACTION_DETECTOR_VERSION = "1.0.0";

export const INTERACTION_RULE_IDS = [
  "UI-033",
  "UI-035",
  "UI-036",
  "UI-037",
  "UI-038",
] as const;

export type InteractionRuleId = (typeof INTERACTION_RULE_IDS)[number];

export type InteractionFixtureVariant =
  | "positive"
  | "negative"
  | "intentional-exception"
  | "rtl"
  | "changed-fingerprint";

export type InteractionDimensionState = {
  promised: unknown;
  observed: unknown;
  matches: boolean;
};

export type InteractionPromiseCheck = {
  scenarioId: string;
  locator: string;
  promisedOutcome: string;
  visualStateChanged: boolean;
  dimensions: {
    accessible: InteractionDimensionState;
    dom: InteractionDimensionState;
    styles: InteractionDimensionState;
    url: InteractionDimensionState;
    storage: InteractionDimensionState;
    network: InteractionDimensionState;
    sideEffects: InteractionDimensionState;
  };
};

export type ItemIdentityObservation = {
  itemId: string;
  localState: unknown;
};

export type ItemIdentityScenario = {
  scenarioId: string;
  kind: "item-identity";
  operation:
    | "reorder"
    | "pagination"
    | "virtualization"
    | "insertion"
    | "deletion"
    | "optimistic-reconciliation";
  items: ItemIdentityObservation[];
  observedAfterOperation: ItemIdentityObservation[];
};

export type HiddenInertScenario = {
  scenarioId: string;
  kind: "hidden-inert-action";
  locator: string;
  inertReason: "hidden" | "inert" | "aria-hidden" | "disabled";
  focusable: boolean;
  hitTestActivates: boolean;
  announcedActionable: boolean;
  keyboardActivates: boolean;
  sideEffectEmitted: boolean;
};

export type InteractionScenario =
  | ContinuityScenario
  | ItemIdentityScenario
  | HiddenInertScenario;

export type InteractionProbe = {
  continuityVersion: string;
  scenarios: InteractionScenario[];
  promiseChecks: InteractionPromiseCheck[];
};

export type InteractionArtifactRef = {
  relativePath: string;
  contentHash: string;
  mediaType: string;
  dimensions?: { width: number; height: number };
};

export type InteractionCellEvidence = CellEvidence & {
  interactionProbe?: InteractionProbe;
  journeyEvidence?: {
    before: JourneyBrowserState;
    after: JourneyBrowserState;
    promiseChecks: DerivedPromiseCheck[];
  };
  interactionArtifacts: {
    fullScreenshot: InteractionArtifactRef;
  };
};

export type JsonObservation = null | boolean | number | string | JsonObservation[] | {
  [key: string]: JsonObservation;
};

export type JourneyBrowserState = {
  accessible: JsonObservation;
  dom: JsonObservation;
  styles: JsonObservation;
  url: JsonObservation;
  storage: JsonObservation;
  network: JsonObservation;
  focus: JsonObservation;
  hitTest: JsonObservation;
};

export type JourneyPromiseDimension = keyof JourneyBrowserState;

export type DerivedPromiseCheck = {
  id: string;
  dimension: JourneyPromiseDimension;
  path: string;
  operator: "exists" | "absent" | "equals";
  passed: boolean;
  before: JsonObservation | undefined;
  after: JsonObservation | undefined;
};

export type DiscoverySource = "native" | "accessibility" | "shadow" | "frame" | "tab";

export type DiscoveredControl = {
  kind: string;
  role: string;
  stateSchema: string[];
  nameClass: string;
  accessibleName?: string;
  landmark: string;
  subtreeFingerprint: string;
  disabled: boolean;
  current: boolean;
  expanded: boolean;
  sameOriginLink: boolean;
  validates: boolean;
  ariaPressed?: string;
  path: string;
  sources: DiscoverySource[];
  boundaries: Array<"representative" | "first" | "last" | "current" | "expanded" | "disabled">;
};

export type InteractionDiscovery = {
  controls: DiscoveredControl[];
  digest: string;
};

export type ActivationInteractionCoverageKind = "unknown-command" | "unsupported-activation";

export type InteractionCoverageKind = ActivationInteractionCoverageKind;

export type PreconditionInteractionCoverageKind = "unproven-precondition";

export type InteractionCoveragePhase = "activation" | "precondition";

export type InteractionCoverageWitnessRef = {
  id: string;
};

export type InteractionCoverageObligation = {
  id: string;
  witnessRefs: [InteractionCoverageWitnessRef, ...InteractionCoverageWitnessRef[]];
};

export type MissingExpectedPreconditionEvidence = {
  unproven: "missing-expected";
  observed: JsonObservation;
  locator: string;
  setup?: JsonObservation;
  witness?: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingObservedPreconditionEvidence = {
  unproven: "missing-observed";
  expected: JsonObservation;
  locator: string;
  observed?: JsonObservation;
  setup?: JsonObservation;
  witness?: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingSetupPreconditionEvidence = {
  unproven: "missing-setup";
  expected: JsonObservation;
  locator: string;
  observed?: JsonObservation;
  setup?: JsonObservation;
  witness?: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingLocatorPreconditionEvidence = {
  unproven: "missing-locator";
  expected: JsonObservation;
  observed: JsonObservation;
  setup: JsonObservation;
  witness: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingWitnessPreconditionEvidence = {
  unproven: "missing-witness";
  expected: JsonObservation;
  locator: string;
  observed?: JsonObservation;
  setup?: JsonObservation;
  artifact?: InteractionArtifactRef;
};

export type MissingArtifactPreconditionEvidence = {
  unproven: "missing-artifact";
  expected: JsonObservation;
  locator: string;
  observed: JsonObservation;
  setup: JsonObservation;
  witness: InteractionCoverageWitnessRef;
};

export type MissingExpectedAndObservedPreconditionEvidence = {
  unproven: "missing-expected-and-observed";
  locator: string;
  setup?: JsonObservation;
  witness?: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingSetupAndLocatorPreconditionEvidence = {
  unproven: "missing-setup-and-locator";
  expected: JsonObservation;
  witness: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type MissingLocatorAndWitnessPreconditionEvidence = {
  unproven: "missing-locator-and-witness";
  expected: JsonObservation;
  observed: JsonObservation;
  setup: JsonObservation;
  artifact?: InteractionArtifactRef;
};

export type MissingWitnessAndArtifactPreconditionEvidence = {
  unproven: "missing-witness-and-artifact";
  expected: JsonObservation;
  locator: string;
  observed: JsonObservation;
  setup: JsonObservation;
};

export type UnrecognizedPreconditionGapEvidence = {
  unproven: "unrecognized-precondition-gap";
  failedPrecondition: string;
  observed: JsonObservation;
  locator?: string;
  setup?: JsonObservation;
  witness?: InteractionCoverageWitnessRef;
  artifact?: InteractionArtifactRef;
};

export type PreconditionObligationEvidence =
  | MissingExpectedPreconditionEvidence
  | MissingObservedPreconditionEvidence
  | MissingSetupPreconditionEvidence
  | MissingLocatorPreconditionEvidence
  | MissingWitnessPreconditionEvidence
  | MissingArtifactPreconditionEvidence
  | MissingExpectedAndObservedPreconditionEvidence
  | MissingSetupAndLocatorPreconditionEvidence
  | MissingLocatorAndWitnessPreconditionEvidence
  | MissingWitnessAndArtifactPreconditionEvidence
  | UnrecognizedPreconditionGapEvidence;

const RECOGNIZED_PRECONDITION_UNPROVEN_DISCRIMINANTS = new Set<PreconditionObligationEvidence["unproven"]>([
  "missing-expected",
  "missing-observed",
  "missing-setup",
  "missing-locator",
  "missing-witness",
  "missing-artifact",
  "missing-expected-and-observed",
  "missing-setup-and-locator",
  "missing-locator-and-witness",
  "missing-witness-and-artifact",
  "unrecognized-precondition-gap",
]);

export type PreconditionCoverageObligation = InteractionCoverageObligation & {
  unproven: PreconditionObligationEvidence;
};

export type ActivationInteractionCoverageEvidence = {
  kind: ActivationInteractionCoverageKind;
  phase: "activation";
  obligations: [InteractionCoverageObligation, ...InteractionCoverageObligation[]];
};

export type PreconditionInteractionCoverageEvidence = {
  kind: "unproven-precondition";
  phase: "precondition";
  obligations: [PreconditionCoverageObligation, ...PreconditionCoverageObligation[]];
};

export type InteractionCoverageEvidence =
  | ActivationInteractionCoverageEvidence
  | PreconditionInteractionCoverageEvidence;

export type DiscoveredControlCoverageOutcome = {
  status: "coverage-incomplete";
  reason: ActivationInteractionCoverageKind;
  phase: "activation";
  evidence: ActivationInteractionCoverageEvidence;
  witnessRefs: [InteractionCoverageWitnessRef, ...InteractionCoverageWitnessRef[]];
};

type SchemaInteractionCoverage = NonNullable<CoverageOutcome["interaction"]>;

export type ActivationInteractionCoverageSerialization = Extract<
  SchemaInteractionCoverage,
  { phase: "activation" }
>;

export type PreconditionInteractionCoverageSerialization = Extract<
  SchemaInteractionCoverage,
  { phase: "precondition" }
>;

export type InteractionCoverageSerialization = SchemaInteractionCoverage;

export type BareInteractionCoverageRow = {
  gapKind: string;
  control: { accessibleName: string; locator: string };
};

function assertNonEmptyObligations(
  obligations: InteractionCoverageObligation[] | PreconditionCoverageObligation[],
): void {
  if (obligations.length === 0) {
    throw new Error("interaction coverage evidence requires a non-empty obligation list");
  }
}

export const preconditionEvidence = {
  missingExpected(
    input: Omit<MissingExpectedPreconditionEvidence, "unproven">,
  ): MissingExpectedPreconditionEvidence {
    return { unproven: "missing-expected", ...input };
  },
  missingObserved(
    input: Omit<MissingObservedPreconditionEvidence, "unproven">,
  ): MissingObservedPreconditionEvidence {
    return { unproven: "missing-observed", ...input };
  },
  missingSetup(
    input: Omit<MissingSetupPreconditionEvidence, "unproven">,
  ): MissingSetupPreconditionEvidence {
    return { unproven: "missing-setup", ...input };
  },
  missingLocator(
    input: Omit<MissingLocatorPreconditionEvidence, "unproven">,
  ): MissingLocatorPreconditionEvidence {
    return { unproven: "missing-locator", ...input };
  },
  missingWitness(
    input: Omit<MissingWitnessPreconditionEvidence, "unproven">,
  ): MissingWitnessPreconditionEvidence {
    return { unproven: "missing-witness", ...input };
  },
  missingArtifact(
    input: Omit<MissingArtifactPreconditionEvidence, "unproven">,
  ): MissingArtifactPreconditionEvidence {
    return { unproven: "missing-artifact", ...input };
  },
  missingExpectedAndObserved(
    input: Omit<MissingExpectedAndObservedPreconditionEvidence, "unproven">,
  ): MissingExpectedAndObservedPreconditionEvidence {
    return { unproven: "missing-expected-and-observed", ...input };
  },
  missingSetupAndLocator(
    input: Omit<MissingSetupAndLocatorPreconditionEvidence, "unproven">,
  ): MissingSetupAndLocatorPreconditionEvidence {
    return { unproven: "missing-setup-and-locator", ...input };
  },
  missingLocatorAndWitness(
    input: Omit<MissingLocatorAndWitnessPreconditionEvidence, "unproven">,
  ): MissingLocatorAndWitnessPreconditionEvidence {
    return { unproven: "missing-locator-and-witness", ...input };
  },
  missingWitnessAndArtifact(
    input: Omit<MissingWitnessAndArtifactPreconditionEvidence, "unproven">,
  ): MissingWitnessAndArtifactPreconditionEvidence {
    return { unproven: "missing-witness-and-artifact", ...input };
  },
  unrecognizedGap(
    input: Omit<UnrecognizedPreconditionGapEvidence, "unproven">,
  ): UnrecognizedPreconditionGapEvidence {
    return { unproven: "unrecognized-precondition-gap", ...input };
  },
} as const;

export function buildPreconditionCoverageObligation(input: {
  id: string;
  witnessRefs: [InteractionCoverageWitnessRef, ...InteractionCoverageWitnessRef[]];
  unproven: PreconditionObligationEvidence;
}): PreconditionCoverageObligation {
  return {
    id: input.id,
    witnessRefs: input.witnessRefs,
    unproven: input.unproven,
  };
}

export function buildPreconditionInteractionCoverage(input: {
  obligations: [PreconditionCoverageObligation, ...PreconditionCoverageObligation[]];
  control: { accessibleName: string; locator: string };
}): PreconditionInteractionCoverageSerialization {
  assertNonEmptyObligations(input.obligations);
  return {
    gapKind: "unproven-precondition",
    phase: "precondition",
    evidence: {
      kind: "unproven-precondition",
      phase: "precondition",
      obligations: input.obligations,
    },
    control: input.control,
  };
}

export function assertInteractionCoverageSerialization(
  interaction: unknown,
): asserts interaction is InteractionCoverageSerialization {
  if (typeof interaction !== "object" || interaction === null) {
    throw new Error("interaction coverage row must be an object");
  }
  const row = interaction as Record<string, unknown>;
  const control = row.control as { accessibleName?: unknown; locator?: unknown } | undefined;
  if (
    control === undefined
    || typeof control.accessibleName !== "string"
    || typeof control.locator !== "string"
  ) {
    throw new Error("interaction coverage row is missing control metadata");
  }
  if (!("phase" in row)) {
    throw new Error("interaction coverage row is missing phase");
  }
  if (!("evidence" in row)) {
    throw new Error("interaction coverage row is missing evidence");
  }
  if (!("gapKind" in row)) {
    throw new Error("interaction coverage row is missing gapKind");
  }
  if (row.phase === undefined || row.evidence === undefined) {
    throw new Error("interaction coverage row is bare or incomplete");
  }
  const evidence = row.evidence as { kind?: unknown; phase?: unknown; obligations?: unknown[] };
  if (typeof evidence.kind !== "string" || typeof evidence.phase !== "string") {
    throw new Error("interaction coverage evidence is missing kind or phase");
  }
  if (evidence.kind !== row.gapKind || evidence.phase !== row.phase) {
    throw new Error("interaction coverage reason, kind, and phase mismatch");
  }
  if (!Array.isArray(evidence.obligations) || evidence.obligations.length === 0) {
    throw new Error("interaction coverage evidence requires non-empty obligations");
  }
  for (const obligation of evidence.obligations) {
    if (
      typeof obligation !== "object"
      || obligation === null
      || typeof (obligation as { id?: unknown }).id !== "string"
      || !Array.isArray((obligation as { witnessRefs?: unknown }).witnessRefs)
      || ((obligation as { witnessRefs: unknown[] }).witnessRefs.length === 0)
    ) {
      throw new Error("interaction coverage obligation is missing id or witness refs");
    }
    if (row.phase === "precondition") {
      const unprovenEvidence = (obligation as { unproven?: unknown }).unproven;
      if (typeof unprovenEvidence !== "object" || unprovenEvidence === null) {
        throw new Error("precondition obligation is missing unproven evidence");
      }
      const discriminant = (unprovenEvidence as { unproven?: unknown }).unproven;
      if (
        typeof discriminant !== "string"
        || !RECOGNIZED_PRECONDITION_UNPROVEN_DISCRIMINANTS.has(
          discriminant as PreconditionObligationEvidence["unproven"],
        )
      ) {
        throw new Error("precondition obligation has invalid unproven discriminant");
      }
    }
  }
  if (row.phase === "precondition" && row.gapKind !== "unproven-precondition") {
    throw new Error("precondition interaction coverage requires unproven-precondition gapKind");
  }
}

export type CoupledNumericObservation = {
  controlIdentity: string;
  displayIdentity: string;
  before: number;
  baselineAfter: number;
  afterFirst: number;
  afterSecond: number;
};

export type CoupledNumericMissingPhase = "baselineAfter" | "afterFirst" | "afterSecond";

export type LostCoupledNumericMeasurement = {
  kind: "missing";
  controlIdentity: string;
  displayIdentity: string;
  missingPhase: CoupledNumericMissingPhase;
};

export type CoupledNumericMeasurement =
  | { kind: "measured"; observation: CoupledNumericObservation }
  | LostCoupledNumericMeasurement;

export type InteractionFixture = {
  fixtureId: string;
  ruleId: InteractionRuleId;
  variant: InteractionFixtureVariant;
  route: string;
  html: string;
  cell?: Partial<MatrixCell>;
  intentionalContractId?: string;
};

export type InteractionViolationFact = {
  ruleId: InteractionRuleId;
  uniRule?: "UNI-090" | "UNI-091" | "UNI-092" | "UNI-093";
  kind: string;
  locator: string;
  measurement: Record<string, unknown>;
  summary: string;
};

export type Ui001EvidenceRecord = {
  ruleId: InteractionRuleId;
  route: string;
  url: string;
  role: string;
  locale: string;
  viewport: MatrixCell["viewport"];
  state: MatrixCell["state"];
  actions: MatrixCell["actions"];
  timestampPhase: string;
  targetLocator: string;
  measuredEvidence: Record<string, unknown>;
  screenshotRefs: string[];
  detectorVersion: string;
  classificationBasis: string;
  violation: InteractionViolationFact;
};
