// file: kb_lib/src/local_pipeline_validation.rs

//! Local pipeline validation helpers for non-regression runs.
//!
//! The diagnostics service reports the persisted state of the local pipeline.
//! This module turns that state into explicit pass/fail validation reports that
//! can be reused by tests, command handlers, and UI validation screens.

/// Configuration for a local pipeline validation pass.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalPipelineValidationConfig {
    /// Stable validation profile code.
    pub profile_code: std::string::String,
    /// DEX codes expected in the diagnostics report.
    ///
    /// When this list is empty, no DEX presence check is performed.
    pub expected_dex_codes: std::vec::Vec<std::string::String>,
    /// Whether every expected DEX code must be present.
    pub require_all_expected_dexes: bool,
    /// Whether DEX codes not listed in `expected_dex_codes` are accepted.
    pub allow_unexpected_dexes: bool,
    /// Whether `diagnostics_clean` must be true.
    pub require_clean_diagnostics: bool,
    /// Whether successful trade candidates must all have a persisted trade event.
    pub require_ok_trade_candidates_fully_materialized: bool,
    /// Whether invalid trade events are blocking issues.
    pub require_no_invalid_trade_events: bool,
    /// Whether duplicate trade rows by decoded event id are blocking issues.
    pub require_no_duplicate_decoded_event_trades: bool,
    /// Whether duplicate candle buckets are blocking issues.
    pub require_no_duplicate_candle_buckets: bool,
    /// Whether every pair must have at least one trade event and one candle.
    pub require_no_pair_gaps: bool,
    /// Whether each DEX summary must contain at least one decoded event.
    pub require_decoded_events_per_dex: bool,
    /// Whether each DEX summary must contain at least one trade event.
    pub require_trade_events_per_dex: bool,
    /// Whether each DEX summary must contain at least one candle.
    pub require_candles_per_dex: bool,
    /// Whether non-actionable classification groups must have no linked trade events.
    pub require_no_non_actionable_trade_events_materialized: bool,
    /// Whether the DEX support matrix must satisfy internal semantic invariants.
    pub require_dex_support_matrix_semantics: bool,
    /// Whether pair trading-readiness diagnostics must satisfy internal semantic invariants.
    pub require_pair_trading_readiness_semantics: bool,
}

impl Default for LocalPipelineValidationConfig {
    fn default() -> Self {
        return Self {
            profile_code: "custom".to_string(),
            expected_dex_codes: std::vec::Vec::new(),
            require_all_expected_dexes: false,
            allow_unexpected_dexes: true,
            require_clean_diagnostics: true,
            require_ok_trade_candidates_fully_materialized: true,
            require_no_invalid_trade_events: true,
            require_no_duplicate_decoded_event_trades: true,
            require_no_duplicate_candle_buckets: true,
            require_no_pair_gaps: true,
            require_decoded_events_per_dex: true,
            require_trade_events_per_dex: true,
            require_candles_per_dex: true,
            require_no_non_actionable_trade_events_materialized: true,
            require_dex_support_matrix_semantics: false,
            require_pair_trading_readiness_semantics: false,
        };
    }
}

impl LocalPipelineValidationConfig {
    /// Builds the strict validation config for `0.7.27` non-regression runs.
    pub fn v0_7_27_multi_dex_non_regression() -> Self {
        return Self {
            profile_code: "0.7.27_multi_dex_non_regression".to_string(),
            expected_dex_codes: vec![
                "pump_fun".to_string(),
                "pump_swap".to_string(),
                "raydium_cpmm".to_string(),
                "raydium_clmm".to_string(),
            ],
            require_all_expected_dexes: true,
            allow_unexpected_dexes: false,
            require_clean_diagnostics: true,
            require_ok_trade_candidates_fully_materialized: true,
            require_no_invalid_trade_events: true,
            require_no_duplicate_decoded_event_trades: true,
            require_no_duplicate_candle_buckets: true,
            require_no_pair_gaps: true,
            require_decoded_events_per_dex: true,
            require_trade_events_per_dex: true,
            require_candles_per_dex: true,
            require_no_non_actionable_trade_events_materialized: true,
            require_dex_support_matrix_semantics: false,
            require_pair_trading_readiness_semantics: false,
        };
    }

    /// Builds the `0.7.28` multi-DEX validation config.
    ///
    /// This profile treats currently materialized DEX decoders as required:
    /// PumpSwap, Raydium CPMM, Raydium CLMM, and Meteora DLMM.
    ///
    /// It intentionally accepts additional observed DEX codes because some DEXes
    /// can be detected before their trade/candle materialization is complete
    /// (`meteora_damm_v1`, future launchpads, and other roadmap DEXes).
    pub fn v0_7_28_multi_dex_non_regression() -> Self {
        return Self {
            profile_code: "0.7.28_multi_dex_non_regression".to_string(),
            expected_dex_codes: vec![
                "pump_swap".to_string(),
                "raydium_cpmm".to_string(),
                "raydium_clmm".to_string(),
                "meteora_dlmm".to_string(),
            ],
            require_all_expected_dexes: true,
            allow_unexpected_dexes: true,
            require_clean_diagnostics: false,
            require_ok_trade_candidates_fully_materialized: true,
            require_no_invalid_trade_events: true,
            require_no_duplicate_decoded_event_trades: true,
            require_no_duplicate_candle_buckets: true,
            require_no_pair_gaps: false,
            require_decoded_events_per_dex: true,
            require_trade_events_per_dex: false,
            require_candles_per_dex: false,
            require_no_non_actionable_trade_events_materialized: true,
            require_dex_support_matrix_semantics: false,
            require_pair_trading_readiness_semantics: false,
        };
    }

    /// Builds the `0.7.29` DEX support matrix baseline validation config.
    ///
    /// This profile preserves the `0.7.28` trade/candle non-regression checks
    /// while requiring the observed partial Meteora DAMM v1 surface to remain
    /// visible in diagnostics. It also exposes the DEX support matrix in the
    /// validation report without making planned DEXes blocking.
    pub fn v0_7_29_multi_dex_matrix_baseline() -> Self {
        return Self {
            profile_code: "0.7.29_multi_dex_matrix_baseline".to_string(),
            expected_dex_codes: vec![
                "pump_swap".to_string(),
                "raydium_cpmm".to_string(),
                "raydium_clmm".to_string(),
                "meteora_dlmm".to_string(),
                "meteora_damm_v1".to_string(),
            ],
            require_all_expected_dexes: true,
            allow_unexpected_dexes: true,
            require_clean_diagnostics: false,
            require_ok_trade_candidates_fully_materialized: true,
            require_no_invalid_trade_events: true,
            require_no_duplicate_decoded_event_trades: true,
            require_no_duplicate_candle_buckets: true,
            require_no_pair_gaps: false,
            require_decoded_events_per_dex: true,
            require_trade_events_per_dex: false,
            require_candles_per_dex: false,
            require_no_non_actionable_trade_events_materialized: true,
            require_dex_support_matrix_semantics: false,
            require_pair_trading_readiness_semantics: false,
        };
    }

    /// Builds the `0.7.30` non-trade event classification validation config.
    ///
    /// This profile keeps the `0.7.29` trade/candle checks and exposes the new
    /// decoded-event classification counters. Non-trade events are intentionally
    /// observable but not blocking until their dedicated materializers are added.
    pub fn v0_7_30_non_trade_event_classification() -> Self {
        let mut config = Self::v0_7_29_multi_dex_matrix_baseline();
        config.profile_code = "0.7.30_non_trade_event_classification".to_string();
        return config;
    }

    /// Builds the `0.7.31` trade-event actionability validation config.
    ///
    /// This profile keeps the `0.7.30` classification checks and additionally
    /// requires failed/non-actionable classification groups to have no linked
    /// persisted trade events.
    pub fn v0_7_31_trade_event_actionability_policy() -> Self {
        let mut config = Self::v0_7_30_non_trade_event_classification();
        config.profile_code = "0.7.31_trade_event_actionability_policy".to_string();
        config.require_no_non_actionable_trade_events_materialized = true;
        return config;
    }

    /// Builds the `0.7.32` validation report semantics config.
    ///
    /// This profile keeps the `0.7.31` trade-actionability policy and validates
    /// the support matrix semantics used to interpret partial/planned DEXes.
    pub fn v0_7_32_validation_report_semantics() -> Self {
        let mut config = Self::v0_7_31_trade_event_actionability_policy();
        config.profile_code = "0.7.32_validation_report_semantics".to_string();
        config.require_dex_support_matrix_semantics = true;
        return config;
    }

    /// Builds the `0.7.33` pair trading-readiness validation config.
    ///
    /// This profile keeps the `0.7.32` diagnostics semantics and requires all
    /// persisted pairs to be covered by the pair trading-readiness summaries.
    pub fn v0_7_33_pair_trading_readiness() -> Self {
        let mut config = Self::v0_7_32_validation_report_semantics();
        config.profile_code = "0.7.33_pair_trading_readiness".to_string();
        config.require_pair_trading_readiness_semantics = true;
        return config;
    }

    /// Builds the `0.7.34` non-trade liquidity/lifecycle validation config.
    ///
    /// This profile keeps the `0.7.33` checks and exposes the first materialized
    /// non-trade tables without allowing them to affect trade/candle validation.
    ///
    /// Unlike the full replay profiles, this profile accepts partial databases
    /// produced by targeted pool backfills. Missing expected DEXes are reported
    /// as warnings instead of blocking issues.
    pub fn v0_7_34_non_trade_liquidity_lifecycle() -> Self {
        let mut config = Self::v0_7_33_pair_trading_readiness();
        config.profile_code = "0.7.34_non_trade_liquidity_lifecycle".to_string();
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        return config;
    }

