// file: kb_lib/src/dex_event_classification.rs

//! Shared DEX event classification and decoded-payload enrichment.
//!
//! This module contains deterministic helpers used by DEX decoding,
//! trade aggregation and future non-trade event materialization.
//!
//! It intentionally does not decode protocol instructions and does not
//! perform database access.

/// Stable business category assigned to one decoded DEX event kind.
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DexEventCategory {
    /// Swap-like event that can potentially become a normalized trade.
    Trade,
    /// Liquidity deposit, withdraw, position open or position close event.
    Liquidity,
    /// Fee collection event.
    Fee,
    /// Reward or emission event.
    Reward,
    /// Pool creation, initialization or migration event.
    PoolLifecycle,
    /// Protocol administration, configuration or permission update event.
    Admin,
    /// Token-account creation, close, wrap or unwrap event.
    TokenAccount,
    /// Informational or audit-only decoded event retained for corpus analysis.
    Informational,
    /// Event kind that is not classified yet.
    Unknown,
}

impl DexEventCategory {
    /// Returns the stable string code persisted inside decoded payload metadata.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Trade => return "trade",
            Self::Liquidity => return "liquidity",
            Self::Fee => return "fee",
            Self::Reward => return "reward",
            Self::PoolLifecycle => return "pool_lifecycle",
            Self::Admin => return "admin",
            Self::TokenAccount => return "token_account",
            Self::Informational => return "informational",
            Self::Unknown => return "unknown",
        }
    }
}

/// Fine-grained lifecycle kind assigned to one decoded DEX event kind.
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DexEventLifecycleKind {
    /// Swap-like trade event.
    TradeSwap,
    /// Pool creation or initialization event.
    PoolCreation,
    /// Pair creation event when it can be distinguished from pool creation.
    PairCreation,
    /// Liquidity deposit or add-liquidity event.
    LiquidityAdd,
    /// Liquidity withdraw or remove-liquidity event.
    LiquidityRemove,
    /// Concentrated-liquidity position open event.
    PositionOpen,
    /// Concentrated-liquidity position close event.
    PositionClose,
    /// Migration event, for example launch surface to AMM/CLMM/DLMM.
    Migration,
    /// Launch or bonding-curve initialization event.
    Launch,
    /// Token mint event detected through a DEX or launch surface decoder.
    Mint,
    /// Token burn event detected through a DEX or launch surface decoder.
    Burn,
    /// Fee collection event.
    FeeCollection,
    /// Reward or emission event.
    Reward,
    /// Administration, configuration or permission update event.
    AdminConfig,
    /// Token-account creation event.
    TokenAccountCreate,
    /// Token-account close event.
    TokenAccountClose,
    /// Instruction-level audit event retained for corpus analysis.
    InstructionAudit,
    /// Event kind that is not classified yet.
    Unknown,
}

impl DexEventLifecycleKind {
    /// Returns the stable string code persisted inside decoded payload metadata.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::TradeSwap => return "trade_swap",
            Self::PoolCreation => return "pool_creation",
            Self::PairCreation => return "pair_creation",
            Self::LiquidityAdd => return "liquidity_add",
            Self::LiquidityRemove => return "liquidity_remove",
            Self::PositionOpen => return "position_open",
            Self::PositionClose => return "position_close",
            Self::Migration => return "migration",
            Self::Launch => return "launch",
            Self::Mint => return "mint",
            Self::Burn => return "burn",
            Self::FeeCollection => return "fee_collection",
            Self::Reward => return "reward",
            Self::AdminConfig => return "admin_config",
            Self::TokenAccountCreate => return "token_account_create",
            Self::TokenAccountClose => return "token_account_close",
            Self::InstructionAudit => return "instruction_audit",
            Self::Unknown => return "unknown",
        }
    }
}

/// Stable actionability class assigned to one decoded DEX event.
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DexEventActionability {
    /// Direct swap-like event that can feed trade/candle materialization.
    TradeCandidate,
    /// Swap-like event detected but not materializable as trade/candle yet.
    NonActionableTrade,
    /// Useful non-trade event that should remain visible for future materialization.
    NonTradeUseful,
    /// Failed transaction event retained for diagnostics but never actionable.
    FailedTransaction,
    /// Classified event that is informational only for the current pipeline.
    Informational,
    /// Event that is not classified yet.
    Unknown,
}

impl DexEventActionability {
    /// Returns the stable string code persisted inside decoded payload metadata.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::TradeCandidate => return "trade_candidate",
            Self::NonActionableTrade => return "non_actionable_trade",
            Self::NonTradeUseful => return "non_trade_useful",
            Self::FailedTransaction => return "failed_transaction",
            Self::Informational => return "informational",
            Self::Unknown => return "unknown",
        }
    }
}

/// Classifies a DEX event kind into a stable business category.
pub fn classify_dex_event_category(event_kind: &str) -> DexEventCategory {
    if is_dex_informational_event_kind(event_kind) {
        return DexEventCategory::Informational;
    }
    if is_dex_reward_event_kind(event_kind) {
        return DexEventCategory::Reward;
    }
    if is_dex_fee_event_kind(event_kind) {
        return DexEventCategory::Fee;
    }
    if is_dex_liquidity_event_kind(event_kind) {
        return DexEventCategory::Liquidity;
    }
    if is_dex_pool_lifecycle_event_kind(event_kind) {
        return DexEventCategory::PoolLifecycle;
    }
    if is_dex_admin_event_kind(event_kind) {
        return DexEventCategory::Admin;
    }
    if is_dex_token_account_event_kind(event_kind) {
        return DexEventCategory::TokenAccount;
    }
    if is_dex_trade_event_kind(event_kind) {
        return DexEventCategory::Trade;
    }
    return DexEventCategory::Unknown;
}

/// Classifies a DEX event kind and returns the persisted category code.
pub fn classify_dex_event_category_code(event_kind: &str) -> &'static str {
    return classify_dex_event_category(event_kind).as_str();
}

/// Classifies a DEX event kind into a fine-grained lifecycle kind.
pub fn classify_dex_event_lifecycle_kind(event_kind: &str) -> DexEventLifecycleKind {
    if is_dex_informational_event_kind(event_kind) {
        return DexEventLifecycleKind::InstructionAudit;
    }
    if event_kind.contains(".create_lock_escrow") {
        return DexEventLifecycleKind::PoolCreation;
    }
    if is_dex_token_burn_event_kind(event_kind) {
        return DexEventLifecycleKind::Burn;
    }
    if is_dex_token_mint_event_kind(event_kind) {
        return DexEventLifecycleKind::Mint;
    }
    if is_dex_migration_event_kind(event_kind) {
        return DexEventLifecycleKind::Migration;
    }
    if is_dex_launch_event_kind(event_kind) {
        return DexEventLifecycleKind::Launch;
    }
    if is_dex_pair_creation_event_kind(event_kind) {
        return DexEventLifecycleKind::PairCreation;
    }
    if is_dex_pool_creation_event_kind(event_kind) {
        return DexEventLifecycleKind::PoolCreation;
    }
    if is_dex_liquidity_add_event_kind(event_kind) {
        return DexEventLifecycleKind::LiquidityAdd;
    }
    if is_dex_liquidity_remove_event_kind(event_kind) {
        return DexEventLifecycleKind::LiquidityRemove;
    }
    if is_dex_position_open_event_kind(event_kind) {
        return DexEventLifecycleKind::PositionOpen;
    }
    if is_dex_position_close_event_kind(event_kind) {
        return DexEventLifecycleKind::PositionClose;
    }
    if is_dex_fee_event_kind(event_kind) {
        return DexEventLifecycleKind::FeeCollection;
    }
    if is_dex_reward_event_kind(event_kind) {
        return DexEventLifecycleKind::Reward;
    }
    if is_dex_admin_event_kind(event_kind) {
        return DexEventLifecycleKind::AdminConfig;
    }
    if is_dex_token_account_close_event_kind(event_kind) {
        return DexEventLifecycleKind::TokenAccountClose;
    }
    if is_dex_token_account_create_event_kind(event_kind) {
        return DexEventLifecycleKind::TokenAccountCreate;
    }
    if is_dex_trade_event_kind(event_kind) {
        return DexEventLifecycleKind::TradeSwap;
    }
    return DexEventLifecycleKind::Unknown;
}

