///|
pub(all) enum ParityGroup {
  SchemaValidation
  DataFrameAndIndicator
  StrategyRouting
  TwoStageRoutine
  PersistenceAndRedaction
  OperatorSurface
  SafetyInvariant
  ImportTraceability
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) enum ParityExpectation {
  ExactMatch
  SemanticEquivalent
  IntentionalDifference
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) enum ParityStatus {
  ParityPass
  ParityNeedsReview
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) enum ParityEvidenceKind {
  LegacyFixture
  MoonfishEvidence
  KnownDifferenceNote
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ParityEvidenceRef {
  kind : ParityEvidenceKind
  path : String
  role : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ParityCase {
  id : String
  group : ParityGroup
  expectation : ParityExpectation
  status : ParityStatus
  source_ref : String
  moonfish_ref : String
  evidence_refs : Array[ParityEvidenceRef]
  assertion : String
  note : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ParityReportInput {
  workspace : @workspace.AnalysisWorkspace
  import_plan : @migration.LegacyImportPlan
  source_fixture_root : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub(all) struct ParityReport {
  source_fixture_root : String
  pass_count : Int
  review_count : Int
  exact_count : Int
  semantic_count : Int
  intentional_difference_count : Int
  fixture_case_count : Int
  untraced_case_count : Int
  cases : Array[ParityCase]
  summary : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn parity_report_input(
  workspace : @workspace.AnalysisWorkspace,
  import_plan : @migration.LegacyImportPlan,
  source_fixture_root? : String = "../paa/tests",
) -> ParityReportInput {
  { workspace, import_plan, source_fixture_root }
}

///|
fn parity_case(
  id : String,
  group : ParityGroup,
  expectation : ParityExpectation,
  status : ParityStatus,
  source_ref : String,
  moonfish_ref : String,
  evidence_refs : Array[ParityEvidenceRef],
  assertion : String,
  note : String,
) -> ParityCase {
  {
    id,
    group,
    expectation,
    status,
    source_ref,
    moonfish_ref,
    evidence_refs,
    assertion,
    note,
  }
}

///|
fn evidence_ref(
  kind : ParityEvidenceKind,
  path : String,
  role : String,
) -> ParityEvidenceRef {
  { kind, path, role }
}

///|
fn legacy_fixture(
  root : String,
  path : String,
  role : String,
) -> ParityEvidenceRef {
  evidence_ref(LegacyFixture, "\{root}/\{path}", role)
}

///|
fn moonfish_evidence(path : String, role : String) -> ParityEvidenceRef {
  evidence_ref(MoonfishEvidence, path, role)
}

///|
fn known_difference(path : String, role : String) -> ParityEvidenceRef {
  evidence_ref(KnownDifferenceNote, path, role)
}

///|
fn schema_validation_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.retry_decision.issue_class is NoIssue {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "schema-validation",
    SchemaValidation,
    ExactMatch,
    status,
    "\{root}/unit/test_json_validator.py",
    "schemas/stage1-diagnosis.schema.json",
    [
      legacy_fixture(
        root, "unit/test_json_validator.py", "strict stage schema categories",
      ),
      legacy_fixture(
        root, "property/test_json_validator_categories.py", "validator class parity",
      ),
      moonfish_evidence("validation/report.mbt", "stage validation report"),
      moonfish_evidence(
        "orchestration/retry_policy.mbt", "retry classification",
      ),
    ],
    "invalid or incomplete stage JSON maps to the same retry/no-retry boundary",
    "stage validation reports feed the retry policy with no retry needed for the fixture",
  )
}

///|
fn data_feature_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.chart_candle_count > 0 {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "data-frame-indicators",
    DataFrameAndIndicator,
    ExactMatch,
    status,
    "\{root}/property/test_indicators_incremental.py",
    "tools/market_features.mbt",
    [
      legacy_fixture(root, "fixtures/kline_bars.py", "closed-bar fixture data"),
      legacy_fixture(
        root, "property/test_indicators_incremental.py", "incremental indicator parity",
      ),
      legacy_fixture(
        root, "property/test_snapshot_bijection.py", "snapshot round trip",
      ),
      moonfish_evidence("data/features.mbt", "deterministic feature frame"),
      moonfish_evidence("chart/frame.mbt", "closed-candle chart projection"),
    ],
    "closed fixture bars produce stable features, chart candles, and snapshot round trips",
    "fixture market data produces deterministic candles and feature frames",
  )
}

///|
fn routing_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let strategy_count = match workspace.bundle.projection {
    Some(projection) =>
      match projection.result.record.stage1 {
        Some(stage1) => stage1.strategy_files_needed.length()
        None => 0
      }
    None => 0
  }
  let status = if strategy_count > 0 { ParityPass } else { ParityNeedsReview }
  parity_case(
    "strategy-routing",
    StrategyRouting,
    ExactMatch,
    status,
    "\{root}/unit/test_pattern_routing.py",
    "tools/routing.mbt",
    [
      legacy_fixture(
        root, "unit/test_pattern_routing.py", "pattern route fixture",
      ),
      legacy_fixture(
        root, "property/test_router_determinism.py", "routing determinism",
      ),
      moonfish_evidence("routing/strategy.mbt", "strategy-file routing output"),
      moonfish_evidence("prompt/assembly.mbt", "prompt strategy section"),
    ],
    "the same diagnosis patterns select deterministic strategy notes",
    "diagnosis routing produces inspectable strategy-note paths",
  )
}

///|
fn routine_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.job_state.outcome is Completed &&
    workspace.job_state.event_count > 0 {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "two-stage-routine",
    TwoStageRoutine,
    SemanticEquivalent,
    status,
    "\{root}/integration/test_two_stage_happy_path.py",
    "workspace/run_workspace.mbt",
    [
      legacy_fixture(
        root, "integration/test_two_stage_happy_path.py", "happy-path orchestrator fixture",
      ),
      legacy_fixture(
        root, "integration/test_two_stage_stage1_missing_field.py", "stage retry fixture",
      ),
      legacy_fixture(
        root, "integration/test_two_stage_user_cancel.py", "operator cancel fixture",
      ),
      moonfish_evidence("routine/two_stage.mbt", "MoonClaw routine result"),
      moonfish_evidence(
        "workspace/run_workspace.mbt", "durable workspace projection",
      ),
    ],
    "MoonClaw phases preserve the observable PA two-stage lifecycle without a Python orchestrator",
    "MoonClaw job/workspace projection replaces the Python orchestrator while preserving phase evidence",
  )
}

///|
fn persistence_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.book_artifact_count > 0 &&
    workspace.safety_report.ok {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "persistence-redaction",
    PersistenceAndRedaction,
    SemanticEquivalent,
    status,
    "\{root}/unit/test_analysis_history.py",
    "book/write_plan.mbt",
    [
      legacy_fixture(
        root, "unit/test_analysis_history.py", "legacy history record",
      ),
      legacy_fixture(
        root, "property/test_record_round_trip.py", "record round trip",
      ),
      legacy_fixture(root, "property/test_mask_secret.py", "secret masking"),
      moonfish_evidence("book/write_plan.mbt", "MoonBook write plan"),
      moonfish_evidence("export/manifest.mbt", "redacted public export"),
    ],
    "analysis records remain round-trippable while public/debug exports redact sensitive evidence",
    "MoonBook write plans preserve raw evidence while public export redaction is checked separately",
  )
}

