// 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,
}

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,
        };
    }
}

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 (obsolete)".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,
        };
    }

    /// 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,
        };
    }


    /// 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,
        };
    }
}

/// 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>,
    /// 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 = 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;
    }
}

/// 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_all_expected_dexes {
        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: true,
                });
            }
        }
    }
    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,
        dex_support_matrix_entry_count: crate::dex_support_matrix_entries().len() as i64,
        dex_support_matrix: crate::dex_support_matrix_entry_dtos(),
        issues,
    };
}

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;
}

#[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,
            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,
            pool_count: 27,
            pair_count: 27,
            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,
                },
            ],
            pair_summaries: vec![],
            decoded_event_summaries: vec![],
            missing_trade_event_reason_summaries: 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;
        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_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 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);
    }
}