    /// Builds the `0.7.35` non-trade fee/reward/admin validation config.
    ///
    /// This profile keeps the `0.7.34` checks and exposes fee, reward and
    /// administration materialization counters without changing trade/candle
    /// validation semantics.
    pub fn v0_7_35_non_trade_fee_reward_admin() -> Self {
        let mut config = Self::v0_7_34_non_trade_liquidity_lifecycle();
        config.profile_code = "0.7.35_non_trade_fee_reward_admin".to_string();
        return config;
    }

    /// Builds the `0.7.36` Meteora family consolidation validation config.
    ///
    /// This profile keeps the `0.7.35` non-trade counters while making the four
    /// Meteora variants explicit in the expected set. Targeted backfills may
    /// still miss some variants, so missing variants remain warnings.
    pub fn v0_7_36_meteora_family_consolidation() -> Self {
        let mut config = Self::v0_7_35_non_trade_fee_reward_admin();
        config.profile_code = "0.7.36_meteora_family_consolidation".to_string();
        config.expected_dex_codes = vec![
            "meteora_dbc".to_string(),
            "meteora_damm_v1".to_string(),
            "meteora_damm_v2".to_string(),
            "meteora_dlmm".to_string(),
        ];
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        return config;
    }

    /// Builds the `0.7.37` token metadata/catalog enrichment validation config.
    ///
    /// This profile keeps the `0.7.36` Meteora-family checks and exposes
    /// token metadata and pair-symbol diagnostics. Missing metadata remains
    /// informational in this profile; refresh/materialization invariants stay
    /// governed by the existing trade and candle checks.
    pub fn v0_7_37_token_metadata_catalog_enrichment() -> Self {
        let mut config = Self::v0_7_36_meteora_family_consolidation();
        config.profile_code = "0.7.37_token_metadata_catalog_enrichment".to_string();
        return config;
    }

    /// Builds the `0.7.38` token metadata gap prioritization validation config.
    ///
    /// This profile keeps the `0.7.37` metadata counters and exposes
    /// prioritized token metadata gap samples so the next backfill targets are
    /// visible without making missing metadata a blocking validation issue.
    pub fn v0_7_38_token_metadata_gap_prioritization() -> Self {
        let mut config = Self::v0_7_37_token_metadata_catalog_enrichment();
        config.profile_code = "0.7.38_token_metadata_gap_prioritization".to_string();
        return config;
    }

    /// Builds the `0.7.39` DEX-first effective swap surfaces validation config.
    ///
    /// This profile keeps the `0.7.38` metadata-gap semantics and requires the
    /// static DEX matrix invariants that distinguish effective swap surfaces,
    /// routers/aggregators, deferred launch surfaces and unverified candidates.
    pub fn v0_7_39_dex_first_effective_swap_surfaces() -> Self {
        let mut config = Self::v0_7_38_token_metadata_gap_prioritization();
        config.profile_code = "0.7.39_dex_first_effective_swap_surfaces".to_string();
        config.require_dex_support_matrix_semantics = true;
        return config;
    }

    /// Builds the `0.7.40` Raydium effective surfaces validation config.
    ///
    /// This profile keeps the DEX-first matrix invariants and focuses expected
    /// corpus warnings on Raydium effective swap surfaces. Missing Raydium
    /// variants are intentionally warnings so AMM v4 or Stable Swap are not
    /// promoted without transaction evidence.
    pub fn v0_7_40_raydium_effective_surfaces() -> Self {
        let mut config = Self::v0_7_39_dex_first_effective_swap_surfaces();
        config.profile_code = "0.7.40_raydium_effective_surfaces".to_string();
        config.expected_dex_codes = vec![
            "raydium_cpmm".to_string(),
            "raydium_clmm".to_string(),
            "raydium_amm_v4".to_string(),
            "raydium_stable_swap".to_string(),
        ];
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        return config;
    }

    /// Builds the `0.7.41` Raydium AMM v4 swap decoder validation config.
    ///
    /// This profile keeps the Raydium surface diagnostics and labels validation
    /// runs produced after the AMM v4 swap decoder is active. Missing AMM v4
    /// remains a warning so empty or unrelated local corpora stay inspectable.
    pub fn v0_7_41_raydium_amm_v4_swap_decoder() -> Self {
        let mut config = Self::v0_7_40_raydium_effective_surfaces();
        config.profile_code = "0.7.41_raydium_amm_v4_swap_decoder".to_string();
        config.expected_dex_codes = vec!["raydium_amm_v4".to_string()];
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        return config;
    }

    /// Builds the `0.7.42` Raydium family event-coverage validation config.
    ///
    /// This profile expects the three effective Raydium trade surfaces to remain
    /// present while allowing audit-only instruction preservation and future
    /// non-swap event coverage to be added without turning absent Stable Swap or
    /// router activity into blockers.
    pub fn v0_7_42_raydium_family_event_coverage() -> Self {
        let mut config = Self::v0_7_41_raydium_amm_v4_swap_decoder();
        config.profile_code = "0.7.42_raydium_family_event_coverage".to_string();
        config.expected_dex_codes = vec![
            "raydium_cpmm".to_string(),
            "raydium_clmm".to_string(),
            "raydium_amm_v4".to_string(),
        ];
        config.require_all_expected_dexes = true;
        config.allow_unexpected_dexes = true;
        config.require_pair_trading_readiness_semantics = false;
        return config;
    }

    /// Builds the `0.7.43` Meteora effective-surface validation config.
    ///
    /// This profile restarts from the established Meteora-family validation but
    /// keeps missing variants as warnings while a fresh corpus is being built.
    /// It is intentionally strict on global trade/candle invariants: successful
    /// trade candidates still need materialized trades, failed transactions stay
    /// non-actionable, and non-trade Meteora events must not feed trades/candles.
    pub fn v0_7_43_meteora_effective_surfaces() -> Self {
        let mut config = Self::v0_7_36_meteora_family_consolidation();
        config.profile_code = "0.7.43_meteora_effective_surfaces".to_string();
        config.expected_dex_codes = vec![
            "meteora_dbc".to_string(),
            "meteora_damm_v1".to_string(),
            "meteora_damm_v2".to_string(),
            "meteora_dlmm".to_string(),
        ];
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        config.require_trade_events_per_dex = false;
        config.require_candles_per_dex = false;
        config.require_pair_trading_readiness_semantics = false;
        return config;
    }

    /// Builds the `0.7.48-pre` event coverage DB checkpoint validation config.
    ///
    /// This profile closes the DB/reporting checkpoint before `0.7.48` opens
    /// the Raydium CPMM event-coverage tranche. It exposes coverage summaries,
    /// keeps core trade/candle invariants, and scopes the successful
    /// trade-candidate materialization check to the expected Raydium DEXes so
    /// unrelated partial/candidate DEXes remain inspectable without blocking
    /// the DB checkpoint.
    pub fn v0_7_48_pre_event_coverage_db_checkpoint() -> Self {
        let mut config = Self::v0_7_42_raydium_family_event_coverage();
        config.profile_code = "0.7.48-pre_event_coverage_db_checkpoint".to_string();
        config.expected_dex_codes = vec![
            "raydium_cpmm".to_string(),
            "raydium_clmm".to_string(),
            "raydium_amm_v4".to_string(),
        ];
        config.require_all_expected_dexes = false;
        config.allow_unexpected_dexes = true;
        config.require_ok_trade_candidates_fully_materialized = false;
        config.require_trade_events_per_dex = false;
        config.require_candles_per_dex = false;
        config.require_pair_trading_readiness_semantics = false;
        return config;
    }

    /// Builds the legacy `0.7.39` launch-surface validation alias.
    ///
    /// The implementation now delegates to the DEX-first profile so callers that
    /// still pass the previous profile code do not accidentally revive the old
    /// launch-surface priority.
    pub fn v0_7_39_launch_surface_origin_baseline() -> Self {
        return Self::v0_7_39_dex_first_effective_swap_surfaces();
    }
}

/// A single local pipeline validation issue.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalPipelineValidationIssueDto {
    /// Stable machine-readable issue code.
    pub code: std::string::String,
    /// Human-readable issue message.
    pub message: std::string::String,
    /// Optional subject associated with the issue.
    pub subject: std::option::Option<std::string::String>,
    /// Whether the issue blocks the validation report.
    pub blocking: bool,
}