/// Classifies a DEX event kind and returns the persisted lifecycle kind code.
pub fn classify_dex_event_lifecycle_kind_code(event_kind: &str) -> &'static str {
    return classify_dex_event_lifecycle_kind(event_kind).as_str();
}

/// Classifies one decoded DEX event actionability from its kind and candidate flags.
pub fn classify_dex_event_actionability(
    event_kind: &str,
    trade_candidate: bool,
    transaction_failed: bool,
) -> DexEventActionability {
    if transaction_failed {
        return DexEventActionability::FailedTransaction;
    }
    if trade_candidate {
        return DexEventActionability::TradeCandidate;
    }
    if is_dex_informational_event_kind(event_kind) {
        return DexEventActionability::Informational;
    }
    if is_dex_trade_event_kind(event_kind) {
        return DexEventActionability::NonActionableTrade;
    }
    if is_dex_orderbook_event_kind(event_kind) {
        return DexEventActionability::NonTradeUseful;
    }
    let category = classify_dex_event_category(event_kind);
    match category {
        DexEventCategory::Liquidity => return DexEventActionability::NonTradeUseful,
        DexEventCategory::Fee => return DexEventActionability::NonTradeUseful,
        DexEventCategory::Reward => return DexEventActionability::NonTradeUseful,
        DexEventCategory::PoolLifecycle => return DexEventActionability::NonTradeUseful,
        DexEventCategory::Admin => return DexEventActionability::NonTradeUseful,
        DexEventCategory::TokenAccount => return DexEventActionability::NonTradeUseful,
        DexEventCategory::Informational => return DexEventActionability::Informational,
        DexEventCategory::Trade => return DexEventActionability::NonActionableTrade,
        DexEventCategory::Unknown => return DexEventActionability::Unknown,
    }
}

/// Classifies one decoded DEX event actionability and returns its persisted code.
pub fn classify_dex_event_actionability_code(
    event_kind: &str,
    trade_candidate: bool,
    transaction_failed: bool,
) -> &'static str {
    return classify_dex_event_actionability(event_kind, trade_candidate, transaction_failed)
        .as_str();
}

/// Returns true for decoded audit-only events retained for corpus analysis.
pub fn is_dex_informational_event_kind(event_kind: &str) -> bool {
    if event_kind == crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND {
        return true;
    }
    if event_kind == "pump_fees.get_fees" {
        return true;
    }
    if event_kind.contains(".instruction_audit") {
        return true;
    }
    if event_kind.ends_with(".cpi_event") {
        return true;
    }
    if event_kind.ends_with(".anchor_idl_instruction") {
        return true;
    }
    if event_kind.contains(".idl_") {
        return true;
    }
    if event_kind.contains(".liquidity_calculate_event") {
        return true;
    }
    if event_kind.ends_with("_audit") {
        return true;
    }
    if event_kind.contains(".unknown_instruction") {
        return true;
    }
    return false;
}

/// Returns true when the event kind represents a swap-like event.
pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
    if is_meteora_dlmm_anchor_swap_log_event_kind(event_kind) {
        return false;
    }
    if event_kind == "raydium_launchpad.trade_event" {
        return true;
    }
    if event_kind == "pump_fun.trade_event" {
        return true;
    }
    if event_kind.ends_with(".buy_v2") {
        return true;
    }
    if event_kind.ends_with(".sell_v2") {
        return true;
    }
    if event_kind.ends_with(".buy") {
        return true;
    }
    if event_kind.ends_with(".sell") {
        return true;
    }
    if event_kind.contains(".buy_exact") {
        return true;
    }
    if event_kind.contains(".sell_exact") {
        return true;
    }
    if event_kind.ends_with(".swap") {
        return true;
    }
    if event_kind.ends_with(".swap2") {
        return true;
    }
    if event_kind.contains(".swap_") {
        return true;
    }
    if event_kind.ends_with(".exact_input") {
        return true;
    }
    if event_kind.ends_with(".exact_output") {
        return true;
    }
    return false;
}

/// Returns true when the event kind can directly produce a candle candidate.
pub fn is_dex_candle_candidate_event_kind(event_kind: &str) -> bool {
    if event_kind.contains("router") {
        return false;
    }
    if event_kind.contains("route") {
        return false;
    }
    return is_dex_trade_event_kind(event_kind);
}

fn is_meteora_dlmm_anchor_swap_log_event_kind(event_kind: &str) -> bool {
    return matches!(event_kind, "meteora_dlmm.swap_event" | "meteora_dlmm.swap2_evt");
}

fn is_meteora_dlmm_position_lifecycle_event_kind(event_kind: &str) -> bool {
    if !event_kind.starts_with("meteora_dlmm.") {
        return false;
    }
    return matches!(
        event_kind,
        "meteora_dlmm.initialize_position"
            | "meteora_dlmm.initialize_position2"
            | "meteora_dlmm.initialize_position_by_operator"
            | "meteora_dlmm.initialize_position_pda"
            | "meteora_dlmm.position_create_event"
            | "meteora_dlmm.close_position"
            | "meteora_dlmm.close_position2"
            | "meteora_dlmm.close_position_if_empty"
            | "meteora_dlmm.position_close_event"
            | "meteora_dlmm.decrease_position_length"
            | "meteora_dlmm.decrease_position_length_event"
            | "meteora_dlmm.increase_position_length"
            | "meteora_dlmm.increase_position_length2"
            | "meteora_dlmm.increase_position_length_event"
            | "meteora_dlmm.update_position_operator"
            | "meteora_dlmm.update_position_operator_event"
            | "meteora_dlmm.update_position_lock_release_point_event"
            | "meteora_dlmm.set_position_permissionless_operation_bits_evt"
    );
}

fn is_meteora_dlmm_pool_lifecycle_event_kind(event_kind: &str) -> bool {
    if !event_kind.starts_with("meteora_dlmm.") {
        return false;
    }
    if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) {
        return true;
    }
    return matches!(
        event_kind,
        "meteora_dlmm.create_pool"
            | "meteora_dlmm.swap_event"
            | "meteora_dlmm.swap2_evt"
            | "meteora_dlmm.lb_pair_create_event"
            | "meteora_dlmm.initialize_lb_pair"
            | "meteora_dlmm.initialize_lb_pair2"
            | "meteora_dlmm.initialize_permission_lb_pair"
            | "meteora_dlmm.initialize_customizable_permissionless_lb_pair"
            | "meteora_dlmm.initialize_customizable_permissionless_lb_pair2"
            | "meteora_dlmm.initialize_bin_array"
            | "meteora_dlmm.initialize_bin_array_bitmap_extension"
            | "meteora_dlmm.close_bin_array"
            | "meteora_dlmm.go_to_a_bin"
            | "meteora_dlmm.go_to_a_bin_event"
            | "meteora_dlmm.increase_oracle_length"
            | "meteora_dlmm.increase_observation_event"
            | "meteora_dlmm.migrate_bin_array"
            | "meteora_dlmm.migrate_position"
    );
}


