import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const here = dirname(fileURLToPath(import.meta.url));
const fixturesDir = join(here, "fixtures");

const ACTIVE_EVENT_TYPES = new Set([
  "New Booking",
  "Update Booking / Amendment",
  "Cancel Booking",
  "Cancel Allotment",
  "Voucher Received",
  "Payment Evidence",
  "Rooming List",
  "AMEND GROUP CODE",
  "Invoice Generation",
  "Invoice Received",
  "Payment Notice",
  "Trace",
  "Manual RateCode",
  "TA RECORDER"
]);

const SOURCE_KEYS = [
  "source_message_id",
  "subject",
  "from",
  "cc",
  "received_at",
  "source_channel"
];

const CASE_KEY_KEYS = [
  "group_code",
  "confirmation_number",
  "reservation_number",
  "block_code"
];

const REVIEW_KEYS = [
  "reason_code",
  "visible_reason",
  "review_record_type",
  "missing_fields",
  "blocking_points",
  "conflicting_points",
  "suggested_human_actions",
  "evidence_to_check",
  "known_fields"
];

const CANDIDATE_KEYS = [
  "candidate_id",
  "classification_status",
  "possible_event_types",
  "evidence_refs",
  "target_hints",
  "uncertainty_codes"
];

const EVIDENCE_SOURCES = new Set([
  "body_current",
  "current_attachments",
  "current_tables",
  "current_ocr",
  "parent_child_split_evidence"
]);

const failures = [];
let passed = 0;
let negativeFixturesRejected = 0;

function runCheck(name, fn) {
  try {
    fn();
    passed += 1;
    process.stdout.write(`PASS ${name}\n`);
  } catch (error) {
    failures.push({ name, error });
    process.stderr.write(`FAIL ${name}: ${error.message}\n`);
  }
}

function loadFixture(name) {
  return JSON.parse(readFileSync(join(fixturesDir, name), "utf8"));
}

function isPlainObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function assertExactKeys(value, expected, label) {
  assert.ok(isPlainObject(value), `${label} must be an object`);
  assert.deepEqual(Object.keys(value).sort(), [...expected].sort(), `${label} key set`);
}

function assertNonblank(value, label) {
  assert.equal(typeof value, "string", `${label} must be a string`);
  assert.ok(value.trim().length > 0, `${label} must be nonblank`);
}

function assertStringOrNull(value, label) {
  assert.ok(value === null || typeof value === "string", `${label} must be string|null`);
}

function assertStringArray(value, label, { nonempty = false } = {}) {
  assert.ok(Array.isArray(value), `${label} must be an array`);
  if (nonempty) assert.ok(value.length > 0, `${label} must not be empty`);
  for (const item of value) assert.equal(typeof item, "string", `${label} items must be strings`);
}

function validateSourceMessage(source) {
  assertExactKeys(source, SOURCE_KEYS, "source_message");
  assertNonblank(source.source_message_id, "source_message.source_message_id");
  assertStringOrNull(source.subject, "source_message.subject");
  assertStringOrNull(source.from, "source_message.from");
  assertStringArray(source.cc, "source_message.cc");
  assertStringOrNull(source.received_at, "source_message.received_at");
  assert.equal(source.source_channel, "Email", "source_message.source_channel");
}

function validateCaseKeys(caseKeys) {
  assertExactKeys(caseKeys, CASE_KEY_KEYS, "case_keys");
  for (const key of CASE_KEY_KEYS) assertStringOrNull(caseKeys[key], `case_keys.${key}`);
}

function isRfc6901Pointer(pointer) {
  if (typeof pointer !== "string" || !pointer.startsWith("/")) return false;
  return pointer
    .slice(1)
    .split("/")
    .every((token) => !/~(?![01])/u.test(token));
}

function pointerTokens(pointer) {
  assert.ok(isRfc6901Pointer(pointer), `invalid RFC 6901 JSON Pointer: ${pointer}`);
  return pointer
    .slice(1)
    .split("/")
    .map((token) => token.replaceAll("~1", "/").replaceAll("~0", "~"));
}

