import type {
  CaptureResult,
  RegexExecutionResult,
  RegexReplacementResult,
} from "../model/match";
import type { SourceRange } from "../model/syntax";
import type { RegexTestExpectation } from "../tests/test-case.types";

export type PredicateEvaluationKind =
  "failure" | "passes" | "inconclusive" | "unsupported";

export interface PredicateEvaluation {
  readonly kind: PredicateEvaluationKind;
  readonly fingerprint: readonly string[];
  readonly summary: string;
}

export interface CaptureRangeExpectation {
  readonly matchIndex: number;
  readonly groupNumber?: number;
  readonly groupName?: string;
  readonly expectedRange: SourceRange;
}

export interface SemanticSideResult {
  readonly execution: RegexExecutionResult;
  readonly replacement?: RegexReplacementResult;
}

export interface SemanticComparisonEvaluation {
  readonly kind: "mismatch" | "same" | "inconclusive" | "unsupported";
  readonly mismatchKinds: readonly string[];
  readonly summary: string;
}

function rangesEqual(
  left: SourceRange | undefined,
  right: SourceRange | undefined,
): boolean {
  return (
    left?.startUtf16 === right?.startUtf16 && left?.endUtf16 === right?.endUtf16
  );
}

function sortedFingerprint(values: Iterable<string>): readonly string[] {
  return [...new Set(values)].sort();
}

function selectedCapture(
  execution: RegexExecutionResult,
  expectation: {
    readonly matchIndex: number;
    readonly groupNumber?: number;
    readonly groupName?: string;
  },
): CaptureResult | undefined {
  const match = execution.matches[expectation.matchIndex];
  const named = expectation.groupName
    ? match?.captures.filter(
        (capture) => capture.groupName === expectation.groupName,
      )
    : undefined;
  return (
    named?.find((capture) => capture.status !== "did-not-participate") ??
    named?.[0] ??
    match?.captures.find(
      (capture) => capture.groupNumber === expectation.groupNumber,
    )
  );
}

function captureValue(
  capture: CaptureResult,
  subject: string,
): string | undefined {
  return capture.range
    ? subject.slice(capture.range.startUtf16, capture.range.endUtf16)
    : capture.value;
}

function completeResult(
  execution: RegexExecutionResult,
  replacement: RegexReplacementResult | undefined,
): PredicateEvaluation | undefined {
  if (!execution.accepted) {
    return {
      kind: "unsupported",
      fingerprint: ["compile-rejected"],
      summary:
        "The selected engine rejected the fixed pattern; a subject reducer cannot repair or preserve that compile failure.",
    };
  }
  if (
    execution.truncated ||
    execution.matches.some(
      (match) =>
        match.valueStatus === "truncated" ||
        match.captures.some(
          (capture) =>
            capture.status === "truncated" || capture.status === "unavailable",
        ),
    ) ||
    replacement?.truncated ||
    replacement?.outputTruncated
  ) {
    return {
      kind: "inconclusive",
      fingerprint: ["truncated-result"],
      summary:
        "The engine result was truncated or unavailable, so it cannot establish the exact failure predicate.",
    };
  }
  return undefined;
}

/**
 * Evaluates the inverse of a saved unit-test assertion. The returned
 * fingerprint records the exact class of assertion failure so a reducer
 * cannot silently turn, for example, a wrong capture value into a missing
 * capture.
 */