fn is_pump_swap_anchor_swap_log_event_kind(event_kind: &str) -> bool {
    return matches!(event_kind, "pump_swap.buy_event" | "pump_swap.sell_event");
}

fn is_pump_swap_admin_event_kind(event_kind: &str) -> bool {
    if !event_kind.starts_with("pump_swap.") {
        return false;
    }
    if is_pump_swap_anchor_swap_log_event_kind(event_kind) {
        return false;
    }
    return matches!(
        event_kind,
        "pump_swap.admin_set_coin_creator"
            | "pump_swap.admin_set_coin_creator_event"
            | "pump_swap.admin_update_token_incentives"
            | "pump_swap.admin_update_token_incentives_event"
            | "pump_swap.create_config"
            | "pump_swap.create_config_event"
            | "pump_swap.disable"
            | "pump_swap.disable_event"
            | "pump_swap.extend_account"
            | "pump_swap.extend_account_event"
            | "pump_swap.migrate_pool_coin_creator"
            | "pump_swap.migrate_pool_coin_creator_event"
            | "pump_swap.reserved_fee_recipients_event"
            | "pump_swap.set_bonding_curve_coin_creator_event"
            | "pump_swap.set_coin_creator"
            | "pump_swap.set_metaplex_coin_creator_event"
            | "pump_swap.set_reserved_fee_recipient"
            | "pump_swap.set_reserved_fee_recipients"
            | "pump_swap.toggle_cashback_enabled"
            | "pump_swap.toggle_mayhem_mode"
            | "pump_swap.update_admin"
            | "pump_swap.update_admin_event"
            | "pump_swap.update_buyback_config"
            | "pump_swap.update_fee_config"
            | "pump_swap.update_fee_config_event"
    );
}

fn is_meteora_dlmm_admin_event_kind(event_kind: &str) -> bool {
    if !event_kind.starts_with("meteora_dlmm.") {
        return false;
    }
    if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
        return false;
    }
    return matches!(
        event_kind,
        "meteora_dlmm.instruction_audit"
            | "meteora_dlmm.close_claim_fee_operator_account"
            | "meteora_dlmm.close_claim_protocol_fee_operator"
            | "meteora_dlmm.close_operator_account"
            | "meteora_dlmm.close_preset_parameter"
            | "meteora_dlmm.close_preset_parameter2"
            | "meteora_dlmm.close_token_badge"
            | "meteora_dlmm.create_claim_protocol_fee_operator"
            | "meteora_dlmm.create_operator_account"
            | "meteora_dlmm.dynamic_fee_parameter_update_event"
            | "meteora_dlmm.fee_parameter_update_event"
            | "meteora_dlmm.initialize_preset_parameter"
            | "meteora_dlmm.initialize_preset_parameter2"
            | "meteora_dlmm.initialize_preset_parameter_v2"
            | "meteora_dlmm.initialize_token_badge"
            | "meteora_dlmm.set_activation_point"
            | "meteora_dlmm.set_pair_status"
            | "meteora_dlmm.set_pair_status_permissionless"
            | "meteora_dlmm.set_permissionless_operation_bits"
            | "meteora_dlmm.set_pre_activation_duration"
            | "meteora_dlmm.set_pre_activation_swap_address"
            | "meteora_dlmm.update_base_fee_parameters"
            | "meteora_dlmm.update_dynamic_fee_parameters"
            | "meteora_dlmm.update_fees_and_reward2"
            | "meteora_dlmm.update_fees_and_rewards"
            | "meteora_dlmm.update_reward_duration"
            | "meteora_dlmm.update_reward_duration_event"
            | "meteora_dlmm.update_reward_funder"
            | "meteora_dlmm.update_reward_funder_event"
    );
}

/// Returns true for liquidity lifecycle changes that must not become candles.
pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
    if event_kind.starts_with("meteora_dlmm.")
        && (event_kind.contains("withdraw_protocol_fee")
            || event_kind.contains("withdraw_ineligible_reward"))
    {
        return false;
    }
    if is_meteora_dlmm_position_lifecycle_event_kind(event_kind) {
        return false;
    }
    if event_kind.starts_with("meteora_dlmm.")
        && (event_kind == "meteora_dlmm.rebalance_liquidity"
            || event_kind == "meteora_dlmm.rebalancing_event"
            || event_kind == "meteora_dlmm.remove_all_liquidity")
    {
        return true;
    }
    if event_kind.starts_with("meteora_dbc.")
        && (event_kind.contains("fee")
            || event_kind.contains("surplus")
            || event_kind.contains("leftover")
            || event_kind.contains("zap_protocol_fee"))
    {
        return false;
    }
    if event_kind == "meteora_dbc.migrate_meteora_damm_claim_lp_token"
        || event_kind == "meteora_dbc.migrate_meteora_damm_lock_lp_token"
    {
        return false;
    }
    if event_kind.starts_with("meteora_dbc.") && event_kind.contains("lp_token") {
        return true;
    }
    if event_kind.contains(".withdraw_pnl") {
        return false;
    }
    if event_kind.contains(".withdraw_srm") {
        return false;
    }
    if event_kind.contains(".deposit") {
        return true;
    }
    if event_kind.contains(".lp_change_event") {
        return true;
    }
    if event_kind.contains(".liquidity_change_event") {
        return true;
    }
    if event_kind.contains(".withdraw") {
        return true;
    }
    if event_kind.contains(".add_liquidity") {
        return true;
    }
    if event_kind.contains(".remove_liquidity") {
        return true;
    }
    if event_kind.contains(".increase_liquidity") {
        return true;
    }
    if event_kind.contains(".decrease_liquidity") {
        return true;
    }
    if event_kind.contains(".initialize_position") {
        return true;
    }
    if event_kind.contains(".create_personal_position_event") {
        return true;
    }
    if event_kind.contains(".open_position") {
        return true;
    }
    if event_kind.contains(".close_position") {
        return true;
    }
    if event_kind.contains(".close_protocol_position") {
        return true;
    }
    if event_kind.contains(".position_close") {
        return true;
    }
    return false;
}

/// Returns true for liquidity add-like DEX events.
pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".deposit") {
        return true;
    }
    if event_kind.contains(".add_liquidity") {
        return true;
    }
    if event_kind.contains(".increase_liquidity") {
        return true;
    }
    return false;
}

/// Returns true for liquidity remove-like DEX events.
pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool {
    if event_kind.starts_with("meteora_dlmm.")
        && (event_kind.contains("withdraw_protocol_fee")
            || event_kind.contains("withdraw_ineligible_reward"))
    {
        return false;
    }
    if event_kind == "meteora_dlmm.remove_all_liquidity" {
        return true;
    }
    if event_kind.contains(".withdraw_pnl") {
        return false;
    }
    if event_kind.contains(".withdraw_srm") {
        return false;
    }
    if event_kind.contains(".withdraw") {
        return true;
    }
    if event_kind.contains(".remove_liquidity") {
        return true;
    }
    if event_kind.contains(".decrease_liquidity") {
        return true;
    }
    return false;
}

/// Returns true for concentrated-liquidity position open events.
pub fn is_dex_position_open_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".initialize_position") {
        return true;
    }
    if event_kind.contains(".open_position") {
        return true;
    }
    if event_kind.contains(".create_personal_position_event") {
        return true;
    }
    if event_kind.contains(".position_create") {
        return true;
    }
    return false;
}

