// src/matchers.ts
var MATCH_OPTIONS_DEFAULTS = {
  toolCallOrder: "ignore",
  maxExtraToolCalls: null,
  argumentMatching: "partial"
};
function resolveMatchOptions(suite, testCase, runOverride) {
  const merged = {
    ...MATCH_OPTIONS_DEFAULTS
  };
  for (const layer of [suite, testCase, runOverride]) {
    if (!layer) continue;
    if (layer.toolCallOrder !== void 0)
      merged.toolCallOrder = layer.toolCallOrder;
    if (layer.argumentMatching !== void 0)
      merged.argumentMatching = layer.argumentMatching;
    if (layer.maxExtraToolCalls !== void 0) {
      merged.maxExtraToolCalls = layer.maxExtraToolCalls;
    } else if (layer.allowExtraToolCalls !== void 0) {
      merged.maxExtraToolCalls = layer.allowExtraToolCalls ? null : 0;
    }
  }
  return merged;
}
function matchArgumentPlaceholder(expectedValue, actualValue) {
  if (typeof expectedValue !== "string") return null;
  switch (expectedValue.trim().toLowerCase()) {
    case "any":
      return actualValue !== void 0;
    case "string":
      return typeof actualValue === "string";
    case "number":
      return typeof actualValue === "number";
    case "boolean":
      return typeof actualValue === "boolean";
    case "object":
      return actualValue !== null && typeof actualValue === "object" && !Array.isArray(actualValue);
    case "array":
      return Array.isArray(actualValue);
    case "null":
      return actualValue === null;
    default:
      return null;
  }
}
function stableStringify(value) {
  if (value === null || typeof value !== "object") {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return `[${value.map(stableStringify).join(",")}]`;
  }
  const keys = Object.keys(value).sort();
  return `{${keys.map(
    (k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`
  ).join(",")}}`;
}
function deepEqual(a, b) {
  return stableStringify(a) === stableStringify(b);
}
function partialArgumentsMatch(expectedArgs, actualArgs) {
  for (const [key, value] of Object.entries(expectedArgs)) {
    const placeholder = matchArgumentPlaceholder(value, actualArgs[key]);
    if (placeholder !== null) {
      if (!placeholder) return false;
      continue;
    }
    if (!deepEqual(actualArgs[key], value)) {
      return false;
    }
  }
  return true;
}
function exactArgumentsMatch(expectedArgs, actualArgs) {
  return deepEqual(expectedArgs, actualArgs);
}
function argsCompatible(expectedArgs, actualArgs, mode) {
  if (mode === "ignore") return true;
  if (mode === "partial") {
    if (Object.keys(expectedArgs).length === 0) return true;
    return partialArgumentsMatch(expectedArgs, actualArgs);
  }
  return exactArgumentsMatch(expectedArgs, actualArgs);
}
function callsCompatible(expected, actual, argumentMatching) {
  if (expected.toolName !== actual.toolName) return false;
  return argsCompatible(
    expected.arguments || {},
    actual.arguments || {},
    argumentMatching
  );
}
function pair(expected, actual, mode, argumentMatching) {
  const expectedToActual = /* @__PURE__ */ new Map();
  if (mode === "strict") {
    const min = Math.min(expected.length, actual.length);
    for (let i = 0; i < min; i++) {
      if (callsCompatible(expected[i], actual[i], argumentMatching)) {
        expectedToActual.set(i, i);
      }
    }
    return { expectedToActual };
  }
  if (mode === "superset") {
    let k = 0;
    const supersetCursorByExpected = new Array(expected.length);
    for (let i = 0; i < expected.length; i++) {
      supersetCursorByExpected[i] = k;
      while (k < actual.length) {
        if (callsCompatible(expected[i], actual[k], argumentMatching)) {
          expectedToActual.set(i, k);
          k++;
          break;
        }
        k++;
      }
      if (k >= actual.length) {
        for (let j = i + 1; j < expected.length; j++) {
          supersetCursorByExpected[j] = k;
        }
        break;
      }
    }
    return { expectedToActual, supersetCursorByExpected };
  }
  const consumedActual = /* @__PURE__ */ new Set();
  for (let i = 0; i < expected.length; i++) {
    for (let j = 0; j < actual.length; j++) {
      if (consumedActual.has(j)) continue;
      if (callsCompatible(expected[i], actual[j], argumentMatching)) {
        expectedToActual.set(i, j);
        consumedActual.add(j);
        break;
      }
    }
  }
  return { expectedToActual };
}
function assertValidMaxExtra(value) {
  if (value === void 0 || value === null) return;
  if (!Number.isInteger(value) || value < 0) {
    throw new Error(
      `Invalid maxExtraToolCalls: ${value}. Must be null (unlimited) or a non-negative integer.`
    );
  }
}
function evaluateToolCalls(expected, actual, options) {
  const normalizedExpected = Array.isArray(expected) ? expected : [];
  const normalizedActual = Array.isArray(actual) ? actual : [];
  let maxExtraToolCalls;
  if (options?.maxExtraToolCalls !== void 0) {
    maxExtraToolCalls = options.maxExtraToolCalls;
  } else if (options?.allowExtraToolCalls !== void 0) {
    maxExtraToolCalls = options.allowExtraToolCalls ? null : 0;
  } else {
    maxExtraToolCalls = MATCH_OPTIONS_DEFAULTS.maxExtraToolCalls;
  }
  assertValidMaxExtra(maxExtraToolCalls);
  const toolCallOrder = options?.toolCallOrder ?? MATCH_OPTIONS_DEFAULTS.toolCallOrder;
  const argumentMatching = options?.argumentMatching ?? MATCH_OPTIONS_DEFAULTS.argumentMatching;
  if (options?.isNegativeTest) {
    return {
      missing: [],
      extra: normalizedActual,
      outOfOrder: [],
      argumentMismatches: [],
      passed: normalizedActual.length === 0
    };
  }
  if (normalizedActual.length === 0) {
    return {
      missing: normalizedExpected,
      extra: [],
      outOfOrder: [],
      argumentMismatches: [],
      passed: false
    };
  }
  const { expectedToActual, supersetCursorByExpected } = pair(
    normalizedExpected,
    normalizedActual,
    toolCallOrder,
    argumentMatching
  );
  const matchedExpectedIndices = new Set(expectedToActual.keys());
  const matchedActualIndices = new Set(expectedToActual.values());
  const argumentMismatches = [];
  for (let ei = 0; ei < normalizedExpected.length; ei++) {
    if (matchedExpectedIndices.has(ei)) continue;
    const exp = normalizedExpected[ei];
    const expectedArgs = exp.arguments || {};
    let scanStart = 0;
    let scanEnd = normalizedActual.length;
    if (toolCallOrder === "strict") {
      scanStart = ei;
      scanEnd = Math.min(ei + 1, normalizedActual.length);
    } else if (toolCallOrder === "superset" && supersetCursorByExpected) {
      scanStart = Math.min(
        supersetCursorByExpected[ei] ?? 0,
        normalizedActual.length
      );
    }
    for (let ai = scanStart; ai < scanEnd; ai++) {
      if (matchedActualIndices.has(ai)) continue;
      const act = normalizedActual[ai];
      if (act.toolName !== exp.toolName) continue;
      const actualArgs = act.arguments || {};
      matchedExpectedIndices.add(ei);
      matchedActualIndices.add(ai);
      expectedToActual.set(ei, ai);
      if (argumentMatching !== "ignore") {
        argumentMismatches.push({
          toolName: exp.toolName,
          expectedArgs,
          actualArgs
        });
      }
      break;
    }
  }
  const outOfOrder = [];
  const missing = normalizedExpected.filter(
    (_, idx) => !matchedExpectedIndices.has(idx)
  );
  const extra = normalizedActual.filter(
    (_, idx) => !matchedActualIndices.has(idx)
  );
  const extraFails = maxExtraToolCalls !== null && extra.length > maxExtraToolCalls;
  const passed = missing.length === 0 && argumentMismatches.length === 0 && outOfOrder.length === 0 && !extraFails;
  return {
    missing,
    extra,
    outOfOrder,
    argumentMismatches,
    passed
  };
}

export { MATCH_OPTIONS_DEFAULTS, evaluateToolCalls, resolveMatchOptions };
//# sourceMappingURL=matchers.js.map
//# sourceMappingURL=matchers.js.map