///|
fn operator_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.run_view.actions.contains(StartFollowUp) &&
    workspace.run_view.actions.contains(ReplayRun) {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "operator-surface",
    OperatorSurface,
    SemanticEquivalent,
    status,
    "\{root}/e2e/test_smoke_happy_path.py",
    "app/run_view.mbt",
    [
      legacy_fixture(
        root, "e2e/test_smoke_happy_path.py", "operator happy-path smoke",
      ),
      legacy_fixture(
        root, "e2e/test_smoke_switch_mid_flight.py", "operator switch smoke",
      ),
      legacy_fixture(
        root, "e2e/test_smoke_free_chat.py", "follow-up/free-chat smoke",
      ),
      moonfish_evidence("app/run_view.mbt", "operator run view"),
      moonfish_evidence("app/tool_manifest.mbt", "Moondesk app-tool contract"),
      moonfish_evidence("history/index.mbt", "run history projection"),
    ],
    "operator workflow surfaces run, replay, export, follow-up, and debug state through removable app-tool contracts",
    "Moondesk app-tool projection exposes the daily operator workflow without PyQt coupling",
  )
}

///|
fn safety_case(
  workspace : @workspace.AnalysisWorkspace,
  root : String,
) -> ParityCase {
  let status = if workspace.safety_report.ok {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "safety-invariants",
    SafetyInvariant,
    IntentionalDifference,
    status,
    "\{root}/e2e/test_smoke_no_order.py",
    "safety/report.mbt",
    [
      legacy_fixture(root, "e2e/test_smoke_no_order.py", "no-order smoke"),
      legacy_fixture(
        root, "property/test_stage2_no_order_invariant.py", "no-order invariant",
      ),
      legacy_fixture(
        root, "property/test_logs_have_no_plaintext_key.py", "no plaintext key invariant",
      ),
      moonfish_evidence("safety/report.mbt", "suite safety checklist"),
      known_difference(
        "docs/pa-agent-migration-plan.md#phase-7", "Moonfish intentionally omits broker/order execution surfaces",
      ),
    ],
    "safety improves on PA by preserving dry-run analysis while removing broker/order authority",
    "Moonfish intentionally has no broker/order execution path",
  )
}