/// Result of a local pipeline validation pass.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalPipelineValidationReportDto {
    /// Stable validation profile code used to produce the report.
    pub validation_profile_code: std::string::String,
    /// Whether the validation passed without blocking issues.
    pub validation_passed: bool,
    /// Number of blocking issues found by validation.
    pub blocking_issue_count: i64,
    /// Number of non-blocking warnings found by validation.
    pub warning_count: i64,
    /// Expected DEX codes used by the validation run.
    pub expected_dex_codes: std::vec::Vec<std::string::String>,
    /// Observed DEX codes found in diagnostics.
    pub observed_dex_codes: std::vec::Vec<std::string::String>,
    /// Total decoded useful non-trade events.
    pub decoded_non_trade_useful_event_count: i64,
    /// Total decoded swap-like events that are intentionally non-actionable.
    pub decoded_non_actionable_trade_event_count: i64,
    /// Total decoded events with unknown classification.
    pub decoded_unknown_event_count: i64,
    /// Total persisted liquidity events.
    pub liquidity_event_count: i64,
    /// Total persisted pool lifecycle events.
    pub pool_lifecycle_event_count: i64,
    /// Total persisted fee events.
    pub fee_event_count: i64,
    /// Total persisted reward events.
    pub reward_event_count: i64,
    /// Total persisted pool administration events.
    pub pool_admin_event_count: i64,
    /// Event coverage entries listed from upstream registry sources.
    pub event_coverage_listed_entry_count: u64,
    /// Event coverage entries wired to local decoder event kinds.
    pub event_coverage_decoded_entry_count: u64,
    /// Event coverage entries observed in local corpus.
    pub event_coverage_observed_entry_count: u64,
    /// Event coverage entries materialized into target tables.
    pub event_coverage_materialized_entry_count: u64,
    /// Sum of local observations across coverage entries.
    pub event_coverage_total_observed_count: u64,
    /// Sum of local materialized rows across coverage entries.
    pub event_coverage_total_materialized_count: u64,
    /// Sum of linked trades across coverage entries.
    pub event_coverage_trade_count: u64,
    /// Audit-only coverage entries.
    pub event_coverage_audit_only_entry_count: u64,
    /// Coverage entries without an expected DB target.
    pub event_coverage_missing_db_target_entry_count: u64,
    /// Coverage entries still upstream Git unverified.
    pub event_coverage_upstream_git_unverified_entry_count: u64,
    /// Coverage entries mapped but not observed locally yet.
    pub event_coverage_upstream_git_mapped_unverified_entry_count: u64,
    /// Coverage entries observed locally.
    pub event_coverage_upstream_git_local_corpus_observed_entry_count: u64,
    /// Coverage entries observed and materialized locally.
    pub event_coverage_upstream_git_local_corpus_materialized_entry_count: u64,
    /// Event coverage summaries grouped by decoder.
    pub event_coverage_summaries: std::vec::Vec<crate::DexEventCoverageSummaryDto>,
    /// Total known tokens.
    pub token_count: i64,
    /// Total tokens missing symbol or name.
    pub token_metadata_missing_count: i64,
    /// Total tokens used by trade-materialized pairs that still miss symbol or name.
    pub tradable_token_metadata_missing_count: i64,
    /// Total quote-side tokens used by pairs that still miss symbol or name.
    pub quote_token_metadata_missing_count: i64,
    /// Total pairs whose display symbol is missing or still falls back to raw mints.
    pub pair_symbol_fallback_count: i64,
    /// Total pairs whose display symbol is present and does not include raw mints.
    pub pair_symbol_resolved_count: i64,
    /// Total pairs whose quote token is WSOL.
    pub wsol_quote_pair_count: i64,
    /// Total pairs whose quote token is a known stable quote.
    pub stable_quote_pair_count: i64,
    /// Number of entries currently exposed by the DEX support matrix.
    pub dex_support_matrix_entry_count: i64,
    /// DEX support matrix snapshot exposed with the validation report.
    pub dex_support_matrix: std::vec::Vec<crate::DexSupportMatrixEntryDto>,
    /// Issues produced by validation.
    pub issues: std::vec::Vec<LocalPipelineValidationIssueDto>,
}

/// Complete local pipeline validation run result.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocalPipelineValidationRunDto {
    /// Stable validation profile code used to produce the run.
    pub validation_profile_code: std::string::String,
    /// Whether the validation passed without blocking issues.
    pub validation_passed: bool,
    /// Number of blocking issues found by validation.
    pub blocking_issue_count: i64,
    /// Number of non-blocking warnings found by validation.
    pub warning_count: i64,
    /// Diagnostics summary that was validated.
    pub summary: crate::LocalPipelineDiagnosticSummaryDto,
    /// Detailed validation report produced from the diagnostics summary.
    pub report: crate::LocalPipelineValidationReportDto,
}

/// Local pipeline validation service.
#[derive(Debug, Clone)]
pub struct LocalPipelineValidationService {
    database: std::sync::Arc<crate::Database>,
}

impl LocalPipelineValidationService {
    /// Creates a new local pipeline validation service.
    pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
        return Self { database };
    }

    /// Diagnoses and validates the persisted local pipeline.
    pub async fn validate(
        &self,
        config: &crate::LocalPipelineValidationConfig,
    ) -> Result<crate::LocalPipelineValidationReportDto, crate::Error> {
        let run_result = self.validate_current_database(config).await;
        let run = match run_result {
            Ok(run) => run,
            Err(error) => return Err(error),
        };
        return Ok(run.report);
    }

    /// Diagnoses the current database and returns both diagnostics and validation report.
    pub async fn validate_current_database(
        &self,
        config: &crate::LocalPipelineValidationConfig,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let diagnostics_service =
            crate::LocalPipelineDiagnosticsService::new(self.database.clone());
        let summary_result = if config.profile_code == "0.7.42_raydium_family_event_coverage"
            || config.profile_code == "0.7.43_meteora_effective_surfaces"
            || config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint"
        {
            diagnostics_service.diagnose_for_validation().await
        } else {
            diagnostics_service.diagnose().await
        };
        let summary = match summary_result {
            Ok(summary) => summary,
            Err(error) => return Err(error),
        };
        return Ok(build_local_pipeline_validation_run(summary, config));
    }

    /// Diagnoses the current database with the strict `0.7.27` non-regression profile.
    pub async fn validate_v0_7_27_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_27_multi_dex_non_regression();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.28` multi-DEX non-regression profile.
    pub async fn validate_v0_7_28_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_28_multi_dex_non_regression();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.29` DEX matrix baseline profile.
    pub async fn validate_v0_7_29_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_29_multi_dex_matrix_baseline();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.30` non-trade classification profile.
    pub async fn validate_v0_7_30_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_30_non_trade_event_classification();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.31` trade actionability profile.
    pub async fn validate_v0_7_31_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config =
            crate::LocalPipelineValidationConfig::v0_7_31_trade_event_actionability_policy();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.32` validation report semantics profile.
    pub async fn validate_v0_7_32_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_32_validation_report_semantics();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.33` pair trading-readiness profile.
    pub async fn validate_v0_7_33_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.34` non-trade liquidity/lifecycle profile.
    pub async fn validate_v0_7_34_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_34_non_trade_liquidity_lifecycle();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.35` non-trade fee/reward/admin profile.
    pub async fn validate_v0_7_35_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_35_non_trade_fee_reward_admin();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.36` Meteora family profile.
    pub async fn validate_v0_7_36_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_36_meteora_family_consolidation();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.37` metadata/catalog profile.
    pub async fn validate_v0_7_37_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config =
            crate::LocalPipelineValidationConfig::v0_7_37_token_metadata_catalog_enrichment();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.38` metadata-gap profile.
    pub async fn validate_v0_7_38_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config =
            crate::LocalPipelineValidationConfig::v0_7_38_token_metadata_gap_prioritization();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.39` DEX-first profile.
    pub async fn validate_v0_7_39_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config =
            crate::LocalPipelineValidationConfig::v0_7_39_dex_first_effective_swap_surfaces();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.40` Raydium profile.
    pub async fn validate_v0_7_40_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_40_raydium_effective_surfaces();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.41` Raydium AMM v4 profile.
    pub async fn validate_v0_7_41_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_41_raydium_amm_v4_swap_decoder();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.42` Raydium family profile.
    pub async fn validate_v0_7_42_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_42_raydium_family_event_coverage();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.43` Meteora effective-surface profile.
    pub async fn validate_v0_7_43_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let config = crate::LocalPipelineValidationConfig::v0_7_43_meteora_effective_surfaces();
        return self.validate_current_database(&config).await;
    }

    /// Diagnoses the current database with the `0.7.48-pre` event coverage checkpoint profile.
    pub async fn validate_v0_7_48_pre_current_database(
        &self,
    ) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
        let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
        let sync_result = coverage_service.sync_upstream_registry(None).await;
        if let Err(error) = sync_result {
            return Err(error);
        }
        let config =
            crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
        return self.validate_current_database(&config).await;
    }
}

