import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementRequest,
  RegexReplacementResult,
} from "../model/match";
import type { CaptureDefinition } from "../model/syntax";
import type {
  RegexTestCase,
  RegexTestExpectation,
  RegexTestResult,
} from "./test-case.types";
import { MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES } from "./test-case.types";
import { utf8ByteLength } from "../execution/request-limits";

const MAXIMUM_VALUE_PREVIEW_UTF16 = 256;

export interface TestExecution {
  execute(
    request: RegexExecutionRequest,
    timeoutMs: number,
  ): Promise<RegexExecutionResult>;
  replace(
    request: RegexReplacementRequest,
    timeoutMs: number,
  ): Promise<RegexReplacementResult>;
}

function valuePreview(value: string | undefined): string {
  if (value === undefined) return "undefined";
  const visible = value.slice(0, MAXIMUM_VALUE_PREVIEW_UTF16);
  const encoded = JSON.stringify(visible);
  return value.length > MAXIMUM_VALUE_PREVIEW_UTF16
    ? `${encoded}… (${value.length.toLocaleString()} UTF-16 units total; diagnostic preview)`
    : encoded;
}

export function boundedTestResultMessage(
  message: string,
  maximumBytes = MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
): string {
  if (maximumBytes <= 0) return "";
  if (
    message.length <= maximumBytes &&
    utf8ByteLength(message) <= maximumBytes
  ) {
    return message;
  }

  const suffix = `… (${message.length.toLocaleString()} UTF-16 units total; diagnostic truncated)`;
  const suffixBytes = utf8ByteLength(suffix);
  if (suffixBytes >= maximumBytes) return suffix.slice(0, maximumBytes);

  const prefixBudget = maximumBytes - suffixBytes;
  let low = 0;
  let high = Math.min(message.length, prefixBudget);
  while (low < high) {
    const middle = Math.ceil((low + high) / 2);
    if (utf8ByteLength(message.slice(0, middle)) <= prefixBudget) {
      low = middle;
    } else {
      high = middle - 1;
    }
  }
  return `${message.slice(0, low)}${suffix}`;
}

function evaluate(
  expectation: RegexTestExpectation,
  execution: RegexExecutionResult,
  replacement: RegexReplacementResult | undefined,
  subject: string,
): { readonly passed: boolean; readonly message: string } {
  const matches = execution.matches;
  if (
    execution.truncated &&
    (expectation.kind === "should-not-match" ||
      expectation.kind === "match-count" ||
      expectation.kind === "must-complete-within")
  ) {
    return {
      passed: false,
      message:
        "The engine truncated match collection; this expectation requires a complete result set.",
    };
  }
  switch (expectation.kind) {
    case "should-match":
      return {
        passed: matches.length > 0,
        message:
          matches.length > 0
            ? `Matched ${matches.length} time(s).`
            : "Expected at least one match.",
      };
    case "should-not-match":
      return {
        passed: matches.length === 0,
        message:
          matches.length === 0
            ? "No match, as expected."
            : `Expected no match; received ${matches.length}.`,
      };
    case "match-count":
      return {
        passed: matches.length === expectation.count,
        message: `Expected ${expectation.count} match(es); received ${matches.length}.`,
      };
    case "full-match": {
      const match = matches[expectation.matchIndex];
      const actual = match
        ? subject.slice(match.range.startUtf16, match.range.endUtf16)
        : undefined;
      return {
        passed: actual === expectation.value,
        message:
          actual === expectation.value
            ? "Full match equals the expected value."
            : `Expected ${valuePreview(expectation.value)}; received ${valuePreview(actual)}.`,
      };
    }
    case "capture": {
      const match = matches[expectation.matchIndex];
      const namedCaptures = expectation.groupName
        ? match?.captures.filter(
            (candidate) => candidate.groupName === expectation.groupName,
          )
        : undefined;
      const capture =
        namedCaptures?.find(
          (candidate) => candidate.status !== "did-not-participate",
        ) ??
        namedCaptures?.[0] ??
        match?.captures.find(
          (candidate) => candidate.groupNumber === expectation.groupNumber,
        );
      const actualStatus =
        capture?.status === "truncated" ? "participated" : capture?.status;
      const statusMatches = actualStatus === expectation.status;
      const actualValue = capture?.range
        ? subject.slice(capture.range.startUtf16, capture.range.endUtf16)
        : capture?.status === "truncated"
          ? undefined
          : capture?.value;
      const valueMatches =
        expectation.value === undefined || actualValue === expectation.value;
      return {
        passed: statusMatches && valueMatches,
        message:
          statusMatches && valueMatches
            ? "Capture status and value match."
            : `Capture received ${actualStatus ?? "unavailable"} ${valuePreview(actualValue)}.`,
      };
    }
    case "replacement":
      if (replacement?.execution.truncated) {
        return {
          passed: false,
          message:
            "Replacement match collection was truncated; exact equality cannot be asserted.",
        };
      }
      if (replacement?.outputTruncated) {
        return {
          passed: false,
          message:
            "Replacement output was truncated; exact equality cannot be asserted.",
        };
      }
      return {
        passed: replacement?.output === expectation.expected,
        message:
          replacement?.output === expectation.expected
            ? "Replacement equals the expected output."
            : `Expected ${valuePreview(expectation.expected)}; received ${valuePreview(replacement?.output)}.`,
      };
    case "must-complete-within":
      return {
        passed: execution.elapsedMs <= expectation.milliseconds,
        message: `Engine completed in ${execution.elapsedMs.toFixed(2)} ms (limit ${expectation.milliseconds} ms).`,
      };
    case "must-time-out":
      return {
        passed: false,
        message: "The engine completed; a timeout was expected.",
      };
  }
}