///|
fn import_case(plan : @migration.LegacyImportPlan, root : String) -> ParityCase {
  let status = if plan.ready_count > 0 &&
    plan.review_count == 0 &&
    plan.skipped_count == 0 {
    ParityPass
  } else {
    ParityNeedsReview
  }
  parity_case(
    "legacy-import-traceability",
    ImportTraceability,
    SemanticEquivalent,
    status,
    "\{root}/../records",
    "migration/import_plan.mbt",
    [
      legacy_fixture(root, "../records", "legacy pending analysis records"),
      legacy_fixture(root, "../experience", "legacy experience memory"),
      legacy_fixture(root, "../trade_records", "legacy trade-log evidence"),
      moonfish_evidence("migration/import_plan.mbt", "idempotent import plan"),
      moonfish_evidence(
        "migration/import_projection.mbt", "MoonBook record projection",
      ),
    ],
    "legacy records, experience, and trade logs keep stable source paths, target paths, and replay anchors",
    "legacy records and experience entries keep source paths, schema version, and trace IDs",
  )
}

///|
fn traced_case(item : ParityCase) -> Bool {
  item.assertion != "" &&
  item.source_ref != "" &&
  item.moonfish_ref != "" &&
  item.evidence_refs.length() >= 2 &&
  item.evidence_refs.any(fn(ref_item) { ref_item.kind is LegacyFixture }) &&
  item.evidence_refs.any(fn(ref_item) { ref_item.kind is MoonfishEvidence })
}

///|
fn count_status(cases : Array[ParityCase], status : ParityStatus) -> Int {
  cases.fold(init=0, fn(count, item) {
    if item.status == status {
      count + 1
    } else {
      count
    }
  })
}

///|
fn count_expectation(
  cases : Array[ParityCase],
  expectation : ParityExpectation,
) -> Int {
  cases.fold(init=0, fn(count, item) {
    if item.expectation == expectation {
      count + 1
    } else {
      count
    }
  })
}

///|
fn count_traced(cases : Array[ParityCase]) -> Int {
  cases.fold(init=0, fn(count, item) {
    if traced_case(item) {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn prepare_parity_report(input : ParityReportInput) -> ParityReport {
  let root = input.source_fixture_root
  let cases = [
    schema_validation_case(input.workspace, root),
    data_feature_case(input.workspace, root),
    routing_case(input.workspace, root),
    routine_case(input.workspace, root),
    persistence_case(input.workspace, root),
    operator_case(input.workspace, root),
    safety_case(input.workspace, root),
    import_case(input.import_plan, root),
  ]
  let pass_count = count_status(cases, ParityPass)
  let review_count = count_status(cases, ParityNeedsReview)
  let fixture_case_count = count_traced(cases)
  let untraced_case_count = cases.length() - fixture_case_count
  {
    source_fixture_root: root,
    pass_count,
    review_count,
    exact_count: count_expectation(cases, ExactMatch),
    semantic_count: count_expectation(cases, SemanticEquivalent),
    intentional_difference_count: count_expectation(
      cases,
      IntentionalDifference,
    ),
    fixture_case_count,
    untraced_case_count,
    cases,
    summary: if review_count == 0 && untraced_case_count == 0 {
      "parity report passed \{pass_count}/\{cases.length()} traced fixture groups"
    } else if untraced_case_count > 0 {
      "parity report has \{untraced_case_count} untraced fixture group(s)"
    } else {
      "parity report has \{review_count} group(s) needing review"
    },
  }
}