/// Validates a diagnostics summary without performing database access.
pub fn validate_local_pipeline_diagnostics_summary(
    summary: &crate::LocalPipelineDiagnosticSummaryDto,
    config: &crate::LocalPipelineValidationConfig,
) -> crate::LocalPipelineValidationReportDto {
    let mut issues = std::vec::Vec::<LocalPipelineValidationIssueDto>::new();
    let mut observed_dex_codes = collect_observed_dex_codes(summary);
    observed_dex_codes.sort();
    let mut expected_dex_codes = config.expected_dex_codes.clone();
    expected_dex_codes.sort();
    if config.require_clean_diagnostics && !summary.diagnostics_clean {
        issues.push(LocalPipelineValidationIssueDto {
            code: "diagnostics_not_clean".to_string(),
            message: format!(
                "local diagnostics report {} blocking issue(s)",
                summary.blocking_issue_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_ok_trade_candidates_fully_materialized
        && summary.actionable_missing_trade_event_count > 0
    {
        issues.push(LocalPipelineValidationIssueDto {
            code: "ok_trade_candidates_without_trade_events".to_string(),
            message: format!(
                "{} actionable successful trade candidate(s) have no persisted trade event",
                summary.actionable_missing_trade_event_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_invalid_trade_events && summary.invalid_trade_event_count > 0 {
        issues.push(LocalPipelineValidationIssueDto {
            code: "invalid_trade_events".to_string(),
            message: format!(
                "{} invalid trade event(s) have missing or non-positive amounts/prices",
                summary.invalid_trade_event_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_duplicate_decoded_event_trades
        && summary.duplicate_decoded_event_trade_count > 0
    {
        issues.push(LocalPipelineValidationIssueDto {
            code: "duplicate_decoded_event_trades".to_string(),
            message: format!(
                "{} decoded event id group(s) have duplicate trade rows",
                summary.duplicate_decoded_event_trade_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_duplicate_candle_buckets && summary.duplicate_candle_bucket_count > 0 {
        issues.push(LocalPipelineValidationIssueDto {
            code: "duplicate_candle_buckets".to_string(),
            message: format!(
                "{} pair candle bucket(s) are duplicated",
                summary.duplicate_candle_bucket_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_pair_gaps && summary.pair_without_trade_count > 0 {
        issues.push(LocalPipelineValidationIssueDto {
            code: "pairs_without_trade".to_string(),
            message: format!(
                "{} pair(s) have no persisted trade event",
                summary.pair_without_trade_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_pair_gaps && summary.pair_without_candle_count > 0 {
        issues.push(LocalPipelineValidationIssueDto {
            code: "pairs_without_candle".to_string(),
            message: format!(
                "{} pair(s) have no persisted candle",
                summary.pair_without_candle_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if config.require_no_non_actionable_trade_events_materialized {
        for classification_summary in &summary.event_classification_summaries {
            if classification_summary.event_actionability != "trade_candidate"
                && classification_summary.trade_event_count > 0
            {
                issues.push(LocalPipelineValidationIssueDto {
                    code: "non_actionable_trade_events_materialized".to_string(),
                    message: format!(
                        "classification '{}' has {} linked trade event(s)",
                        classification_summary.event_actionability,
                        classification_summary.trade_event_count
                    ),
                    subject: Some(classification_summary.event_actionability.clone()),
                    blocking: true,
                });
            }
        }
    }
    if config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint" {
        validate_expected_dex_trade_candidates_materialized_for_checkpoint(
            &mut issues,
            summary,
            &expected_dex_codes,
        );
    }
    if config.require_dex_support_matrix_semantics {
        validate_dex_support_matrix_semantics(&mut issues);
    }
    if config.require_pair_trading_readiness_semantics {
        validate_pair_trading_readiness_semantics(&mut issues, summary);
    }
    let missing_expected_dex_is_warning = config.profile_code
        == "0.7.34_non_trade_liquidity_lifecycle"
        || config.profile_code == "0.7.35_non_trade_fee_reward_admin"
        || config.profile_code == "0.7.36_meteora_family_consolidation"
        || config.profile_code == "0.7.37_token_metadata_catalog_enrichment"
        || config.profile_code == "0.7.38_token_metadata_gap_prioritization"
        || config.profile_code == "0.7.39_dex_first_effective_swap_surfaces"
        || config.profile_code == "0.7.39_launch_surface_origin_baseline"
        || config.profile_code == "0.7.40_raydium_effective_surfaces"
        || config.profile_code == "0.7.41_raydium_amm_v4_swap_decoder"
        || config.profile_code == "0.7.43_meteora_effective_surfaces"
        || config.profile_code == "0.7.48-pre_event_coverage_db_checkpoint";
    if config.require_all_expected_dexes || missing_expected_dex_is_warning {
        for expected_dex_code in &expected_dex_codes {
            if !observed_dex_codes.contains(expected_dex_code) {
                issues.push(LocalPipelineValidationIssueDto {
                    code: "expected_dex_missing".to_string(),
                    message: format!("expected DEX '{}' is missing", expected_dex_code),
                    subject: Some(expected_dex_code.clone()),
                    blocking: config.require_all_expected_dexes,
                });
            }
        }
    }
    if !config.allow_unexpected_dexes && !expected_dex_codes.is_empty() {
        for observed_dex_code in &observed_dex_codes {
            if !expected_dex_codes.contains(observed_dex_code) {
                issues.push(LocalPipelineValidationIssueDto {
                    code: "unexpected_dex".to_string(),
                    message: format!("unexpected DEX '{}' is present", observed_dex_code),
                    subject: Some(observed_dex_code.clone()),
                    blocking: true,
                });
            }
        }
    }
    for dex_summary in &summary.dex_summaries {
        if config.require_decoded_events_per_dex && dex_summary.decoded_event_count <= 0 {
            issues.push(LocalPipelineValidationIssueDto {
                code: "dex_without_decoded_events".to_string(),
                message: format!("DEX '{}' has no decoded event", dex_summary.dex_code),
                subject: Some(dex_summary.dex_code.clone()),
                blocking: true,
            });
        }
        if config.require_trade_events_per_dex && dex_summary.trade_event_count <= 0 {
            issues.push(LocalPipelineValidationIssueDto {
                code: "dex_without_trade_events".to_string(),
                message: format!("DEX '{}' has no trade event", dex_summary.dex_code),
                subject: Some(dex_summary.dex_code.clone()),
                blocking: true,
            });
        }
        if config.require_candles_per_dex && dex_summary.pair_candle_count <= 0 {
            issues.push(LocalPipelineValidationIssueDto {
                code: "dex_without_candles".to_string(),
                message: format!("DEX '{}' has no candle", dex_summary.dex_code),
                subject: Some(dex_summary.dex_code.clone()),
                blocking: true,
            });
        }
    }
    let mut blocking_issue_count = 0_i64;
    let mut warning_count = 0_i64;
    for issue in &issues {
        if issue.blocking {
            blocking_issue_count += 1;
        } else {
            warning_count += 1;
        }
    }
    return LocalPipelineValidationReportDto {
        validation_profile_code: config.profile_code.clone(),
        validation_passed: blocking_issue_count == 0,
        blocking_issue_count,
        warning_count,
        expected_dex_codes,
        observed_dex_codes,
        decoded_non_trade_useful_event_count: summary.decoded_non_trade_useful_event_count,
        decoded_non_actionable_trade_event_count: summary.decoded_non_actionable_trade_event_count,
        decoded_unknown_event_count: summary.decoded_unknown_event_count,
        liquidity_event_count: summary.liquidity_event_count,
        pool_lifecycle_event_count: summary.pool_lifecycle_event_count,
        fee_event_count: summary.fee_event_count,
        reward_event_count: summary.reward_event_count,
        pool_admin_event_count: summary.pool_admin_event_count,
        event_coverage_listed_entry_count: summary.event_coverage_listed_entry_count,
        event_coverage_decoded_entry_count: summary.event_coverage_decoded_entry_count,
        event_coverage_observed_entry_count: summary.event_coverage_observed_entry_count,
        event_coverage_materialized_entry_count: summary.event_coverage_materialized_entry_count,
        event_coverage_total_observed_count: summary.event_coverage_total_observed_count,
        event_coverage_total_materialized_count: summary.event_coverage_total_materialized_count,
        event_coverage_trade_count: summary.event_coverage_trade_count,
        event_coverage_audit_only_entry_count: summary.event_coverage_audit_only_entry_count,
        event_coverage_missing_db_target_entry_count: summary
            .event_coverage_missing_db_target_entry_count,
        event_coverage_upstream_git_unverified_entry_count: summary
            .event_coverage_upstream_git_unverified_entry_count,
        event_coverage_upstream_git_mapped_unverified_entry_count: summary
            .event_coverage_upstream_git_mapped_unverified_entry_count,
        event_coverage_upstream_git_local_corpus_observed_entry_count: summary
            .event_coverage_upstream_git_local_corpus_observed_entry_count,
        event_coverage_upstream_git_local_corpus_materialized_entry_count: summary
            .event_coverage_upstream_git_local_corpus_materialized_entry_count,
        event_coverage_summaries: summary.event_coverage_summaries.clone(),
        token_count: summary.token_count,
        token_metadata_missing_count: summary.token_metadata_missing_count,
        tradable_token_metadata_missing_count: summary.tradable_token_metadata_missing_count,
        quote_token_metadata_missing_count: summary.quote_token_metadata_missing_count,
        pair_symbol_fallback_count: summary.pair_symbol_fallback_count,
        pair_symbol_resolved_count: summary.pair_symbol_resolved_count,
        wsol_quote_pair_count: summary.wsol_quote_pair_count,
        stable_quote_pair_count: summary.stable_quote_pair_count,
        dex_support_matrix_entry_count: crate::dex_support_matrix_entries().len() as i64,
        dex_support_matrix: crate::dex_support_matrix_entry_dtos(),
        issues,
    };
}

fn validate_pair_trading_readiness_semantics(
    issues: &mut std::vec::Vec<crate::LocalPipelineValidationIssueDto>,
    summary: &crate::LocalPipelineDiagnosticSummaryDto,
) {
    if summary.pair_count > 0 && summary.pair_trading_readiness_summaries.is_empty() {
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "pair_trading_readiness_summary_missing".to_string(),
            message: "pair trading-readiness summaries are missing".to_string(),
            subject: None,
            blocking: true,
        });
        return;
    }
    let mut readiness_pair_count = 0_i64;
    let mut readiness_trade_materialized_pair_count = 0_i64;
    let mut readiness_trade_event_count = 0_i64;
    let mut readiness_pair_candle_count = 0_i64;
    for readiness_summary in &summary.pair_trading_readiness_summaries {
        if readiness_summary.pair_trading_readiness.trim().is_empty() {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "pair_trading_readiness_empty".to_string(),
                message: "pair trading-readiness class must not be empty".to_string(),
                subject: None,
                blocking: true,
            });
        }
        if readiness_summary.pair_count <= 0 {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "pair_trading_readiness_empty_group".to_string(),
                message: format!(
                    "pair trading-readiness group '{}' has no pair",
                    readiness_summary.pair_trading_readiness
                ),
                subject: Some(readiness_summary.pair_trading_readiness.clone()),
                blocking: true,
            });
        }
        readiness_pair_count += readiness_summary.pair_count;
        readiness_trade_event_count += readiness_summary.trade_event_count;
        readiness_pair_candle_count += readiness_summary.pair_candle_count;
        if readiness_summary.pair_trading_readiness != "non_trade_materialized" {
            readiness_trade_materialized_pair_count += readiness_summary.pair_count;
        }
        if readiness_summary.pair_trading_readiness == "unknown_quote"
            && readiness_summary.trade_event_count > 0
        {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "pair_trading_readiness_unknown_quote_materialized".to_string(),
                message: format!(
                    "unknown quote readiness group has {} linked trade event(s)",
                    readiness_summary.trade_event_count
                ),
                subject: Some(readiness_summary.pair_trading_readiness.clone()),
                blocking: true,
            });
        }
    }
    if readiness_pair_count != summary.pair_count {
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "pair_trading_readiness_pair_count_mismatch".to_string(),
            message: format!(
                "pair trading-readiness summaries cover {} pair(s), expected {}",
                readiness_pair_count, summary.pair_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if readiness_trade_materialized_pair_count != summary.trade_materialized_pair_count {
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "pair_trading_readiness_materialized_pair_count_mismatch".to_string(),
            message: format!(
                "pair trading-readiness materialized groups cover {} pair(s), expected {}",
                readiness_trade_materialized_pair_count, summary.trade_materialized_pair_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if readiness_trade_event_count != summary.trade_event_count {
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "pair_trading_readiness_trade_event_count_mismatch".to_string(),
            message: format!(
                "pair trading-readiness summaries cover {} trade event(s), expected {}",
                readiness_trade_event_count, summary.trade_event_count
            ),
            subject: None,
            blocking: true,
        });
    }
    if readiness_pair_candle_count != summary.pair_candle_count {
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "pair_trading_readiness_candle_count_mismatch".to_string(),
            message: format!(
                "pair trading-readiness summaries cover {} candle bucket(s), expected {}",
                readiness_pair_candle_count, summary.pair_candle_count
            ),
            subject: None,
            blocking: true,
        });
    }
}

fn validate_dex_support_matrix_semantics(
    issues: &mut std::vec::Vec<crate::LocalPipelineValidationIssueDto>,
) {
    let entries = crate::dex_support_matrix_entry_dtos();
    for entry in entries {
        let entry_code = entry.code.clone();
        if entry.status == "supported" {
            let supported_materialization_ok = entry.decoded
                && entry.materialized
                && entry.trade_candidate
                && entry.candle_candidate
                && entry.pair_candidate
                && entry.pool_candidate
                && entry.catalog_enabled;
            if !supported_materialization_ok {
                issues.push(crate::LocalPipelineValidationIssueDto {
                    code: "supported_dex_matrix_entry_not_fully_materialized".to_string(),
                    message: format!(
                        "supported DEX '{}' must expose decoded/materialized trade, candle, pair and pool capabilities",
                        entry_code
                    ),
                    subject: Some(entry_code.clone()),
                    blocking: true,
                });
            }
        }
        if entry.status == "partial" {
            let has_skip_reason = match &entry.skip_reason {
                Some(skip_reason) => !skip_reason.trim().is_empty(),
                None => false,
            };
            if !has_skip_reason {
                issues.push(crate::LocalPipelineValidationIssueDto {
                    code: "partial_dex_matrix_entry_without_skip_reason".to_string(),
                    message: format!(
                        "partial DEX '{}' must expose an explicit skip reason",
                        entry_code
                    ),
                    subject: Some(entry_code.clone()),
                    blocking: true,
                });
            }
        }
        if (entry.status == "planned" || entry.status == "to_verify" || entry.status == "unknown")
            && entry.catalog_enabled
        {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "inactive_dex_matrix_entry_catalog_enabled".to_string(),
                message: format!(
                    "inactive DEX '{}' must not be enabled in the runtime catalog",
                    entry_code
                ),
                subject: Some(entry_code.clone()),
                blocking: true,
            });
        }
        if (entry.status == "planned" || entry.status == "to_verify" || entry.status == "unknown")
            && (entry.surface_role == "launch_surface" || entry.surface_role == "to_verify")
            && (entry.decoded
                || entry.materialized
                || entry.trade_candidate
                || entry.candle_candidate)
        {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "inactive_deferred_surface_matrix_entry_actionable".to_string(),
                message: format!(
                    "inactive deferred surface '{}' must not be decoded, materialized, or priced as trade/candle candidate",
                    entry_code
                ),
                subject: Some(entry_code.clone()),
                blocking: true,
            });
        }
        if entry.surface_role == "aggregator_router"
            && (entry.trade_candidate || entry.candle_candidate || entry.catalog_enabled)
        {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "aggregator_router_matrix_entry_direct_trade_enabled".to_string(),
                message: format!(
                    "aggregator/router '{}' must not be enabled as a direct trade/candle surface before proof",
                    entry_code
                ),
                subject: Some(entry_code.clone()),
                blocking: true,
            });
        }
        if entry.surface_role == "to_verify"
            && (entry.program_id_status != "to_verify"
                || entry.catalog_enabled
                || entry.decoded
                || entry.materialized
                || entry.trade_candidate
                || entry.candle_candidate)
        {
            issues.push(crate::LocalPipelineValidationIssueDto {
                code: "to_verify_matrix_entry_promoted_without_proof".to_string(),
                message: format!(
                    "to-verify surface '{}' must not expose catalog activation, decoding or trade/candle materialization without corpus proof",
                    entry_code
                ),
                subject: Some(entry_code.clone()),
                blocking: true,
            });
        }
    }
}

fn build_local_pipeline_validation_run(
    summary: crate::LocalPipelineDiagnosticSummaryDto,
    config: &crate::LocalPipelineValidationConfig,
) -> crate::LocalPipelineValidationRunDto {
    let report = crate::validate_local_pipeline_diagnostics_summary(&summary, config);
    return crate::LocalPipelineValidationRunDto {
        validation_profile_code: report.validation_profile_code.clone(),
        validation_passed: report.validation_passed,
        blocking_issue_count: report.blocking_issue_count,
        warning_count: report.warning_count,
        summary,
        report,
    };
}

fn collect_observed_dex_codes(
    summary: &crate::LocalPipelineDiagnosticSummaryDto,
) -> std::vec::Vec<std::string::String> {
    let mut observed_dex_codes = std::vec::Vec::<std::string::String>::new();
    for dex_summary in &summary.dex_summaries {
        if !observed_dex_codes.contains(&dex_summary.dex_code) {
            observed_dex_codes.push(dex_summary.dex_code.clone());
        }
    }
    return observed_dex_codes;
}

fn validate_expected_dex_trade_candidates_materialized_for_checkpoint(
    issues: &mut std::vec::Vec<crate::LocalPipelineValidationIssueDto>,
    summary: &crate::LocalPipelineDiagnosticSummaryDto,
    expected_dex_codes: &[std::string::String],
) {
    for decoded_summary in &summary.decoded_event_summaries {
        if !expected_dex_codes.contains(&decoded_summary.protocol_name) {
            continue;
        }
        let actionability = match &decoded_summary.event_actionability {
            Some(value) => value.as_str(),
            None => "",
        };
        let is_trade_candidate = match decoded_summary.trade_candidate {
            Some(value) => value,
            None => false,
        };
        if actionability != "trade_candidate" && !is_trade_candidate {
            continue;
        }
        let missing_trade_event_count =
            decoded_summary.event_count - decoded_summary.trade_event_count;
        if missing_trade_event_count <= 0 {
            continue;
        }
        issues.push(crate::LocalPipelineValidationIssueDto {
            code: "expected_dex_trade_candidates_without_trade_events".to_string(),
            message: format!(
                "expected DEX '{}' event '{}' has {} trade candidate(s) without persisted trade event",
                decoded_summary.protocol_name,
                decoded_summary.event_kind,
                missing_trade_event_count
            ),
            subject: Some(format!(
                "{}:{}",
                decoded_summary.protocol_name, decoded_summary.event_kind
            )),
            blocking: true,
        });
    }
}

#[cfg(test)]
mod tests {
    fn make_clean_summary() -> crate::LocalPipelineDiagnosticSummaryDto {
        return crate::LocalPipelineDiagnosticSummaryDto {
            transaction_count: 300,
            ok_transaction_count: 157,
            failed_transaction_count: 143,
            decoded_event_count: 216,
            decoded_trade_candidate_count: 216,
            decoded_candle_candidate_count: 216,
            decoded_non_trade_useful_event_count: 0,
            decoded_non_actionable_trade_event_count: 0,
            decoded_unknown_event_count: 0,
            liquidity_event_count: 0,
            pool_lifecycle_event_count: 0,
            fee_event_count: 0,
            reward_event_count: 0,
            pool_admin_event_count: 0,
            event_coverage_listed_entry_count: 0,
            event_coverage_decoded_entry_count: 0,
            event_coverage_observed_entry_count: 0,
            event_coverage_materialized_entry_count: 0,
            event_coverage_total_observed_count: 0,
            event_coverage_total_materialized_count: 0,
            event_coverage_trade_count: 0,
            event_coverage_audit_only_entry_count: 0,
            event_coverage_missing_db_target_entry_count: 0,
            event_coverage_upstream_git_unverified_entry_count: 0,
            event_coverage_upstream_git_mapped_unverified_entry_count: 0,
            event_coverage_upstream_git_local_corpus_observed_entry_count: 0,
            event_coverage_upstream_git_local_corpus_materialized_entry_count: 0,
            event_coverage_summaries: vec![],
            diagnostics_clean: true,
            blocking_issue_count: 0,
            missing_trade_event_count: 6,
            trade_event_count: 210,
            decoded_trade_candidate_without_trade_event_count: 6,
            decoded_trade_candidate_without_trade_event_on_ok_transaction_count: 0,
            decoded_trade_candidate_without_trade_event_on_failed_transaction_count: 6,
            actionable_missing_trade_event_count: 0,
            ignored_failed_transaction_trade_candidate_count: 6,
            decoded_trade_candidate_without_amount_payload_count: 6,
            invalid_trade_event_count: 0,
            pair_candle_count: 230,
            duplicate_decoded_event_trade_count: 0,
            multi_trade_signature_pair_count: 7,
            duplicate_candle_bucket_count: 0,
            token_count: 22,
            token_metadata_missing_count: 0,
            tradable_token_metadata_missing_count: 0,
            quote_token_metadata_missing_count: 0,
            pair_symbol_fallback_count: 4,
            pair_symbol_resolved_count: 23,
            wsol_quote_pair_count: 20,
            stable_quote_pair_count: 3,
            pool_count: 27,
            pair_count: 27,
            pair_gap_counter_semantics: "blocking_actionable_pairs_only".to_string(),
            literal_pair_without_trade_count: 4,
            literal_pair_without_candle_count: 3,
            trade_materialized_pair_count: 23,
            candle_materialized_pair_count: 24,
            actionable_pair_count: 23,
            candle_bucket_timeframe_count: 4,
            candles_are_bucketed: true,
            blocking_pair_without_trade_count: 0,
            blocking_pair_without_candle_count: 0,
            non_actionable_pair_count: 0,
            pair_without_trade_count: 0,
            pair_without_candle_count: 0,
            dex_summaries: vec![
                crate::LocalDexDiagnosticSummaryDto {
                    dex_code: "pump_fun".to_string(),
                    pool_count: 1,
                    pair_count: 1,
                    decoded_event_count: 6,
                    decoded_trade_candidate_count: 6,
                    decoded_candle_candidate_count: 6,
                    trade_event_count: 6,
                    pair_candle_count: 12,
                },
                crate::LocalDexDiagnosticSummaryDto {
                    dex_code: "pump_swap".to_string(),
                    pool_count: 1,
                    pair_count: 1,
                    decoded_event_count: 18,
                    decoded_trade_candidate_count: 18,
                    decoded_candle_candidate_count: 18,
                    trade_event_count: 18,
                    pair_candle_count: 30,
                },
                crate::LocalDexDiagnosticSummaryDto {
                    dex_code: "raydium_cpmm".to_string(),
                    pool_count: 7,
                    pair_count: 7,
                    decoded_event_count: 104,
                    decoded_trade_candidate_count: 104,
                    decoded_candle_candidate_count: 104,
                    trade_event_count: 103,
                    pair_candle_count: 87,
                },
                crate::LocalDexDiagnosticSummaryDto {
                    dex_code: "raydium_clmm".to_string(),
                    pool_count: 19,
                    pair_count: 19,
                    decoded_event_count: 106,
                    decoded_trade_candidate_count: 106,
                    decoded_candle_candidate_count: 106,
                    trade_event_count: 101,
                    pair_candle_count: 131,
                },
            ],
            raydium_surface_summaries: vec![crate::LocalRaydiumSurfaceDiagnosticSummaryDto {
                dex_code: "raydium_clmm".to_string(),
                display_name: "Raydium CLMM".to_string(),
                surface_role: "dex_effective".to_string(),
                program_id: Some(crate::RAYDIUM_CLMM_PROGRAM_ID.to_string()),
                program_id_status: "known".to_string(),
                status: "supported".to_string(),
                catalog_enabled: true,
                instruction_count: 106,
                transaction_count: 101,
                decoded_event_count: 106,
                trade_event_count: 101,
                pair_candle_count: 131,
                latest_slot: Some(1),
                latest_signature: Some("raydium_clmm_fixture".to_string()),
                observed_in_current_corpus: true,
                decoded_in_current_corpus: true,
                trade_materialized_in_current_corpus: true,
            }],
            pair_summaries: vec![],
            pair_actionability_summaries: vec![
                crate::LocalPairActionabilityDiagnosticSummaryDto {
                    pair_actionability: "trade_materialized".to_string(),
                    pair_count: 23,
                    decoded_event_count: 210,
                    decoded_trade_candidate_count: 210,
                    actionable_trade_candidate_count: 210,
                    failed_trade_candidate_count: 0,
                    trade_event_count: 210,
                    pair_candle_count: 230,
                },
                crate::LocalPairActionabilityDiagnosticSummaryDto {
                    pair_actionability: "catalog_only".to_string(),
                    pair_count: 4,
                    decoded_event_count: 0,
                    decoded_trade_candidate_count: 0,
                    actionable_trade_candidate_count: 0,
                    failed_trade_candidate_count: 0,
                    trade_event_count: 0,
                    pair_candle_count: 0,
                },
            ],
            pair_trading_readiness_summaries: vec![
                crate::LocalPairTradingReadinessDiagnosticSummaryDto {
                    pair_trading_readiness: "direct_wsol_quote".to_string(),
                    quote_asset_class: "wsol".to_string(),
                    trading_route_required: false,
                    pair_count: 20,
                    decoded_event_count: 180,
                    decoded_trade_candidate_count: 180,
                    trade_event_count: 180,
                    pair_candle_count: 200,
                },
                crate::LocalPairTradingReadinessDiagnosticSummaryDto {
                    pair_trading_readiness: "direct_stable_quote".to_string(),
                    quote_asset_class: "stable".to_string(),
                    trading_route_required: false,
                    pair_count: 3,
                    decoded_event_count: 30,
                    decoded_trade_candidate_count: 30,
                    trade_event_count: 30,
                    pair_candle_count: 30,
                },
                crate::LocalPairTradingReadinessDiagnosticSummaryDto {
                    pair_trading_readiness: "non_trade_materialized".to_string(),
                    quote_asset_class: "other".to_string(),
                    trading_route_required: false,
                    pair_count: 4,
                    decoded_event_count: 0,
                    decoded_trade_candidate_count: 0,
                    trade_event_count: 0,
                    pair_candle_count: 0,
                },
            ],
            decoded_event_summaries: vec![],
            event_classification_summaries: vec![],
            missing_trade_event_reason_summaries: vec![],
            launch_origin_samples: vec![],
            pool_origin_samples: vec![],
            token_metadata_gap_samples: vec![],
            non_actionable_pair_summaries: vec![],
            missing_trade_event_samples: vec![],
            duplicate_decoded_event_trade_samples: vec![],
            multi_trade_signature_pair_samples: vec![],
            pair_without_trade_samples: vec![],
            pair_without_candle_samples: vec![],
        };
    }

    #[test]
    fn validation_accepts_clean_0_7_27_summary() {
        let summary = make_clean_summary();
        let config = crate::LocalPipelineValidationConfig::v0_7_27_multi_dex_non_regression();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.27_multi_dex_non_regression");
        assert_eq!(report.blocking_issue_count, 0);
        assert_eq!(report.warning_count, 0);
    }

    fn make_0_7_28_summary_with_meteora() -> crate::LocalPipelineDiagnosticSummaryDto {
        let mut summary = make_clean_summary();
        summary.dex_summaries.retain(|dex_summary| {
            return dex_summary.dex_code != "pump_fun";
        });
        summary.dex_summaries.push(crate::LocalDexDiagnosticSummaryDto {
            dex_code: "meteora_dlmm".to_string(),
            pool_count: 67,
            pair_count: 67,
            decoded_event_count: 795,
            decoded_trade_candidate_count: 795,
            decoded_candle_candidate_count: 795,
            trade_event_count: 404,
            pair_candle_count: 448,
        });
        summary.dex_summaries.push(crate::LocalDexDiagnosticSummaryDto {
            dex_code: "meteora_damm_v1".to_string(),
            pool_count: 1,
            pair_count: 1,
            decoded_event_count: 1,
            decoded_trade_candidate_count: 1,
            decoded_candle_candidate_count: 1,
            trade_event_count: 0,
            pair_candle_count: 0,
        });
        summary.pool_count = 95;
        summary.pair_count = 95;
        for readiness_summary in &mut summary.pair_trading_readiness_summaries {
            if readiness_summary.pair_trading_readiness == "non_trade_materialized" {
                readiness_summary.pair_count = 72;
            }
        }
        return summary;
    }

    #[test]
    fn validation_accepts_meteora_dlmm_and_partial_future_dex_in_0_7_28_profile() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_28_multi_dex_non_regression();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.28_multi_dex_non_regression");
        assert_eq!(report.blocking_issue_count, 0);
        assert!(report.observed_dex_codes.contains(&"meteora_dlmm".to_string()));
        assert!(report.observed_dex_codes.contains(&"meteora_damm_v1".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_29_matrix_baseline_summary() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_29_multi_dex_matrix_baseline();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.29_multi_dex_matrix_baseline");
        assert!(report.observed_dex_codes.contains(&"meteora_damm_v1".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_30_non_trade_classification_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.decoded_non_trade_useful_event_count = 3;
        summary.decoded_non_actionable_trade_event_count = 1;
        summary.decoded_unknown_event_count = 0;
        summary.event_classification_summaries.push(
            crate::LocalEventClassificationDiagnosticSummaryDto {
                event_category: "pool_lifecycle".to_string(),
                event_lifecycle_kind: "pool_creation".to_string(),
                event_actionability: "non_trade_useful".to_string(),
                non_trade_useful: true,
                event_count: 3,
                decoded_trade_candidate_count: 0,
                decoded_candle_candidate_count: 0,
                trade_event_count: 0,
            },
        );
        let config = crate::LocalPipelineValidationConfig::v0_7_30_non_trade_event_classification();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.30_non_trade_event_classification");
        assert_eq!(report.decoded_non_trade_useful_event_count, 3);
        assert_eq!(report.decoded_non_actionable_trade_event_count, 1);
        assert_eq!(report.decoded_unknown_event_count, 0);
    }

    #[test]
    fn validation_accepts_0_7_31_trade_actionability_policy_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.event_classification_summaries.push(
            crate::LocalEventClassificationDiagnosticSummaryDto {
                event_category: "trade".to_string(),
                event_lifecycle_kind: "trade_swap".to_string(),
                event_actionability: "failed_transaction".to_string(),
                non_trade_useful: false,
                event_count: 10,
                decoded_trade_candidate_count: 0,
                decoded_candle_candidate_count: 0,
                trade_event_count: 0,
            },
        );
        summary.event_classification_summaries.push(
            crate::LocalEventClassificationDiagnosticSummaryDto {
                event_category: "trade".to_string(),
                event_lifecycle_kind: "trade_swap".to_string(),
                event_actionability: "trade_candidate".to_string(),
                non_trade_useful: false,
                event_count: 12,
                decoded_trade_candidate_count: 12,
                decoded_candle_candidate_count: 12,
                trade_event_count: 12,
            },
        );
        let config =
            crate::LocalPipelineValidationConfig::v0_7_31_trade_event_actionability_policy();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.31_trade_event_actionability_policy");
    }

    #[test]
    fn validation_accepts_0_7_32_validation_report_semantics_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.literal_pair_without_trade_count = 12;
        summary.literal_pair_without_candle_count = 12;
        summary.blocking_pair_without_trade_count = 0;
        summary.blocking_pair_without_candle_count = 0;
        summary.pair_without_trade_count = 0;
        summary.pair_without_candle_count = 0;
        summary.pair_gap_counter_semantics = "blocking_actionable_pairs_only".to_string();
        let config = crate::LocalPipelineValidationConfig::v0_7_32_validation_report_semantics();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.32_validation_report_semantics");
        assert_eq!(summary.literal_pair_without_trade_count, 12);
        assert_eq!(summary.blocking_pair_without_trade_count, 0);
    }

    #[test]
    fn validation_accepts_0_7_33_pair_trading_readiness_summary() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.33_pair_trading_readiness");
        assert_eq!(report.blocking_issue_count, 0);
    }

    #[test]
    fn validation_accepts_0_7_34_non_trade_liquidity_lifecycle_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.decoded_non_trade_useful_event_count = 4;
        summary.liquidity_event_count = 2;
        summary.pool_lifecycle_event_count = 2;
        let config = crate::LocalPipelineValidationConfig::v0_7_34_non_trade_liquidity_lifecycle();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.34_non_trade_liquidity_lifecycle");
        assert_eq!(report.liquidity_event_count, 2);
        assert_eq!(report.pool_lifecycle_event_count, 2);
    }

    #[test]
    fn validation_accepts_0_7_34_partial_database_without_expected_dex_coverage() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.dex_summaries.retain(|dex_summary| {
            return dex_summary.dex_code != "meteora_damm_v1";
        });
        let config = crate::LocalPipelineValidationConfig::v0_7_34_non_trade_liquidity_lifecycle();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.blocking_issue_count, 0);
        assert_eq!(report.warning_count, 1);
        assert_eq!(report.issues[0].code, "expected_dex_missing");
        assert_eq!(report.issues[0].subject, Some("meteora_damm_v1".to_string()));
        assert!(!report.issues[0].blocking);
    }

    #[test]
    fn validation_accepts_0_7_35_non_trade_fee_reward_admin_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.decoded_non_trade_useful_event_count = 5;
        summary.liquidity_event_count = 2;
        summary.pool_lifecycle_event_count = 1;
        summary.fee_event_count = 1;
        summary.reward_event_count = 1;
        summary.pool_admin_event_count = 1;
        let config = crate::LocalPipelineValidationConfig::v0_7_35_non_trade_fee_reward_admin();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.35_non_trade_fee_reward_admin");
        assert_eq!(report.fee_event_count, 1);
        assert_eq!(report.reward_event_count, 1);
        assert_eq!(report.pool_admin_event_count, 1);
    }

    #[test]
    fn validation_accepts_0_7_36_meteora_family_partial_corpus() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.dex_summaries.retain(|dex_summary| {
            return dex_summary.dex_code == "meteora_dlmm";
        });
        let config = crate::LocalPipelineValidationConfig::v0_7_36_meteora_family_consolidation();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.36_meteora_family_consolidation");
        assert_eq!(report.blocking_issue_count, 0);
        assert_eq!(report.warning_count, 3);
        assert!(report.expected_dex_codes.contains(&"meteora_dbc".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_damm_v2".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_dlmm".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_37_metadata_catalog_summary() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.token_count = 108;
        summary.token_metadata_missing_count = 102;
        summary.tradable_token_metadata_missing_count = 12;
        summary.quote_token_metadata_missing_count = 4;
        summary.pair_symbol_fallback_count = 16;
        summary.pair_symbol_resolved_count = 5;
        summary.wsol_quote_pair_count = 13;
        summary.stable_quote_pair_count = 2;
        let config =
            crate::LocalPipelineValidationConfig::v0_7_37_token_metadata_catalog_enrichment();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.37_token_metadata_catalog_enrichment");
        assert_eq!(report.blocking_issue_count, 0);
        assert_eq!(report.token_count, 108);
        assert_eq!(report.token_metadata_missing_count, 102);
        assert_eq!(report.tradable_token_metadata_missing_count, 12);
        assert_eq!(report.quote_token_metadata_missing_count, 4);
        assert_eq!(report.pair_symbol_fallback_count, 16);
        assert_eq!(report.pair_symbol_resolved_count, 5);
        assert_eq!(report.wsol_quote_pair_count, 13);
        assert_eq!(report.stable_quote_pair_count, 2);
    }

    #[test]
    fn validation_accepts_0_7_38_metadata_gap_samples() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.token_count = 108;
        summary.token_metadata_missing_count = 102;
        summary.tradable_token_metadata_missing_count = 6;
        summary.quote_token_metadata_missing_count = 5;
        summary
            .token_metadata_gap_samples
            .push(crate::LocalTokenMetadataGapDiagnosticSampleDto {
                token_id: 42,
                mint: "MissingMetaMint111".to_string(),
                symbol: None,
                name: None,
                decimals: Some(6),
                token_program: crate::SPL_TOKEN_PROGRAM_ID.to_string(),
                is_quote_token: false,
                used_by_trade_materialized_pair: true,
                used_as_quote_token: false,
                trade_materialized_pair_count: 2,
                total_pair_count: 3,
                priority: "tradable_token_missing_metadata".to_string(),
            });
        let config =
            crate::LocalPipelineValidationConfig::v0_7_38_token_metadata_gap_prioritization();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.38_token_metadata_gap_prioritization");
        assert_eq!(report.blocking_issue_count, 0);
        assert_eq!(report.token_metadata_missing_count, 102);
        assert_eq!(summary.token_metadata_gap_samples.len(), 1);
    }

    #[test]
    fn validation_accepts_0_7_39_dex_first_effective_swap_surfaces() {
        let summary = make_0_7_28_summary_with_meteora();
        let config =
            crate::LocalPipelineValidationConfig::v0_7_39_dex_first_effective_swap_surfaces();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.39_dex_first_effective_swap_surfaces");
        assert_eq!(report.blocking_issue_count, 0);
    }

    #[test]
    fn validation_accepts_0_7_40_raydium_effective_surfaces_with_missing_variants_as_warnings() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_40_raydium_effective_surfaces();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.40_raydium_effective_surfaces");
        assert_eq!(report.blocking_issue_count, 0);
        assert!(report.warning_count >= 1);
        assert!(report.expected_dex_codes.contains(&"raydium_amm_v4".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_41_raydium_amm_v4_swap_decoder_profile() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_41_raydium_amm_v4_swap_decoder();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.41_raydium_amm_v4_swap_decoder");
        assert_eq!(report.blocking_issue_count, 0);
        assert!(report.warning_count >= 1);
        assert!(report.expected_dex_codes.contains(&"raydium_amm_v4".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_42_raydium_family_event_coverage_profile() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.dex_summaries.push(crate::LocalDexDiagnosticSummaryDto {
            dex_code: "raydium_amm_v4".to_string(),
            pool_count: 11,
            pair_count: 11,
            decoded_event_count: 58,
            decoded_trade_candidate_count: 58,
            decoded_candle_candidate_count: 58,
            trade_event_count: 58,
            pair_candle_count: 147,
        });
        let config = crate::LocalPipelineValidationConfig::v0_7_42_raydium_family_event_coverage();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.42_raydium_family_event_coverage");
        assert_eq!(report.blocking_issue_count, 0);
        assert!(report.expected_dex_codes.contains(&"raydium_cpmm".to_string()));
        assert!(report.expected_dex_codes.contains(&"raydium_clmm".to_string()));
        assert!(report.expected_dex_codes.contains(&"raydium_amm_v4".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_43_meteora_effective_surfaces_partial_corpus() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.dex_summaries.retain(|dex_summary| {
            return dex_summary.dex_code == "meteora_dlmm"
                || dex_summary.dex_code == "meteora_damm_v1";
        });
        summary.decoded_non_trade_useful_event_count = 7;
        summary.liquidity_event_count = 4;
        summary.pool_lifecycle_event_count = 1;
        summary.fee_event_count = 2;
        let config = crate::LocalPipelineValidationConfig::v0_7_43_meteora_effective_surfaces();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.43_meteora_effective_surfaces");
        assert_eq!(report.blocking_issue_count, 0);
        assert!(report.warning_count >= 2);
        assert!(report.expected_dex_codes.contains(&"meteora_dbc".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_damm_v2".to_string()));
        assert!(report.expected_dex_codes.contains(&"meteora_dlmm".to_string()));
        assert_eq!(report.liquidity_event_count, 4);
        assert_eq!(report.fee_event_count, 2);
    }

    #[test]
    fn validation_accepts_0_7_48_pre_event_coverage_checkpoint() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.event_coverage_listed_entry_count = 4;
        summary.event_coverage_decoded_entry_count = 3;
        summary.event_coverage_observed_entry_count = 2;
        summary.event_coverage_materialized_entry_count = 1;
        summary.event_coverage_total_observed_count = 8;
        summary.event_coverage_total_materialized_count = 5;
        summary.event_coverage_trade_count = 4;
        summary.event_coverage_audit_only_entry_count = 1;
        summary.event_coverage_upstream_git_mapped_unverified_entry_count = 1;
        summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
        summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
        summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
            decoder_code: "raydium_cpmm".to_string(),
            listed_entry_count: 4,
            decoded_entry_count: 3,
            observed_entry_count: 2,
            materialized_entry_count: 1,
            total_observed_count: 8,
            total_materialized_count: 5,
            trade_count: 4,
            audit_only_entry_count: 1,
            missing_db_target_entry_count: 0,
            upstream_git_unverified_entry_count: 0,
            upstream_git_mapped_unverified_entry_count: 1,
            upstream_git_local_corpus_observed_entry_count: 1,
            upstream_git_local_corpus_materialized_entry_count: 1,
        });
        let config =
            crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.validation_profile_code, "0.7.48-pre_event_coverage_db_checkpoint");
        assert_eq!(report.event_coverage_listed_entry_count, 4);
        assert_eq!(report.event_coverage_summaries.len(), 1);
        assert!(report.expected_dex_codes.contains(&"raydium_cpmm".to_string()));
    }

    #[test]
    fn validation_accepts_0_7_48_pre_with_unrelated_partial_dex_gap() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.actionable_missing_trade_event_count = 2;
        summary.decoded_trade_candidate_without_trade_event_on_ok_transaction_count = 2;
        summary.decoded_trade_candidate_without_trade_event_count = 2;
        summary.decoded_trade_candidate_without_amount_payload_count = 2;
        summary
            .decoded_event_summaries
            .push(crate::LocalDecodedEventDiagnosticSummaryDto {
                protocol_name: "fluxbeam".to_string(),
                event_kind: "fluxbeam.swap".to_string(),
                event_category: Some("trade".to_string()),
                event_lifecycle_kind: Some("trade_swap".to_string()),
                event_actionability: Some("trade_candidate".to_string()),
                non_trade_useful: Some(false),
                trade_candidate: Some(true),
                candle_candidate: Some(true),
                event_count: 2,
                trade_event_count: 0,
            });
        let config =
            crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.validation_passed);
        assert_eq!(report.blocking_issue_count, 0);
    }

    #[test]
    fn validation_rejects_0_7_48_pre_expected_dex_trade_gap() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary
            .decoded_event_summaries
            .push(crate::LocalDecodedEventDiagnosticSummaryDto {
                protocol_name: "raydium_cpmm".to_string(),
                event_kind: "raydium_cpmm.swap_base_input".to_string(),
                event_category: Some("trade".to_string()),
                event_lifecycle_kind: Some("trade_swap".to_string()),
                event_actionability: Some("trade_candidate".to_string()),
                non_trade_useful: Some(false),
                trade_candidate: Some(true),
                candle_candidate: Some(true),
                event_count: 7,
                trade_event_count: 6,
            });
        let config =
            crate::LocalPipelineValidationConfig::v0_7_48_pre_event_coverage_db_checkpoint();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(!report.validation_passed);
        assert_eq!(report.issues[0].code, "expected_dex_trade_candidates_without_trade_events");
    }

    #[test]
    fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.pair_trading_readiness_summaries.retain(|readiness_summary| {
            return readiness_summary.pair_trading_readiness != "non_trade_materialized";
        });
        let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(!report.validation_passed);
        assert_eq!(report.issues[0].code, "pair_trading_readiness_pair_count_mismatch");
    }

    #[test]
    fn validation_rejects_0_7_31_failed_transaction_trade_events() {
        let mut summary = make_0_7_28_summary_with_meteora();
        summary.event_classification_summaries.push(
            crate::LocalEventClassificationDiagnosticSummaryDto {
                event_category: "trade".to_string(),
                event_lifecycle_kind: "trade_swap".to_string(),
                event_actionability: "failed_transaction".to_string(),
                non_trade_useful: false,
                event_count: 10,
                decoded_trade_candidate_count: 0,
                decoded_candle_candidate_count: 0,
                trade_event_count: 2,
            },
        );
        let config =
            crate::LocalPipelineValidationConfig::v0_7_31_trade_event_actionability_policy();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(!report.validation_passed);
        assert_eq!(report.blocking_issue_count, 1);
        assert_eq!(report.issues[0].code, "non_actionable_trade_events_materialized");
    }

    #[test]
    fn validation_report_exposes_dex_support_matrix() {
        let summary = make_0_7_28_summary_with_meteora();
        let config = crate::LocalPipelineValidationConfig::v0_7_29_multi_dex_matrix_baseline();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(report.dex_support_matrix_entry_count > 0);
        assert_eq!(
            report.dex_support_matrix_entry_count,
            crate::dex_support_matrix_entries().len() as i64
        );
        let mut found_pump_swap = false;
        for entry in &report.dex_support_matrix {
            if entry.code == "pump_swap" {
                found_pump_swap = true;
                assert!(entry.trade_candidate);
                assert!(entry.candle_candidate);
            }
        }
        assert!(found_pump_swap);
    }

    #[test]
    fn partial_dex_matrix_entries_have_skip_reasons() {
        for entry in crate::dex_support_matrix_entry_dtos() {
            if entry.status == "partial" {
                let has_skip_reason = match entry.skip_reason {
                    Some(skip_reason) => !skip_reason.trim().is_empty(),
                    None => false,
                };
                assert!(has_skip_reason, "{}", entry.code);
            }
        }
    }

    #[test]
    fn supported_dex_matrix_entries_have_materialization_flags() {
        for entry in crate::dex_support_matrix_entry_dtos() {
            if entry.status == "supported" {
                assert!(entry.decoded, "{}", entry.code);
                assert!(entry.materialized, "{}", entry.code);
                assert!(entry.trade_candidate, "{}", entry.code);
                assert!(entry.candle_candidate, "{}", entry.code);
                assert!(entry.pair_candidate, "{}", entry.code);
                assert!(entry.pool_candidate, "{}", entry.code);
            }
        }
    }

    #[test]
    fn planned_dex_matrix_entries_are_not_catalog_enabled() {
        for entry in crate::dex_support_matrix_entry_dtos() {
            if entry.status == "planned" || entry.status == "to_verify" {
                assert!(!entry.catalog_enabled, "{}", entry.code);
            }
        }
    }

    #[test]
    fn validation_rejects_missing_expected_dex() {
        let mut summary = make_clean_summary();
        summary
            .dex_summaries
            .retain(|dex_summary| return dex_summary.dex_code != "pump_swap");
        let config = crate::LocalPipelineValidationConfig::v0_7_27_multi_dex_non_regression();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(!report.validation_passed);
        assert_eq!(report.blocking_issue_count, 1);
        assert_eq!(report.issues[0].code, "expected_dex_missing");
    }

    #[test]
    fn validation_rejects_actionable_missing_trade_events() {
        let mut summary = make_clean_summary();
        summary.diagnostics_clean = false;
        summary.blocking_issue_count = 1;
        summary.decoded_trade_candidate_without_trade_event_on_ok_transaction_count = 2;
        summary.actionable_missing_trade_event_count = 2;
        let config = crate::LocalPipelineValidationConfig::v0_7_27_multi_dex_non_regression();
        let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
        assert!(!report.validation_passed);
        assert_eq!(report.blocking_issue_count, 2);
    }

    #[test]
    fn validation_run_contains_summary_and_report() {
        let summary = make_clean_summary();
        let config = crate::LocalPipelineValidationConfig::v0_7_27_multi_dex_non_regression();
        let run = super::build_local_pipeline_validation_run(summary, &config);
        assert!(run.validation_passed);
        assert_eq!(run.validation_profile_code, "0.7.27_multi_dex_non_regression");
        assert_eq!(run.summary.decoded_event_count, 216);
        assert_eq!(run.report.observed_dex_codes.len(), 4);
    }
}
