import { preconditionObligationEvidenceSchema } from "../../../schema/src/records/coverage.js";
import type {
  ActivationInteractionCoverageEvidence,
  ActivationInteractionCoverageKind,
  InteractionArtifactRef,
  InteractionCoverageObligation,
  InteractionCoverageSerialization,
  InteractionCoverageWitnessRef,
  JsonObservation,
  PreconditionCoverageObligation,
  PreconditionInteractionCoverageEvidence,
  PreconditionObligationEvidence,
} from "../../../schema/src/records/coverage.js";
import type { CellEvidence } from "../../../playwright/src/cell-runner.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 {
  ActivationInteractionCoverageEvidence,
  ActivationInteractionCoverageKind,
  InteractionArtifactRef,
  InteractionCoverageObligation,
  InteractionCoverageSerialization,
  InteractionCoverageWitnessRef,
  JsonObservation,
  PreconditionCoverageObligation,
  PreconditionInteractionCoverageEvidence,
  PreconditionObligationEvidence,
};

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

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 InteractionCoverageKind = ActivationInteractionCoverageKind;

export type PreconditionInteractionCoverageKind = "unproven-precondition";

const RECOGNIZED_PRECONDITION_UNPROVEN_DISCRIMINANTS = new Set<
  PreconditionObligationEvidence["unproven"]
>(preconditionObligationEvidenceSchema.options.map((option) => option.shape.unproven.value));

export type InteractionCoveragePhase = InteractionCoverageSerialization["phase"];

type PreconditionEvidenceOf<K extends PreconditionObligationEvidence["unproven"]> =
  Extract<PreconditionObligationEvidence, { unproven: K }>;

export type MissingExpectedPreconditionEvidence = PreconditionEvidenceOf<"missing-expected">;
export type MissingObservedPreconditionEvidence = PreconditionEvidenceOf<"missing-observed">;
export type MissingSetupPreconditionEvidence = PreconditionEvidenceOf<"missing-setup">;
export type MissingLocatorPreconditionEvidence = PreconditionEvidenceOf<"missing-locator">;
export type MissingWitnessPreconditionEvidence = PreconditionEvidenceOf<"missing-witness">;
export type MissingArtifactPreconditionEvidence = PreconditionEvidenceOf<"missing-artifact">;
export type MissingExpectedAndObservedPreconditionEvidence =
  PreconditionEvidenceOf<"missing-expected-and-observed">;
export type MissingSetupAndLocatorPreconditionEvidence =
  PreconditionEvidenceOf<"missing-setup-and-locator">;
export type MissingLocatorAndWitnessPreconditionEvidence =
  PreconditionEvidenceOf<"missing-locator-and-witness">;
export type MissingWitnessAndArtifactPreconditionEvidence =
  PreconditionEvidenceOf<"missing-witness-and-artifact">;
export type UnrecognizedPreconditionGapEvidence =
  PreconditionEvidenceOf<"unrecognized-precondition-gap">;

export type InteractionCoverageEvidence =
  | ActivationInteractionCoverageEvidence
  | PreconditionInteractionCoverageEvidence;

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

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

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

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,
    },
    ...(input.control === undefined ? {} : { 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>;
  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;
};