function resolvePointer(document, pointer) {
  let current = document;
  for (const token of pointerTokens(pointer)) {
    assert.ok(current !== null && typeof current === "object", `${pointer} traverses a scalar`);
    assert.ok(Object.prototype.hasOwnProperty.call(current, token), `${pointer} target does not exist`);
    current = current[token];
  }
  return current;
}

function setPointer(document, pointer, value) {
  const tokens = pointerTokens(pointer);
  const finalToken = tokens.pop();
  let current = document;
  for (const token of tokens) {
    assert.ok(current !== null && typeof current === "object", `${pointer} traverses a scalar`);
    assert.ok(Object.prototype.hasOwnProperty.call(current, token), `${pointer} target does not exist`);
    current = current[token];
  }
  assert.ok(Object.prototype.hasOwnProperty.call(current, finalToken), `${pointer} target does not exist`);
  current[finalToken] = value;
}

function validateReview(review, recordType, { businessEvent } = {}) {
  assertExactKeys(review, REVIEW_KEYS, "manual_review");
  assertNonblank(review.reason_code, "manual_review.reason_code");
  assertNonblank(review.visible_reason, "manual_review.visible_reason");
  assert.equal(review.review_record_type, recordType, "manual_review.review_record_type");
  for (const key of [
    "missing_fields",
    "blocking_points",
    "conflicting_points",
    "suggested_human_actions",
    "evidence_to_check"
  ]) {
    assertStringArray(review[key], `manual_review.${key}`);
  }
  assert.ok(isPlainObject(review.known_fields), "manual_review.known_fields must be an object");
  if (businessEvent) {
    for (const pointer of review.missing_fields) {
      assert.ok(isRfc6901Pointer(pointer), `${pointer} must be RFC 6901`);
      resolvePointer(businessEvent, pointer);
    }
  }
}

function validateMainOutcome(value) {
  assertExactKeys(value, [
    "source_message",
    "route_code",
    "handler_type",
    "result_type",
    "current_or_history",
    "agent_assessment",
    "notification",
    "manual_review"
  ], "main outcome");
  validateSourceMessage(value.source_message);
  assert.ok(value.route_code === "S10" || value.route_code === "S99", "route_code must be S10|S99");
  assert.equal(value.handler_type, "main_agent_outcome");
  assert.equal(value.result_type, "source_message_review_notification");
  assert.equal(value.current_or_history, "current");
  assertExactKeys(value.agent_assessment, ["status", "reason_code", "automation_action"], "agent_assessment");
  assert.equal(value.agent_assessment.automation_action, "none");
  assertExactKeys(value.notification, [
    "required",
    "notification_type",
    "show_source_message",
    "requires_user_decision",
    "visible_message"
  ], "notification");
  assert.equal(value.notification.required, true);
  assert.equal(value.notification.notification_type, "source_message_review");
  assert.equal(value.notification.show_source_message, true);
  assert.equal(value.notification.requires_user_decision, true);
  assertNonblank(value.notification.visible_message, "notification.visible_message");
  assert.ok(!Object.prototype.hasOwnProperty.call(value.notification, "action_required"));

  if (value.route_code === "S10") {
    assert.equal(value.agent_assessment.status, "no_booking_action_detected");
    assert.equal(value.agent_assessment.reason_code, "no_booking_action_detected");
    assert.equal(value.manual_review, null, "S10.manual_review must be null");
  } else {
    assert.equal(value.agent_assessment.status, "material_package_unavailable");
    assert.equal(value.agent_assessment.reason_code, "material_package_unavailable");
    validateReview(value.manual_review, "main_agent_entry_review");
    assert.equal(value.manual_review.reason_code, value.agent_assessment.reason_code);
  }
}