/// Returns true for concentrated-liquidity position close events.
pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".close_position") {
        return true;
    }
    if event_kind.contains(".close_protocol_position") {
        return true;
    }
    return false;
}

/// Returns true for fee collection events.
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
    if event_kind.starts_with("meteora_dlmm.") && event_kind.contains("claim_fee_operator") {
        return false;
    }
    if event_kind.starts_with("meteora_dlmm.")
        && (event_kind.contains("claim_fee")
            || event_kind.contains("composition_fee")
            || event_kind.contains("withdraw_protocol_fee")
            || event_kind.contains("zap_protocol_fee"))
    {
        return true;
    }
    if event_kind.starts_with("meteora_dbc.")
        && (event_kind.contains("fee")
            || event_kind.contains("surplus")
            || event_kind.contains("leftover")
            || event_kind.contains("zap_protocol_fee"))
    {
        return true;
    }
    if event_kind.starts_with("pump_fees.")
        && (event_kind.contains("donation_fee_pda_cranked")
            || event_kind.contains("sweep_buyback")
            || event_kind.contains("crank_donation_fee_pda"))
    {
        return true;
    }
    if event_kind.contains("claim_creator_fee") {
        return true;
    }
    if event_kind.contains("claim_platform_fee") {
        return true;
    }
    if event_kind.contains("collect_migrate_fee") {
        return true;
    }
    if event_kind.contains("collect_creator_fee") {
        return true;
    }
    if event_kind.contains("collect_coin_creator_fee") {
        return true;
    }
    if event_kind.contains("transfer_creator_fees") {
        return true;
    }
    if event_kind.contains("collect_protocol_fee") {
        return true;
    }
    if event_kind.contains("collect_fund_fee") {
        return true;
    }
    if event_kind.contains("collect_fee") {
        return true;
    }
    if event_kind.contains("claim_fee") {
        return true;
    }
    if event_kind.contains("withdraw_protocol_fees") {
        return true;
    }
    if event_kind.contains(".withdraw_pnl") {
        return true;
    }
    if event_kind.contains(".withdraw_srm") {
        return true;
    }
    if event_kind.contains("partner_claim_fee") {
        return true;
    }
    if event_kind.contains("distribute_creator_fees") {
        return true;
    }
    if event_kind.contains("minimum_distributable_fee") {
        return true;
    }
    if event_kind.contains("reserved_fee_recipients_event") {
        return false;
    }
    return false;
}

/// Returns true for reward or incentive events.
pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
    if event_kind.starts_with("meteora_dlmm.")
        && (event_kind.contains("update_fees_and_reward")
            || event_kind.contains("update_reward_duration")
            || event_kind.contains("update_reward_funder"))
    {
        return false;
    }
    if event_kind.starts_with("pump_fees.")
        && (event_kind.contains("social_fee_pda_claimed")
            || event_kind.contains("claim_social_fee_pda"))
    {
        return true;
    }
    if event_kind.contains("reward") {
        return true;
    }
    if event_kind.contains("emission") {
        return true;
    }
    if event_kind.contains("incentive") {
        return true;
    }
    if event_kind.contains("toggle_cashback_enabled") {
        return false;
    }
    if event_kind.contains("cashback") {
        return true;
    }
    if event_kind.contains("volume_accumulator") {
        return true;
    }
    return false;
}

/// Returns true for orderbook or limit-order events that must not become candles.
pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".monitor_step") {
        return true;
    }
    if event_kind.contains(".migrate_to_open_book") {
        return true;
    }
    if event_kind.contains(".admin_cancel_orders") {
        return true;
    }
    if event_kind.contains(".order_place") {
        return true;
    }
    if event_kind.contains(".order_cancel") {
        return true;
    }
    if event_kind.contains(".order_fill") {
        return true;
    }
    if event_kind.contains(".settle_funds") {
        return true;
    }
    if event_kind.contains(".open_limit_order") {
        return true;
    }
    if event_kind.contains(".place_limit_order") {
        return true;
    }
    if event_kind.contains(".cancel_limit_order") {
        return true;
    }
    if event_kind.contains("limit_order_evt") {
        return true;
    }
    if event_kind.contains(".increase_limit_order") {
        return true;
    }
    if event_kind.contains(".decrease_limit_order") {
        return true;
    }
    if event_kind.contains(".close_limit_order") {
        return true;
    }
    if event_kind.contains(".settle_limit_order") {
        return true;
    }
    return false;
}

/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
    if is_pump_swap_anchor_swap_log_event_kind(event_kind) {
        return true;
    }
    if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
        return true;
    }
    if event_kind.starts_with("pump_fees.")
        && (event_kind.contains("create_fee_sharing_config")
            || event_kind.contains("create_social_fee_pda")
            || event_kind.contains("create_donation_fee_pda")
            || event_kind.contains("social_fee_pda_created")
            || event_kind.contains("donation_fee_pda_created")
            || event_kind.contains("initialize_fee_config")
            || event_kind.contains("initialize_fee_program_global")
            || event_kind.contains("initialize_buyback")
            || event_kind.contains("extend_fee_config"))
    {
        return true;
    }
    if event_kind == "raydium_amm_v4.pre_initialize" {
        return true;
    }
    if event_kind.contains(".create_lock_escrow") || event_kind.contains(".create_locker") {
        return true;
    }
    if event_kind.contains(".initialize_bin_array") {
        return true;
    }
    if is_dex_pool_creation_event_kind(event_kind) {
        return true;
    }
    if is_dex_pair_creation_event_kind(event_kind) {
        return true;
    }
    if is_dex_launch_event_kind(event_kind) {
        return true;
    }
    if is_dex_token_mint_event_kind(event_kind) {
        return true;
    }
    if is_dex_token_burn_event_kind(event_kind) {
        return true;
    }
    if is_dex_migration_event_kind(event_kind) {
        return true;
    }
    return false;
}

/// Returns true for launch or bonding-curve creation events.
pub fn is_dex_launch_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".claim_vested_event") {
        return true;
    }
    if event_kind.contains(".claim_vested_token") {
        return true;
    }
    if event_kind.contains(".create_platform_vesting_account") {
        return true;
    }
    if event_kind.contains(".create_vesting_account") {
        return true;
    }
    if event_kind.contains(".create_vesting_event") {
        return true;
    }
    if event_kind.contains("pump_fun.create") {
        return true;
    }
    if event_kind.contains(".launch") {
        return true;
    }
    if event_kind.contains(".create_v2_token") {
        return true;
    }
    if event_kind.contains(".create_bonding_curve") {
        return true;
    }
    return false;
}

/// Returns true for token mint events detected by DEX or launch-surface decoders.
pub fn is_dex_token_mint_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".mint") {
        return true;
    }
    if event_kind.contains(".token_mint") {
        return true;
    }
    return false;
}

/// Returns true for token burn events detected by DEX or launch-surface decoders.
pub fn is_dex_token_burn_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".burn") {
        return true;
    }
    if event_kind.contains(".token_burn") {
        return true;
    }
    return false;
}

/// Returns true for launch-surface or pool migration events.
pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
    if event_kind == "raydium_amm_v4.migrate_to_open_book" {
        return false;
    }
    if event_kind.contains(".migrate_to_open_book") {
        return false;
    }
    if event_kind.contains(".migrate_pool_coin_creator") {
        return false;
    }
    if event_kind.starts_with("meteora_dbc.") && event_kind.contains("metadata") {
        return false;
    }
    if event_kind.contains(".migrate") {
        return true;
    }
    if event_kind.contains(".migration") {
        return true;
    }
    return false;
}

/// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
    if event_kind.starts_with("meteora_dlmm.") {
        if event_kind.contains(".initialize_position") {
            return false;
        }
        if event_kind.contains(".initialize_bin_array") {
            return true;
        }
        if event_kind.contains(".initialize_customizable_permissionless_lb_pair")
            || event_kind.contains(".initialize_permission_lb_pair")
            || event_kind.contains(".initialize_lb_pair")
            || event_kind.contains(".lb_pair_create")
            || event_kind.contains(".create_pool")
        {
            return true;
        }
        if event_kind.contains(".initialize") {
            return false;
        }
    }
    if event_kind == "meteora_dbc.evt_initialize_pool_event"
        || event_kind.contains(".initialize_virtual_pool")
    {
        return true;
    }
    if event_kind == "raydium_amm_v4.pre_initialize" {
        return true;
    }
    if event_kind.contains("amm_config") {
        return false;
    }
    if event_kind.contains(".initialize_position") {
        return false;
    }
    if event_kind.contains(".initialize_bin_array") {
        return true;
    }
    if event_kind.contains(".initialize") {
        return true;
    }
    if event_kind.contains(".initialize_with_permission") {
        return true;
    }
    if event_kind.contains(".create_pool") {
        return true;
    }
    if event_kind.contains(".create_customizable_pool") {
        return true;
    }
    if event_kind.contains(".pool_created_event") {
        return true;
    }
    if event_kind.contains(".pool_create_event") {
        return true;
    }
    if event_kind.contains(".create_amm") {
        return true;
    }
    return false;
}

/// Returns true for pair creation events when they are distinguishable from pool creation.
pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".create_pair") {
        return true;
    }
    if event_kind.contains(".pair_create") {
        return true;
    }
    return false;
}

/// Returns true for token-account lifecycle events detected by DEX decoders.
pub fn is_dex_token_account_event_kind(event_kind: &str) -> bool {
    if is_dex_token_account_create_event_kind(event_kind) {
        return true;
    }
    if is_dex_token_account_close_event_kind(event_kind) {
        return true;
    }
    return false;
}

/// Returns true for token-account creation events detected by DEX decoders.
pub fn is_dex_token_account_create_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".create_support_mint_associated") {
        return true;
    }
    if event_kind.contains(".create_ata") {
        return true;
    }
    if event_kind.contains(".init_account") {
        return true;
    }
    return false;
}

/// Returns true for token-account close events detected by DEX decoders.
pub fn is_dex_token_account_close_event_kind(event_kind: &str) -> bool {
    if event_kind.contains(".close_account") {
        return true;
    }
    return false;
}

/// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
    if is_meteora_dlmm_pool_lifecycle_event_kind(event_kind) {
        return false;
    }
    if is_meteora_dlmm_admin_event_kind(event_kind) {
        return true;
    }
    if event_kind.starts_with("meteora_dbc.")
        && (event_kind.contains("config")
            || event_kind.contains("operator")
            || event_kind.contains("metadata")
            || event_kind.contains("pool_creator")
            || event_kind == "meteora_dbc.transfer_pool_creator")
    {
        return true;
    }
    if is_pump_swap_admin_event_kind(event_kind) {
        return true;
    }
    if event_kind.starts_with("pump_fees.")
        && (event_kind.contains("authority")
            || event_kind.contains("admin")
            || event_kind.contains("config")
            || event_kind.contains("tier")
            || event_kind.contains("disable")
            || event_kind.contains("reset_fee_sharing")
            || event_kind.contains("update_fee_shares")
            || event_kind.contains("set_claim_rate_limit")
            || event_kind.contains("buyback_claim_rate_limit"))
    {
        return true;
    }
    if event_kind.contains(".admin_cancel_orders") {
        return false;
    }
    if event_kind.contains(".monitor_step") {
        return false;
    }
    if event_kind.contains(".migrate_to_open_book") {
        return false;
    }
    if event_kind.contains(".migrate_pool_coin_creator") {
        return true;
    }
    if event_kind.contains(".close_platform_global_access") {
        return true;
    }
    if event_kind.contains(".create_platform_global_access") {
        return true;
    }
    if event_kind.contains(".create_operation_account") {
        return true;
    }
    if event_kind.contains("platform_curve_param") {
        return true;
    }
    if event_kind.contains(".initialize_with_permission") {
        return false;
    }
    if event_kind.contains(".lock_liquidity") {
        return true;
    }
    if event_kind.contains("admin") {
        return true;
    }
    if event_kind.contains("config") {
        return true;
    }
    if event_kind.contains("permission") {
        return true;
    }
    if event_kind.contains("set_") {
        return true;
    }
    if event_kind.contains("toggle_") {
        return true;
    }
    if event_kind.contains(".disable") {
        return true;
    }
    if event_kind.contains(".extend_account") {
        return true;
    }
    if event_kind.contains(".add_quote_mint") {
        return true;
    }
    if event_kind.contains(".remove_quote_mint") {
        return true;
    }
    if event_kind.contains("reserved_fee_recipients") {
        return true;
    }
    if event_kind.contains("update_") {
        return true;
    }
    return false;
}

/// Returns true when a decoded payload contains at least one direct amount or price field.
///
/// This is a conservative payload-level check. It does not inspect transaction
/// token balance deltas and is intended for protocol decoders that cannot yet
/// produce a deterministic materializable swap payload.
pub(crate) fn decoded_payload_has_trade_amount_or_price_payload(
    payload: &serde_json::Value,
) -> bool {
    return value_contains_any_non_null_key(
        payload,
        &[
            "baseAmountRaw",
            "base_amount_raw",
            "baseAmount",
            "amountBase",
            "amountInBase",
            "quoteAmountRaw",
            "quote_amount_raw",
            "quoteAmount",
            "amountQuote",
            "amountOutQuote",
            "amountIn",
            "amountOut",
            "priceQuotePerBase",
            "price_quote_per_base",
            "quotePerBase",
            "lastPriceQuotePerBase",
        ],
    );
}

/// Returns true when a decoded payload is marked as a trade candidate.
///
/// Explicit payload metadata wins over event-kind inference. This allows
/// incomplete decoded events, such as incomplete PumpSwap trades, to opt out.
pub fn is_decoded_event_trade_candidate(event_kind: &str, payload: &serde_json::Value) -> bool {
    let trade_candidate_option =
        extract_top_level_bool_by_candidate_keys(payload, &["tradeCandidate", "trade_candidate"]);
    if let Some(trade_candidate) = trade_candidate_option {
        return trade_candidate;
    }
    let event_category_option =
        extract_string_by_candidate_keys(payload, &["eventCategory", "event_category"]);
    if let Some(event_category) = event_category_option {
        return event_category.as_str() == DexEventCategory::Trade.as_str();
    }
    return is_dex_trade_event_kind(event_kind);
}

/// Returns true when a decoded payload can be materialized as a candle candidate.
pub fn is_decoded_event_candle_candidate(event_kind: &str, payload: &serde_json::Value) -> bool {
    let candle_candidate_option =
        extract_top_level_bool_by_candidate_keys(payload, &["candleCandidate", "candle_candidate"]);
    if let Some(candle_candidate) = candle_candidate_option {
        return candle_candidate;
    }
    if !is_decoded_event_trade_candidate(event_kind, payload) {
        return false;
    }
    return is_dex_candle_candidate_event_kind(event_kind);
}