export async function runRegexTest(
  test: RegexTestCase,
  captures: readonly CaptureDefinition[],
  executor: TestExecution,
  defaultTimeoutMs: number,
  maximumMatches: number,
  maximumCaptureRows: number,
  maximumOutputBytes: number,
  maximumMessageBytes = MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
): Promise<RegexTestResult> {
  const timeoutMs =
    test.resourceLimits?.manualExecutionTimeoutMs ?? defaultTimeoutMs;
  const request: RegexExecutionRequest = {
    flavour: test.flavour,
    ...(test.flavourVersion === undefined
      ? {}
      : { flavourVersion: test.flavourVersion }),
    pattern: test.pattern,
    flags: test.flags,
    options: test.options,
    subject: test.subject,
    captureMetadata: captures,
    scanAll: test.scanAll ?? false,
    maximumMatches,
    maximumCaptureRows,
  };
  const started = performance.now();
  try {
    let replacementResult: RegexReplacementResult | undefined;
    const execution =
      test.expectation.kind === "replacement"
        ? (replacementResult = await executor.replace(
            {
              ...request,
              replacement: test.replacement ?? "",
              maximumOutputBytes,
            },
            timeoutMs,
          )).execution
        : await executor.execute(request, timeoutMs);
    const evaluation = execution.accepted
      ? evaluate(test.expectation, execution, replacementResult, test.subject)
      : {
          passed: false,
          message: `Execution engine rejected the pattern: ${boundedTestResultMessage(
            execution.diagnostics[0]?.message ?? "compile failed",
            maximumMessageBytes,
          )}`,
        };
    return {
      testId: test.id,
      ...evaluation,
      message: boundedTestResultMessage(
        evaluation.message,
        maximumMessageBytes,
      ),
      elapsedMs: performance.now() - started,
      engineIdentity: `${execution.engine.engineName} — ${execution.engine.engineVersion}`,
      timedOut: false,
    };
  } catch (error) {
    const timedOut =
      error instanceof Error &&
      "kind" in error &&
      (error as { readonly kind?: string }).kind === "timeout";
    const expectedTimeout = test.expectation.kind === "must-time-out";
    return {
      testId: test.id,
      passed: timedOut && expectedTimeout,
      elapsedMs: performance.now() - started,
      message: boundedTestResultMessage(
        timedOut
          ? expectedTimeout
            ? "Execution timed out as expected; the worker was terminated."
            : "Execution timed out; the worker was terminated."
          : error instanceof Error
            ? error.message
            : String(error),
        maximumMessageBytes,
      ),
      timedOut,
    };
  }
}
