/** Evidence-bound compatibility checks for recorded V2 HTTP responses. */

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

export type ResponseSchema = {
  type?: string;
  properties?: Record<string, ResponseSchema>;
  required?: string[];
  items?: ResponseSchema;
  anyOf?: ResponseSchema[];
};

export type ExactV2Fixture = {
  path: string;
  method: string;
  fixtureStatus: "exact";
  evidence: { file: string; capturedPath: string; sha256: string };
  responseSchema: ResponseSchema;
};

export type UnresolvedV2Fixture = {
  path: string;
  method: null;
  fixtureStatus: "unresolved";
  reason: string;
  responseSchema: null;
};

export type V2RouteFixture = ExactV2Fixture | UnresolvedV2Fixture;

function jsonType(value: unknown): string {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  if (typeof value === "number" && Number.isInteger(value)) return "integer";
  return typeof value;
}

/**
 * Validates that a candidate retains every captured V2 field and its JSON
 * shape. Schemas deliberately allow unknown object properties: those are
 * additive, optional fields from the perspective of existing consumers.
 */
export function compatibilityViolations(
  schema: ResponseSchema,
  candidate: unknown,
  path = "$",
): string[] {
  if (schema.anyOf) {
    const alternatives = schema.anyOf.map((option) => compatibilityViolations(option, candidate, path));
    if (alternatives.some((violations) => violations.length === 0)) return [];
    return [`${path}: does not match any recorded array item shape`];
  }

  if (schema.type && jsonType(candidate) !== schema.type) {
    return [`${path}: expected ${schema.type}, received ${jsonType(candidate)}`];
  }

  if (schema.type === "object") {
    if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return [];
    const record = candidate as Record<string, unknown>;
    const violations: string[] = [];
    for (const key of schema.required ?? []) {
      if (!(key in record)) violations.push(`${path}.${key}: missing captured field`);
    }
    for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
      if (key in record) violations.push(...compatibilityViolations(propertySchema, record[key], `${path}.${key}`));
    }
    return violations;
  }

  if (schema.type === "array") {
    if (!Array.isArray(candidate) || !schema.items) return [];
    return candidate.flatMap((item, index) => compatibilityViolations(schema.items!, item, `${path}[${index}]`));
  }

  return [];
}

export function assertV2ResponseCompatible(schema: ResponseSchema, candidate: unknown): void {
  const violations = compatibilityViolations(schema, candidate);
  if (violations.length > 0) throw new Error(`V2 response compatibility violation:\n${violations.join("\n")}`);
}