/// Enriches a decoded payload with non-destructive classification metadata.
pub fn enrich_dex_decoded_payload(
    protocol_name: &str,
    event_kind: &str,
    payload_json: serde_json::Value,
) -> serde_json::Value {
    let event_category = classify_dex_event_category_code(event_kind);
    let event_lifecycle_kind = classify_dex_event_lifecycle_kind_code(event_kind);
    let mut object = match payload_json {
        serde_json::Value::Object(object) => object,
        other => {
            let mut object = serde_json::Map::new();
            object.insert("rawPayload".to_owned(), other);
            object
        },
    };
    let payload_snapshot = serde_json::Value::Object(object.clone());
    let explicit_trade_candidate = extract_top_level_bool_by_candidate_keys(
        &payload_snapshot,
        &["tradeCandidate", "trade_candidate"],
    );
    let trade_candidate = match explicit_trade_candidate {
        Some(trade_candidate) => trade_candidate,
        None => is_dex_trade_event_kind(event_kind),
    };
    let explicit_candle_candidate = extract_top_level_bool_by_candidate_keys(
        &payload_snapshot,
        &["candleCandidate", "candle_candidate"],
    );
    let candle_candidate = match explicit_candle_candidate {
        Some(candle_candidate) => candle_candidate,
        None => {
            if !trade_candidate {
                false
            } else {
                is_dex_candle_candidate_event_kind(event_kind)
            }
        },
    };
    let transaction_failed = match extract_top_level_bool_by_candidate_keys(
        &payload_snapshot,
        &["transactionFailed", "transaction_failed"],
    ) {
        Some(transaction_failed) => transaction_failed,
        None => false,
    };
    let event_actionability =
        classify_dex_event_actionability_code(event_kind, trade_candidate, transaction_failed);
    let non_trade_useful = event_actionability == DexEventActionability::NonTradeUseful.as_str();
    json_insert_string_if_missing(&mut object, "protocolName", protocol_name);
    json_insert_string_if_missing(&mut object, "eventKind", event_kind);
    json_insert_string_if_missing(&mut object, "eventCategory", event_category);
    json_insert_string_if_missing(&mut object, "eventLifecycleKind", event_lifecycle_kind);
    json_insert_string_if_missing(&mut object, "eventActionability", event_actionability);
    json_insert_bool_if_missing(&mut object, "nonTradeUseful", non_trade_useful);
    json_insert_bool_if_missing(&mut object, "tradeCandidate", trade_candidate);
    json_insert_bool_if_missing(&mut object, "candleCandidate", candle_candidate);
    json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 2);
    if !trade_candidate {
        if is_dex_trade_event_kind(event_kind) {
            json_insert_string_if_missing(&mut object, "skipTradeReason", "non_actionable_trade");
        } else {
            json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event");
        }
    } else if !candle_candidate {
        json_insert_string_if_missing(
            &mut object,
            "skipCandleReason",
            "route_or_multihop_event_requires_leg_resolution",
        );
    }
    return serde_json::Value::Object(object);
}

/// Enriches a decoded payload and serializes it as JSON.
pub fn enrich_and_serialize_dex_decoded_payload(
    protocol_name: &str,
    event_kind: &str,
    payload_json: serde_json::Value,
) -> Result<std::string::String, crate::Error> {
    let enriched_payload = enrich_dex_decoded_payload(protocol_name, event_kind, payload_json);
    let payload_json_result = serde_json::to_string(&enriched_payload);
    match payload_json_result {
        Ok(payload_json) => return Ok(payload_json),
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot serialize enriched decoded payload for '{}': {}",
                event_kind, error
            )));
        },
    }
}

/// Parses, enriches and serializes a decoded payload.
pub fn enrich_serialized_dex_decoded_payload(
    protocol_name: &str,
    event_kind: &str,
    payload_json: &str,
) -> Result<std::string::String, crate::Error> {
    let payload_value_result = serde_json::from_str::<serde_json::Value>(payload_json);
    let payload_value = match payload_value_result {
        Ok(payload_value) => payload_value,
        Err(error) => {
            return Err(crate::Error::Json(format!(
                "cannot parse decoded payload for '{}': {}",
                event_kind, error
            )));
        },
    };
    return enrich_and_serialize_dex_decoded_payload(protocol_name, event_kind, payload_value);
}

fn json_insert_string_if_missing(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: &str,
) {
    if object.contains_key(key) {
        return;
    }
    object.insert(key.to_owned(), serde_json::Value::String(value.to_owned()));
}

fn json_insert_bool_if_missing(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: bool,
) {
    if object.contains_key(key) {
        return;
    }
    object.insert(key.to_owned(), serde_json::Value::Bool(value));
}

fn json_insert_i64_if_missing(
    object: &mut serde_json::Map<std::string::String, serde_json::Value>,
    key: &str,
    value: i64,
) {
    if object.contains_key(key) {
        return;
    }
    object.insert(key.to_owned(), serde_json::Value::Number(serde_json::Number::from(value)));
}

fn value_contains_any_non_null_key(value: &serde_json::Value, candidate_keys: &[&str]) -> bool {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            let candidate_value_option = object.get(*candidate_key);
            if let Some(candidate_value) = candidate_value_option {
                if !candidate_value.is_null() {
                    if let Some(text) = candidate_value.as_str() {
                        if text.trim().is_empty() {
                            continue;
                        }
                    }
                    return true;
                }
            }
        }
        for nested_value in object.values() {
            if value_contains_any_non_null_key(nested_value, candidate_keys) {
                return true;
            }
        }
        return false;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            if value_contains_any_non_null_key(nested_value, candidate_keys) {
                return true;
            }
        }
    }
    return false;
}

fn extract_top_level_bool_by_candidate_keys(
    payload: &serde_json::Value,
    candidate_keys: &[&str],
) -> std::option::Option<bool> {
    let object = match payload.as_object() {
        Some(object) => object,
        None => return None,
    };
    for candidate_key in candidate_keys {
        let value_option = object.get(*candidate_key);
        let value = match value_option {
            Some(value) => value,
            None => continue,
        };
        if let Some(value_bool) = value.as_bool() {
            return Some(value_bool);
        }
        if let Some(value_i64) = value.as_i64() {
            return Some(value_i64 != 0);
        }
        if let Some(value_u64) = value.as_u64() {
            return Some(value_u64 != 0);
        }
        if let Some(value_text) = value.as_str() {
            let normalized = value_text.trim().to_ascii_lowercase();
            if normalized.as_str() == "true" {
                return Some(true);
            }
            if normalized.as_str() == "false" {
                return Some(false);
            }
            if normalized.as_str() == "1" {
                return Some(true);
            }
            if normalized.as_str() == "0" {
                return Some(false);
            }
        }
    }
    return None;
}