function validateCandidateEvents(candidateEvents) {
  assert.ok(Array.isArray(candidateEvents), "candidate_events must be an array");
  const ids = new Set();
  for (const candidate of candidateEvents) {
    assertExactKeys(candidate, CANDIDATE_KEYS, "candidate_event");
    assert.match(candidate.candidate_id, /^C[1-9][0-9]*$/u, "candidate_id format");
    assert.ok(!ids.has(candidate.candidate_id), `duplicate candidate_id ${candidate.candidate_id}`);
    ids.add(candidate.candidate_id);
    assert.equal(candidate.classification_status, "coarse_supported_signal");
    assertStringArray(candidate.possible_event_types, "possible_event_types", { nonempty: true });
    assert.equal(new Set(candidate.possible_event_types).size, candidate.possible_event_types.length, "possible_event_types must be unique");
    for (const eventType of candidate.possible_event_types) {
      assert.ok(ACTIVE_EVENT_TYPES.has(eventType), `${eventType} is not an active event type`);
    }
    assert.ok(Array.isArray(candidate.evidence_refs) && candidate.evidence_refs.length > 0, "evidence_refs must be a nonempty array");
    for (const evidenceRef of candidate.evidence_refs) {
      assertExactKeys(evidenceRef, ["source", "index"], "candidate evidence_ref");
      assert.ok(EVIDENCE_SOURCES.has(evidenceRef.source), `invalid evidence source ${evidenceRef.source}`);
      assert.ok(Number.isInteger(evidenceRef.index) && evidenceRef.index >= 0, "evidence_ref.index must be a nonnegative integer");
      if (evidenceRef.source === "body_current") assert.equal(evidenceRef.index, 0, "body_current index must be 0");
    }
    assert.ok(Array.isArray(candidate.target_hints), "target_hints must be an array");
    for (const targetHint of candidate.target_hints) {
      assertExactKeys(targetHint, ["case_keys", "target_locator_raw"], "target_hint");
      validateCaseKeys(targetHint.case_keys);
      assertStringOrNull(targetHint.target_locator_raw, "target_hint.target_locator_raw");
    }
    assertStringArray(candidate.uncertainty_codes, "uncertainty_codes");
  }
}

function validateNoSupportedEventDisposition(disposition, candidateEvents) {
  assertExactKeys(disposition, ["internal_route", "reason_code", "candidate_ids"], "no_supported_event");
  assert.equal(disposition.internal_route, "no_supported_event");
  assert.equal(disposition.reason_code, "skill_validation_found_no_active_event");
  assertStringArray(disposition.candidate_ids, "no_supported_event.candidate_ids", { nonempty: true });
  const knownCandidateIds = new Set(candidateEvents.map((candidate) => candidate.candidate_id));
  for (const candidateId of disposition.candidate_ids) {
    assert.ok(knownCandidateIds.has(candidateId), `no_supported_event references unknown ${candidateId}`);
  }
  assert.deepEqual([...new Set(disposition.candidate_ids)].sort(), [...knownCandidateIds].sort());
}

function validateEvent(event) {
  for (const key of [
    "event_type",
    "event_role",
    "current_or_history",
    "source_event_index",
    "case_keys",
    "relevant_message_excerpt",
    "attachments",
    "file_references",
    "context_used",
    "extracted_fields",
    "manual_review"
  ]) {
    assert.ok(Object.prototype.hasOwnProperty.call(event, key), `event missing ${key}`);
  }
  assert.ok(ACTIVE_EVENT_TYPES.has(event.event_type) || event.event_type === "Need Manual Review", `invalid event_type ${event.event_type}`);
  assertNonblank(event.event_role, "event_role");
  assert.equal(event.current_or_history, "current");
  assertNonblank(event.source_event_index, "source_event_index");
  validateCaseKeys(event.case_keys);
  assert.equal(typeof event.relevant_message_excerpt, "string");
  assert.ok(Array.isArray(event.attachments));
  assert.ok(Array.isArray(event.file_references));
  assert.ok(isPlainObject(event.context_used));
  assert.ok(isPlainObject(event.extracted_fields));
  if (event.manual_review !== null) validateReview(event.manual_review, "business_event_review", { businessEvent: event });
  if (event.event_type === "Need Manual Review") assert.notEqual(event.manual_review, null, "type-unknown Fallback needs manual_review");
}