export function evaluateUnitTestFailure(
  expectation: RegexTestExpectation,
  execution: RegexExecutionResult,
  replacement: RegexReplacementResult | undefined,
  subject: string,
): PredicateEvaluation {
  const incomplete = completeResult(execution, replacement);
  if (incomplete) return incomplete;

  const matches = execution.matches;
  switch (expectation.kind) {
    case "should-match":
      return matches.length === 0
        ? {
            kind: "failure",
            fingerprint: ["expected-match:absent"],
            summary: "The expected match is still absent.",
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The subject now matches.",
          };
    case "should-not-match":
      return matches.length > 0
        ? {
            kind: "failure",
            fingerprint: ["expected-no-match:present"],
            summary: "At least one forbidden match is still present.",
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The subject no longer matches.",
          };
    case "match-count":
      return matches.length !== expectation.count
        ? {
            kind: "failure",
            fingerprint: ["match-count:mismatch"],
            summary: `The match count is ${matches.length}, not ${expectation.count}.`,
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The expected match count is now produced.",
          };
    case "full-match": {
      const match = matches[expectation.matchIndex];
      if (!match) {
        return {
          kind: "failure",
          fingerprint: ["full-match:missing"],
          summary: `Match ${expectation.matchIndex} is still missing.`,
        };
      }
      const actual = subject.slice(
        match.range.startUtf16,
        match.range.endUtf16,
      );
      return actual !== expectation.value
        ? {
            kind: "failure",
            fingerprint: ["full-match:value-mismatch"],
            summary: "The selected full-match value is still wrong.",
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary:
              "The selected full-match value now equals the expectation.",
          };
    }
    case "capture": {
      const capture = selectedCapture(execution, expectation);
      if (!capture) {
        return {
          kind: "failure",
          fingerprint: ["capture:missing"],
          summary: "The selected capture is still missing.",
        };
      }
      const failures = new Set<string>();
      if (capture.status !== expectation.status) {
        failures.add("capture:status-mismatch");
      }
      if (
        expectation.value !== undefined &&
        captureValue(capture, subject) !== expectation.value
      ) {
        failures.add("capture:value-mismatch");
      }
      return failures.size > 0
        ? {
            kind: "failure",
            fingerprint: sortedFingerprint(failures),
            summary: `The selected capture still fails: ${[...failures]
              .map((value) => value.replace("capture:", ""))
              .join(", ")}.`,
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The selected capture now satisfies the expectation.",
          };
    }
    case "replacement":
      return replacement?.output !== expectation.expected
        ? {
            kind: "failure",
            fingerprint: ["replacement:value-mismatch"],
            summary: "The replacement output is still different.",
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The replacement output now equals the expectation.",
          };
    case "must-complete-within":
      return execution.elapsedMs > expectation.milliseconds
        ? {
            kind: "failure",
            fingerprint: ["duration:above-limit"],
            summary: `The engine still exceeds the ${expectation.milliseconds} ms assertion.`,
          }
        : {
            kind: "passes",
            fingerprint: [],
            summary: "The engine now completes within the asserted duration.",
          };
    case "must-time-out":
      return {
        kind: "failure",
        fingerprint: ["timeout:unexpected-completion"],
        summary:
          "The engine still completes, which fails the must-time-out assertion.",
      };
  }
}

export function evaluateCaptureRangeMismatch(
  expectation: CaptureRangeExpectation,
  execution: RegexExecutionResult,
  subject: string,
): PredicateEvaluation {
  const incomplete = completeResult(execution, undefined);
  if (incomplete) return incomplete;
  const capture = selectedCapture(execution, expectation);
  if (!capture) {
    return {
      kind: "unsupported",
      fingerprint: ["capture:missing"],
      summary:
        "The selected capture is absent; that is not a reproducible capture-range mismatch.",
    };
  }
  if (!capture.range) {
    return {
      kind: "unsupported",
      fingerprint: ["capture:range-unavailable"],
      summary: "The selected capture has no complete UTF-16 range to compare.",
    };
  }
  // Read the slice as well as the range so malformed engine ranges cannot be
  // accepted merely because their numeric coordinates differ.
  void subject.slice(capture.range.startUtf16, capture.range.endUtf16);
  return rangesEqual(capture.range, expectation.expectedRange)
    ? {
        kind: "passes",
        fingerprint: [],
        summary: "The selected capture range now equals the expected range.",
      }
    : {
        kind: "failure",
        fingerprint: ["capture:range-mismatch"],
        summary: `The selected capture is still at ${capture.range.startUtf16}–${capture.range.endUtf16}, not ${expectation.expectedRange.startUtf16}–${expectation.expectedRange.endUtf16}.`,
      };
}