fn extract_string_by_candidate_keys(
    value: &serde_json::Value,
    candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
    if let Some(object) = value.as_object() {
        for candidate_key in candidate_keys {
            let direct_option = object.get(*candidate_key);
            if let Some(direct) = direct_option {
                let direct_text_option = direct.as_str();
                if let Some(direct_text) = direct_text_option {
                    return Some(direct_text.to_string());
                }
            }
        }
        for nested_value in object.values() {
            let nested_result = extract_string_by_candidate_keys(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
        return None;
    }
    if let Some(array) = value.as_array() {
        for nested_value in array {
            let nested_result = extract_string_by_candidate_keys(nested_value, candidate_keys);
            if nested_result.is_some() {
                return nested_result;
            }
        }
    }

    return None;
}

#[cfg(test)]
mod tests {
    #[test]
    fn classifies_swap_events_as_trade_candidates() {
        assert_eq!(
            super::classify_dex_event_category_code("raydium_cpmm.swap_base_input"),
            "trade"
        );
        assert_eq!(
            super::classify_dex_event_category_code("raydium_cpmm.swap_base_output"),
            "trade"
        );
        assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap"), "trade");
        assert_eq!(super::classify_dex_event_category_code("raydium_clmm.swap_v2"), "trade");
        assert_eq!(super::classify_dex_event_category_code("meteora_dbc.swap2"), "trade");
        assert_eq!(super::classify_dex_event_category_code("raydium_clmm.exact_output"), "trade");
        assert_eq!(super::classify_dex_event_category_code("pump_fun.buy"), "trade");
        assert_eq!(super::classify_dex_event_category_code("pump_fun.buy_v2"), "trade");
        assert_eq!(super::classify_dex_event_category_code("pump_fun.sell_v2"), "trade");
        assert_eq!(super::classify_dex_event_category_code("pump_fun.trade_event"), "trade");
        assert!(super::is_dex_trade_event_kind("raydium_cpmm.swap_base_input"));
        assert!(super::is_dex_trade_event_kind("meteora_dbc.swap2"));
        assert!(super::is_dex_candle_candidate_event_kind("raydium_cpmm.swap_base_input"));
        assert!(super::is_dex_candle_candidate_event_kind("meteora_dbc.swap2"));
    }

    #[test]
    fn classifies_router_swap_as_trade_but_not_direct_candle_candidate() {
        assert_eq!(
            super::classify_dex_event_category_code("raydium_clmm.swap_router_base_in"),
            "trade"
        );
        assert!(super::is_dex_trade_event_kind("raydium_clmm.swap_router_base_in"));
        assert!(!super::is_dex_candle_candidate_event_kind("raydium_clmm.swap_router_base_in"));
    }

    #[test]
    fn classifies_fee_reward_liquidity_and_lifecycle_events() {
        assert_eq!(
            super::classify_dex_event_category_code("raydium_cpmm.collect_creator_fee"),
            "fee"
        );
        assert_eq!(
            super::classify_dex_event_category_code("raydium_clmm.collect_protocol_fee"),
            "fee"
        );
        assert_eq!(
            super::classify_dex_event_category_code("raydium_clmm.set_reward_params"),
            "reward"
        );
        assert_eq!(
            super::classify_dex_event_category_code("pump_swap.toggle_cashback_enabled"),
            "admin"
        );
        assert!(!super::is_dex_reward_event_kind("pump_swap.toggle_cashback_enabled"));
        assert!(super::is_dex_admin_event_kind("pump_swap.toggle_cashback_enabled"));
        assert_eq!(
            super::classify_dex_event_category_code("pump_swap.migrate_pool_coin_creator"),
            "admin"
        );
        assert!(!super::is_dex_pool_lifecycle_event_kind("pump_swap.migrate_pool_coin_creator"));
        assert_eq!(
            super::classify_dex_event_category_code("raydium_clmm.increase_liquidity_v2"),
            "liquidity"
        );
        assert_eq!(
            super::classify_dex_event_category_code("raydium_cpmm.initialize"),
            "pool_lifecycle"
        );
        assert_eq!(
            super::classify_dex_event_category_code("raydium_amm_v4.pre_initialize"),
            "pool_lifecycle"
        );
    }

    #[test]
    fn classifies_fine_grained_non_trade_lifecycle_kinds() {
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("raydium_cpmm.initialize"),
            "pool_creation"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("raydium_amm_v4.pre_initialize"),
            "pool_creation"
        );
        assert_eq!(super::classify_dex_event_lifecycle_kind_code("pump_fun.create"), "launch");
        assert_eq!(
            crate::dex_event_classification::classify_dex_event_category_code(
                "raydium_amm_v4.migrate_to_open_book",
            ),
            "unknown",
        );
        assert_eq!(
            crate::dex_event_classification::classify_dex_event_lifecycle_kind_code(
                "raydium_amm_v4.migrate_to_open_book",
            ),
            "unknown",
        );
        assert!(crate::dex_event_classification::is_dex_orderbook_event_kind(
            "raydium_amm_v4.migrate_to_open_book",
        ));
        assert!(!crate::dex_event_classification::is_dex_pool_lifecycle_event_kind(
            "raydium_amm_v4.migrate_to_open_book",
        ));
        assert!(!crate::dex_event_classification::is_dex_migration_event_kind(
            "raydium_amm_v4.migrate_to_open_book",
        ));
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("meteora_dbc.migrate"),
            "migration"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("raydium_clmm.increase_liquidity_v2"),
            "liquidity_add"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("raydium_clmm.decrease_liquidity_v2"),
            "liquidity_remove"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code(
                "raydium_clmm.increase_liquidity_v2",
                false,
                false,
            ),
            "non_trade_useful"
        );
    }

    #[test]
    fn classifies_meteora_dlmm_fee_reward_admin_without_multi_target_overlap() {
        assert!(super::is_dex_fee_event_kind("meteora_dlmm.withdraw_protocol_fee"));
        assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.withdraw_protocol_fee"));
        assert!(!super::is_dex_liquidity_remove_event_kind("meteora_dlmm.withdraw_protocol_fee"));
        assert!(!super::is_dex_fee_event_kind("meteora_dlmm.close_claim_fee_operator_account"));
        assert!(super::is_dex_admin_event_kind("meteora_dlmm.close_claim_fee_operator_account"));
        assert!(super::is_dex_reward_event_kind("meteora_dlmm.initialize_reward"));
        assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.initialize_reward"));
        assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_fees_and_rewards"));
        assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_fees_and_rewards"));
        assert!(!super::is_dex_reward_event_kind("meteora_dlmm.update_reward_duration_event"));
        assert!(super::is_dex_admin_event_kind("meteora_dlmm.update_reward_duration_event"));
        assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.position_create_event"));
        assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.position_create_event"));
        assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.increase_position_length"));
        assert!(!super::is_dex_liquidity_event_kind("meteora_dlmm.increase_position_length"));
        assert!(!super::is_dex_admin_event_kind("meteora_dlmm.update_position_operator"));
        assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.close_bin_array"));
        assert!(super::is_dex_admin_event_kind("meteora_dlmm.create_operator_account"));
        assert!(super::is_dex_admin_event_kind(
            "meteora_dlmm.initialize_preset_parameter_v2"
        ));
        assert!(super::is_dex_admin_event_kind("meteora_dlmm.instruction_audit"));
        assert!(!super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.instruction_audit"));
        assert!(!super::is_dex_fee_event_kind("meteora_dlmm.instruction_audit"));
        assert!(!super::is_dex_reward_event_kind("meteora_dlmm.instruction_audit"));
        assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.rebalancing_event"));
        assert!(super::is_dex_liquidity_event_kind("meteora_dlmm.remove_all_liquidity"));
        assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap_event"));
        assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap_event"));
        assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap_event"));
        assert!(!super::is_dex_trade_event_kind("meteora_dlmm.swap2_evt"));
        assert!(!super::is_dex_candle_candidate_event_kind("meteora_dlmm.swap2_evt"));
        assert!(super::is_dex_pool_lifecycle_event_kind("meteora_dlmm.swap2_evt"));
    }
    #[test]
    fn enriched_payload_keeps_existing_fields() {
        let payload_json = serde_json::json!({
            "eventCategory": "custom",
            "amountIn": "10"
        });
        let enriched_payload = super::enrich_dex_decoded_payload(
            "raydium_cpmm",
            "raydium_cpmm.swap_base_input",
            payload_json,
        );

        let object_option = enriched_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected enriched payload object");
            },
        };
        assert_eq!(
            object.get("eventCategory"),
            Some(&serde_json::Value::String("custom".to_owned()))
        );
        assert_eq!(
            object.get("protocolName"),
            Some(&serde_json::Value::String("raydium_cpmm".to_owned()))
        );
        assert_eq!(
            object.get("eventKind"),
            Some(&serde_json::Value::String("raydium_cpmm.swap_base_input".to_owned()))
        );
        assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(true)));
        assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(true)));
        assert_eq!(
            object.get("eventLifecycleKind"),
            Some(&serde_json::Value::String("trade_swap".to_owned()))
        );
        assert_eq!(
            object.get("eventActionability"),
            Some(&serde_json::Value::String("trade_candidate".to_owned()))
        );
    }

    #[test]
    fn enriched_non_trade_payload_is_visible_but_not_trade_candidate() {
        let enriched_payload = super::enrich_dex_decoded_payload(
            "raydium_clmm",
            "raydium_clmm.increase_liquidity_v2",
            serde_json::json!({}),
        );
        let object_option = enriched_payload.as_object();
        let object = match object_option {
            Some(object) => object,
            None => {
                panic!("expected enriched payload object");
            },
        };
        assert_eq!(
            object.get("eventCategory"),
            Some(&serde_json::Value::String("liquidity".to_owned()))
        );
        assert_eq!(
            object.get("eventLifecycleKind"),
            Some(&serde_json::Value::String("liquidity_add".to_owned()))
        );
        assert_eq!(
            object.get("eventActionability"),
            Some(&serde_json::Value::String("non_trade_useful".to_owned()))
        );
        assert_eq!(object.get("nonTradeUseful"), Some(&serde_json::Value::Bool(true)));
        assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
        assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
    }

    #[test]
    fn decoded_event_payload_candidate_flags_win_over_event_kind() {
        let payload_json = serde_json::json!({
            "tradeCandidate": false,
            "candleCandidate": false
        });
        assert!(!super::is_decoded_event_trade_candidate("pump_swap.buy", &payload_json));
        assert!(!super::is_decoded_event_candle_candidate("pump_swap.buy", &payload_json));
    }

    #[test]
    fn detects_direct_amount_or_price_payload_recursively() {
        let payload_json = serde_json::json!({
            "nested": {
                "quoteAmountRaw": "2500"
            }
        });
        assert!(super::decoded_payload_has_trade_amount_or_price_payload(&payload_json));
        let empty_payload_json = serde_json::json!({
            "nested": {
                "quoteAmountRaw": ""
            }
        });
        assert!(!super::decoded_payload_has_trade_amount_or_price_payload(&empty_payload_json));
    }

    #[test]
    fn classifies_dlmm_claim_fee2_as_fee_collection() {
        assert_eq!(super::classify_dex_event_category_code("meteora_dlmm.claim_fee2"), "fee");
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.claim_fee2"),
            "fee_collection"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code("meteora_dlmm.claim_fee2", false, false,),
            "non_trade_useful"
        );
    }

    #[test]
    fn classifies_dlmm_add_remove_liquidity_and_positions_as_non_trade_useful() {
        assert_eq!(
            super::classify_dex_event_category_code("meteora_dlmm.add_liquidity"),
            "liquidity"
        );
        assert_eq!(
            super::classify_dex_event_category_code("meteora_dlmm.remove_liquidity"),
            "liquidity"
        );
        assert_eq!(
            super::classify_dex_event_category_code("meteora_dlmm.initialize_position"),
            "pool_lifecycle"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("meteora_dlmm.initialize_position"),
            "position_open"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code(
                "meteora_dlmm.add_liquidity",
                false,
                false,
            ),
            "non_trade_useful"
        );
    }

    #[test]
    fn classifies_dlmm_bin_array_initialization_as_pool_lifecycle() {
        assert_eq!(
            super::classify_dex_event_category_code("meteora_dlmm.initialize_bin_array"),
            "pool_lifecycle"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code(
                "meteora_dlmm.initialize_bin_array",
                false,
                false,
            ),
            "non_trade_useful"
        );
    }

    #[test]
    fn classifies_damm_v1_lock_events_as_non_trade_useful() {
        assert_eq!(
            super::classify_dex_event_category_code("meteora_damm_v1.create_lock_escrow"),
            "pool_lifecycle"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("meteora_damm_v1.create_lock_escrow"),
            "pool_creation"
        );
        assert_eq!(
            super::classify_dex_event_category_code("meteora_damm_v1.lock_liquidity"),
            "admin"
        );
        assert_eq!(
            super::classify_dex_event_lifecycle_kind_code("meteora_damm_v1.lock_liquidity"),
            "admin_config"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code(
                "meteora_damm_v1.lock_liquidity",
                false,
                false,
            ),
            "non_trade_useful"
        );
    }

    #[test]
    fn classifies_initialize_with_permission_as_lifecycle_only() {
        let event_kind = "raydium_cpmm.initialize_with_permission";
        assert!(super::is_dex_pool_lifecycle_event_kind(event_kind));
        assert!(!super::is_dex_admin_event_kind(event_kind));
        assert_eq!(super::classify_dex_event_category_code(event_kind), "pool_lifecycle");
        assert_eq!(super::classify_dex_event_lifecycle_kind_code(event_kind), "pool_creation");
    }

    #[test]
    fn classifies_audit_suffix_events_as_informational() {
        assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));
        assert_eq!(
            super::classify_dex_event_category_code("openbook_v2.settle_funds_audit"),
            "informational"
        );
        assert_eq!(
            super::classify_dex_event_actionability_code(
                "openbook_v2.settle_funds_audit",
                false,
                false,
            ),
            "informational"
        );
        assert!(!super::is_dex_trade_event_kind("openbook_v2.order_place_audit"));
    }

    #[test]
    fn classifies_upstream_registry_instruction_match_as_informational() {
        assert!(super::is_dex_informational_event_kind(
            crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND
        ));
        assert_eq!(
            super::classify_dex_event_category_code(
                crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND
            ),
            "informational"
        );
        assert!(!super::is_dex_trade_event_kind(
            crate::UPSTREAM_REGISTRY_INSTRUCTION_MATCH_EVENT_KIND
        ));
    }

    #[test]
    fn classifies_pump_swap_anchor_swap_logs_as_lifecycle_not_trade() {
        assert!(super::is_dex_pool_lifecycle_event_kind("pump_swap.buy_event"));
        assert!(super::is_dex_pool_lifecycle_event_kind("pump_swap.sell_event"));
        assert!(!super::is_dex_trade_event_kind("pump_swap.buy_event"));
        assert!(!super::is_dex_trade_event_kind("pump_swap.sell_event"));
    }

    #[test]
    fn classifies_pump_swap_admin_instructions_and_events_explicitly() {
        assert!(super::is_dex_admin_event_kind("pump_swap.update_fee_config"));
        assert!(super::is_dex_admin_event_kind("pump_swap.update_fee_config_event"));
        assert!(super::is_dex_admin_event_kind("pump_swap.set_coin_creator"));
        assert!(super::is_dex_admin_event_kind("pump_swap.extend_account_event"));
        assert!(!super::is_dex_admin_event_kind("pump_swap.buy_event"));
    }


}