function validateUnhandledIntent(item) {
  assertExactKeys(item, [
    "category",
    "current_or_history",
    "reason_code",
    "text_raw",
    "visible_message",
    "requires_user_decision",
    "case_keys",
    "attachments",
    "file_references"
  ], "unhandled_current_intent");
  assert.equal(item.category, "unhandled_current_business_content");
  assert.equal(item.current_or_history, "current");
  assert.equal(item.reason_code, "requires_business_approval_or_unsupported_task_card");
  assertNonblank(item.text_raw, "unhandled text_raw");
  assertNonblank(item.visible_message, "unhandled visible_message");
  assert.equal(item.requires_user_decision, true);
  validateCaseKeys(item.case_keys);
  assert.ok(Array.isArray(item.attachments));
  assert.ok(Array.isArray(item.file_references));
}

function validateBusinessRoot(root) {
  assertExactKeys(root, [
    "source_message",
    "message_events",
    "case_candidates",
    "extraction_warnings",
    "unhandled_current_intents"
  ], "business root");
  validateSourceMessage(root.source_message);
  assert.ok(Array.isArray(root.message_events));
  assert.ok(Array.isArray(root.case_candidates));
  assert.ok(Array.isArray(root.extraction_warnings));
  assert.ok(Array.isArray(root.unhandled_current_intents));
  const indexes = new Set();
  for (const event of root.message_events) {
    validateEvent(event);
    assert.ok(!indexes.has(event.source_event_index), `duplicate source_event_index ${event.source_event_index}`);
    indexes.add(event.source_event_index);
  }
  for (const item of root.unhandled_current_intents) validateUnhandledIntent(item);
}

function deriveAdapterTriplet(event) {
  validateEvent(event);
  if (event.event_type === "Need Manual Review") {
    return {
      result_type: "manual_review",
      task_type: "Fallback",
      task_subtype: "business_event_review"
    };
  }

  const resultType = event.manual_review === null ? "normal_task" : "manual_review";
  if (event.event_type === "New Booking") {
    const subtypeByObject = {
      "FIT Reservation": "new_fit_reservation",
      "Group Block": "new_group_block",
      "Allotment / Control Block": "new_allotment_control_block"
    };
    const subtype = subtypeByObject[event.extracted_fields.booking_object_type];
    assert.ok(subtype, "New Booking requires known booking_object_type");
    return { result_type: resultType, task_type: "New Booking", task_subtype: subtype };
  }
  if (event.event_type === "Update Booking / Amendment") {
    return { result_type: resultType, task_type: "Update Booking", task_subtype: "update_booking_amendment" };
  }
  if (event.event_type === "Cancel Booking") {
    if (event.relationship_type === "linked_parent_release_after_child_split") {
      return {
        result_type: resultType,
        task_type: "Cancel Booking",
        task_subtype: "linked_parent_release_after_child_split"
      };
    }
    const subtypeByObject = {
      fit_reservation: "cancel_fit_reservation",
      group_block: "cancel_group_block"
    };
    const subtype = subtypeByObject[event.extracted_fields.cancel_object_type];
    assert.ok(subtype, "Cancel Booking requires known cancel_object_type");
    return { result_type: resultType, task_type: "Cancel Booking", task_subtype: subtype };
  }
  if (event.event_type === "Cancel Allotment") {
    return {
      result_type: resultType,
      task_type: "Cancel Allotment",
      task_subtype: "cancel_allotment_control_block"
    };
  }
  if (event.event_type === "Trace") {
    assert.ok(event.extracted_fields.trace_subtype === "extra_bed" || event.extracted_fields.trace_subtype === "general_request");
    return {
      result_type: resultType,
      task_type: "Trace / Reservation Notes",
      task_subtype: event.extracted_fields.trace_subtype
    };
  }
  throw new Error(`triplet derivation not implemented for ${event.event_type}`);
}

function infrastructureGuard(input) {
  const sourceId = input?.source_message?.source_message_id;
  if (typeof sourceId !== "string" || sourceId.trim() === "") {
    return {
      result_type: "infrastructure_input_error",
      error_code: "missing_source_message_id",
      retryable: true,
      missing_fields: ["source_message.source_message_id"]
    };
  }
  return null;
}