function collectCaptureDifferences(
  left: readonly CaptureResult[],
  right: readonly CaptureResult[],
  kinds: Set<string>,
): void {
  if (left.length !== right.length) kinds.add("capture-presence");
  const length = Math.min(left.length, right.length);
  for (let index = 0; index < length; index += 1) {
    const leftCapture = left[index];
    const rightCapture = right[index];
    if (!leftCapture || !rightCapture) continue;
    if (
      leftCapture.groupNumber !== rightCapture.groupNumber ||
      leftCapture.groupName !== rightCapture.groupName
    ) {
      kinds.add("capture-identity");
    }
    if (leftCapture.status !== rightCapture.status) {
      kinds.add("capture-status");
    }
    if (!rangesEqual(leftCapture.range, rightCapture.range)) {
      kinds.add("capture-range");
    }
    if (leftCapture.value !== rightCapture.value) {
      kinds.add("capture-value");
    }
  }
}

/**
 * Compares only normalized semantic output: ordered UTF-16 ranges, retained
 * values, capture identity/participation/ranges/values, and replacement text.
 * Engine metadata, native offsets, timing, and effective implementation flags
 * are deliberately excluded.
 */
export function compareSemanticResults(
  left: SemanticSideResult,
  right: SemanticSideResult,
): SemanticComparisonEvaluation {
  const leftIncomplete = completeResult(left.execution, left.replacement);
  const rightIncomplete = completeResult(right.execution, right.replacement);
  if (leftIncomplete?.kind === "unsupported") {
    return {
      kind: "unsupported",
      mismatchKinds: [],
      summary: `Left side: ${leftIncomplete.summary}`,
    };
  }
  if (rightIncomplete?.kind === "unsupported") {
    return {
      kind: "unsupported",
      mismatchKinds: [],
      summary: `Right side: ${rightIncomplete.summary}`,
    };
  }
  if (leftIncomplete || rightIncomplete) {
    return {
      kind: "inconclusive",
      mismatchKinds: [],
      summary:
        "At least one side returned incomplete data, so semantic equality cannot be decided.",
    };
  }

  const kinds = new Set<string>();
  const leftMatches = left.execution.matches;
  const rightMatches = right.execution.matches;
  if (leftMatches.length !== rightMatches.length) kinds.add("match-count");
  const matchCount = Math.min(leftMatches.length, rightMatches.length);
  for (let index = 0; index < matchCount; index += 1) {
    const leftMatch = leftMatches[index];
    const rightMatch = rightMatches[index];
    if (!leftMatch || !rightMatch) continue;
    if (!rangesEqual(leftMatch.range, rightMatch.range)) {
      kinds.add("match-range");
    }
    if (leftMatch.value !== rightMatch.value) kinds.add("match-value");
    collectCaptureDifferences(leftMatch.captures, rightMatch.captures, kinds);
  }
  if (left.replacement !== undefined || right.replacement !== undefined) {
    if (left.replacement?.output !== right.replacement?.output) {
      kinds.add("replacement-output");
    }
  }
  const mismatchKinds = [...kinds].sort();
  return mismatchKinds.length > 0
    ? {
        kind: "mismatch",
        mismatchKinds,
        summary: `Semantic mismatch: ${mismatchKinds.join(", ")}.`,
      }
    : {
        kind: "same",
        mismatchKinds: [],
        summary: "Both engines produced the same normalized semantic result.",
      };
}

export function engineIdentity(execution: RegexExecutionResult): string {
  return [
    execution.engine.flavour,
    execution.engine.engineName,
    execution.engine.engineVersion,
    execution.engine.offsetUnit,
  ].join(" · ");
}