function processSourceIdentityStep(input) {
  const processing = {
    material_reads: 0,
    skill_calls: 0
  };
  const outcome = infrastructureGuard(input);
  return { outcome, processing };
}

function resolveSameCard(taskIdentity, initialState, resolvedState, agentEvent) {
  assertExactKeys(initialState, ["review_status", "review_resolution"], "initial review state");
  assert.equal(initialState.review_status, "pending");
  assert.equal(initialState.review_resolution, null);
  assertExactKeys(resolvedState, ["review_status", "review_resolution"], "resolved review state");
  assert.equal(resolvedState.review_status, "resolved");
  assertExactKeys(resolvedState.review_resolution, ["field_overrides", "resolved_by", "resolved_at"], "review_resolution");
  assert.ok(Array.isArray(resolvedState.review_resolution.field_overrides));
  assert.ok(resolvedState.review_resolution.field_overrides.length > 0, "field_overrides must not be empty");
  assertNonblank(resolvedState.review_resolution.resolved_by, "review_resolution.resolved_by");
  assertNonblank(resolvedState.review_resolution.resolved_at, "review_resolution.resolved_at");
  const originalSnapshot = JSON.stringify(agentEvent);
  const effectivePayload = structuredClone(agentEvent);
  for (const override of resolvedState.review_resolution.field_overrides) {
    assertExactKeys(override, ["field_pointer", "value"], "field_override");
    assert.ok(agentEvent.manual_review.missing_fields.includes(override.field_pointer), `${override.field_pointer} is not a declared missing field`);
    setPointer(effectivePayload, override.field_pointer, override.value);
  }
  for (const pointer of agentEvent.manual_review.missing_fields) {
    const value = resolvePointer(effectivePayload, pointer);
    assert.ok(value !== null && value !== "", `${pointer} remains unresolved`);
  }
  assert.equal(JSON.stringify(agentEvent), originalSnapshot, "Agent payload mutated");
  return {
    task_id: taskIdentity.task_id,
    initial_result_type: taskIdentity.result_type,
    review_status: resolvedState.review_status,
    next_state: "preflight",
    creates_second_card: false,
    agent_payload_immutable: true
  };
}

runCheck("all JSON fixtures parse", () => {
  const files = readdirSync(fixturesDir).filter((name) => name.endsWith(".json"));
  assert.deepEqual(files.sort(), [
    "allotment_scope.json",
    "candidate_gate.json",
    "main_outcomes.json",
    "manual_review_resolution.json",
    "parent_split_two_children.json",
    "row_multiple_derived.json",
    "source_identity_errors.json"
  ]);
  for (const file of files) loadFixture(file);
});

const mainOutcomes = loadFixture("main_outcomes.json");
for (const fixture of mainOutcomes.cases) {
  runCheck(`main outcome ${fixture.case_id}`, () => {
    if (fixture.expect_valid) {
      validateMainOutcome(fixture.value);
    } else {
      assert.throws(() => validateMainOutcome(fixture.value));
      negativeFixturesRejected += 1;
    }
  });
}

const candidateGate = loadFixture("candidate_gate.json");
for (const fixture of candidateGate.cases) {
  runCheck(`candidate gate ${fixture.case_id}`, () => {
    if (fixture.expect_valid) {
      validateCandidateEvents(fixture.candidate_events);
      if (fixture.candidate_events.length === 0) {
        assert.equal(fixture.input_is_understood, true);
        assert.equal(fixture.expected_action, "return_S10");
        assert.ok(!Object.prototype.hasOwnProperty.call(fixture, "skill_disposition"), "Main no-candidate route does not come from Skill");
      } else if (fixture.skill_disposition) {
        validateNoSupportedEventDisposition(fixture.skill_disposition, fixture.candidate_events);
        assert.equal(fixture.expected_action, "convert_internal_no_supported_event_to_S10");
      } else {
        assert.equal(fixture.expected_action, "invoke_booking_desk_event");
      }
    } else {
      assert.throws(() => validateCandidateEvents(fixture.candidate_events));
      negativeFixturesRejected += 1;
    }
  });
}

const parentSplit = loadFixture("parent_split_two_children.json");
runCheck("Parent split has two Child New events and one linked Parent Cancel", () => {
  validateBusinessRoot(parentSplit);
  const children = parentSplit.message_events.filter((event) => event.event_type === "New Booking");
  const parents = parentSplit.message_events.filter((event) => event.relationship_type === "linked_parent_release_after_child_split");
  assert.equal(children.length, 2);
  assert.equal(parents.length, 1);
  for (const child of children) {
    assert.equal(child.extracted_fields.booking_object_type, "Group Block");
    assert.notEqual(child.case_keys.group_code, "PARENT-2608");
  }
  const parent = parents[0];
  assert.equal(parent.event_type, "Cancel Booking");
  assert.equal(parent.extracted_fields.cancel_object_type, "group_block");
  assert.equal(parent.case_keys.group_code, "PARENT-2608");
  assert.equal(parent.requires_downstream_hard_validation, true);
  assert.deepEqual(
    [...parent.related_source_event_indices].sort(),
    children.map((child) => child.source_event_index).sort()
  );
  assert.deepEqual(
    [...parent.extracted_fields.child_group_codes].sort(),
    children.map((child) => child.case_keys.group_code).sort()
  );
  assert.deepEqual(deriveAdapterTriplet(parent), {
    result_type: "normal_task",
    task_type: "Cancel Booking",
    task_subtype: "linked_parent_release_after_child_split"
  });
  assert.ok(parentSplit.message_events.every((event) => event.event_type !== "Allotment Maintenance"));
});

const rowDerived = loadFixture("row_multiple_derived.json");
runCheck("one current row may produce a main event plus linked derived events", () => {
  validateBusinessRoot(rowDerived);
  assert.ok(rowDerived.message_events.length > 1);
  const rowReferences = new Set(
    rowDerived.message_events.map((event) => event.context_used.row_evidence.row_reference)
  );
  assert.deepEqual([...rowReferences], ["booking-update.xlsx#BOOKING 08-2026#42"]);
  const main = rowDerived.message_events.find((event) => event.source_event_index === "E_ROW_CHILD");
  const parent = rowDerived.message_events.find((event) => event.relationship_type === "linked_parent_release_after_child_split");
  const trace = rowDerived.message_events.find((event) => event.relationship_type === "linked_trace");
  assert.equal(main.event_type, "New Booking");
  assert.deepEqual(parent.related_source_event_indices, [main.source_event_index]);
  assert.equal(trace.related_source_event_index, main.source_event_index);
  assert.equal(trace.case_keys.group_code, main.case_keys.group_code);
  assert.equal(trace.extracted_fields.trace_subtype, "extra_bed");
});

const allotmentScope = loadFixture("allotment_scope.json");
runCheck("standalone partial allotment maintenance is a Main no-candidate S10", () => {
  const fixture = allotmentScope.standalone_partial_allotment;
  validateCandidateEvents(fixture.candidate_events);
  assert.equal(fixture.candidate_events.length, 0);
  assert.equal(fixture.input_is_understood, true);
  assert.equal(fixture.expected_route, "S10");
});

runCheck("mixed partial allotment maintenance is preserved as unhandled content", () => {
  const fixture = allotmentScope.mixed_partial_allotment;
  validateBusinessRoot(fixture.output);
  assert.equal(fixture.output.message_events.length, 1);
  assert.equal(fixture.output.unhandled_current_intents.length, 1);
  assert.equal(fixture.output.unhandled_current_intents[0].text_raw, fixture.expected_unhandled_text);
  for (const forbidden of fixture.forbidden_event_types) {
    assert.ok(!fixture.output.message_events.some((event) => event.event_type === forbidden));
  }
});

runCheck("new Allotment or Control Block remains supported", () => {
  const fixture = allotmentScope.new_allotment_control_block;
  validateEvent(fixture.event);
  assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet);
});

runCheck("entire Allotment or Control Block cancellation remains supported", () => {
  const fixture = allotmentScope.cancel_entire_allotment;
  validateEvent(fixture.event);
  assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet);
});

const manualReview = loadFixture("manual_review_resolution.json");
runCheck("generic SUITE keeps known New Booking subtype in manual-review mode", () => {
  const fixture = manualReview.known_subtype_manual_review;
  validateEvent(fixture.event);
  assert.equal(fixture.event.event_type, "New Booking");
  assert.notEqual(fixture.event.manual_review, null);
  assert.equal(fixture.event.extracted_fields.booking_object_type, "Group Block");
  assert.equal(fixture.event.extracted_fields.room_items[0].room_type_raw, "SUITE");
  assert.equal(fixture.event.extracted_fields.room_items[0].pms_room_type_code, null);
  assert.notEqual(fixture.event.requires_downstream_hard_validation, true);
  assert.ok(!Object.prototype.hasOwnProperty.call(fixture.event.manual_review, "required_user_inputs"));
  assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet);
  assert.equal(fixture.expected_adapter_triplet.task_type, "New Booking");
  assert.equal(fixture.expected_adapter_triplet.task_subtype, "new_group_block");
});

runCheck("business manual_review.missing_fields uses resolvable RFC 6901 pointers", () => {
  const event = manualReview.known_subtype_manual_review.event;
  assert.ok(event.manual_review.missing_fields.length > 0);
  for (const pointer of event.manual_review.missing_fields) {
    assert.ok(isRfc6901Pointer(pointer));
    assert.equal(resolvePointer(event, pointer), null);
  }
});

runCheck("unknown business type alone routes to Fallback", () => {
  const fixture = manualReview.type_unknown_fallback;
  validateEvent(fixture.event);
  assert.equal(fixture.event.event_type, "Need Manual Review");
  assert.deepEqual(deriveAdapterTriplet(fixture.event), fixture.expected_adapter_triplet);
});

runCheck("same-card resolution keeps Agent payload and initial result immutable, then enters Preflight", () => {
  const resolution = manualReview.same_card_resolution;
  const source = manualReview[resolution.source_case].event;
  const actual = resolveSameCard(
    resolution.task_identity,
    resolution.initial_state,
    resolution.resolved_state,
    source
  );
  assert.deepEqual(actual, resolution.expected);
});

const sourceIdentity = loadFixture("source_identity_errors.json");
const observedSourceErrors = [];
const rejectedMessageClasses = new Set();
for (const fixture of sourceIdentity.cases) {
  runCheck(`source identity ${fixture.case_id}`, () => {
    const actual = processSourceIdentityStep(fixture.input);
    assert.deepEqual(actual.outcome, fixture.expected_error);
    if (actual.outcome !== null) {
      assert.deepEqual(actual.processing, fixture.expected_processing);
      assert.equal(actual.processing.material_reads, 0);
      assert.equal(actual.processing.skill_calls, 0);
      assertExactKeys(actual.outcome, ["result_type", "error_code", "retryable", "missing_fields"], "typed infrastructure error");
      assert.equal(actual.outcome.result_type, "infrastructure_input_error");
      assert.equal(actual.outcome.error_code, "missing_source_message_id");
      assert.deepEqual(actual.outcome.missing_fields, ["source_message.source_message_id"]);
      assert.ok(!Object.prototype.hasOwnProperty.call(actual.outcome, "route_code"));
      assert.ok(!Object.prototype.hasOwnProperty.call(actual.outcome, "message_events"));
      observedSourceErrors.push(actual.outcome);
      rejectedMessageClasses.add(fixture.message_class);
    }
  });
}

runCheck("Cancel, Thanks, and See Attached inputs share the same Step-0 typed error", () => {
  assert.deepEqual([...rejectedMessageClasses].sort(), ["Cancel", "See Attached", "Thanks"]);
  assert.ok(observedSourceErrors.length >= 3);
  for (const outcome of observedSourceErrors.slice(1)) assert.deepEqual(outcome, observedSourceErrors[0]);
});

if (failures.length > 0) {
  process.stderr.write(`\n${failures.length} contract check(s) failed; ${passed} passed.\n`);
  process.exitCode = 1;
} else {
  process.stdout.write(`\nP0 contract fixtures valid: ${passed} checks passed; ${negativeFixturesRejected} negative fixtures rejected as expected.\n`);
}
