use super::{
    AbstractClassValueProvenanceNodeV0, AbstractClassValueProvenanceV0, AbstractClassValueV0,
    AbstractPropertyValueCandidateV0, AbstractPropertyValueV0, CascadeContextV0,
    CascadeValueFamilyMemberV0, ClassValueControlFlowBlockV0, ClassValueControlFlowGraphV0,
    ClassValueFlowGraphV0, ClassValueFlowNodeV0, ClassValueFlowTransferV0,
    CompositeClassValueInputV0, ExternalStringTypeFactsV0, KLimitedCallSiteFlowInputV0,
    Lin01ProvenanceSemiringV0, LinearProvenanceV0, MAX_FINITE_CLASS_VALUES,
    NaturalCountProvenanceSemiringV0, OneCfaCallSiteFlowInputV0, ProvenanceSemiringV0,
    SecurityLabelProvenanceSemiringV0, SelectorProjectionCertaintyV0, TropicalProvenanceSemiringV0,
    ViterbiProvenanceSemiringV0, abstract_class_value_from_facts, abstract_class_value_is_subset,
    analyze_class_value_control_flow_graph, analyze_class_value_flow,
    analyze_class_value_flow_incremental, analyze_class_value_flow_incremental_batch_with_reuse,
    analyze_class_value_flow_incremental_with_database,
    analyze_class_value_flow_incremental_with_reuse, analyze_k_limited_call_site_flows,
    analyze_one_cfa_call_site_flows, bottom_class_value, cascade_family_context_values,
    cascade_value_for_context, char_inclusion_class_value, composite_class_value,
    concatenate_abstract_class_values, concatenate_reduced_class_value_products,
    derive_cascade_restriction_maps_v0, derive_selector_projection_certainty, exact_class_value,
    finite_set_class_value, finite_values_from_facts, intersect_abstract_class_values,
    intersect_reduced_class_value_products, iterate_reduced_class_value_product_constraints,
    join_abstract_class_values, join_reduced_class_value_products,
    m4_alpha_provenance_semiring_law_reports_v0, narrow_abstract_property_value_for_pseudo_state,
    prefix_class_value, prefix_suffix_class_value, project_abstract_value_selectors,
    reduce_class_value_product, reduced_abstract_class_value_from_facts,
    reduced_class_value_derivation_from_facts, reduced_class_value_product_is_subset,
    reduced_class_value_product_matches_string, reduced_value_domain_kind_from_facts,
    selector_certainty_from_facts, selector_certainty_shape_kind_from_facts,
    selector_certainty_shape_label_from_facts, suffix_class_value,
    summarize_abstract_class_value_provenance_tree, summarize_belief_propagation_iteration_v0,
    summarize_cascade_value_family_v0, summarize_omena_abstract_value_domain,
    summarize_omena_abstract_value_flow_analysis, summarize_reduced_class_value_product,
    top_class_value, value_certainty_from_facts, value_certainty_shape_kind_from_facts,
    value_certainty_shape_label_from_facts,
};
use omena_incremental::OmenaIncrementalDatabaseV0;
use std::collections::BTreeMap;

#[test]
fn summarizes_domain_boundary_contract() {
    let summary = summarize_omena_abstract_value_domain();

    assert_eq!(summary.schema_version, "0");
    assert_eq!(summary.product, "omena-abstract-value.domain");
    assert_eq!(summary.max_finite_class_values, MAX_FINITE_CLASS_VALUES);
    assert!(summary.domain_kinds.contains(&"exact"));
    assert!(summary.domain_kinds.contains(&"composite"));
    assert!(summary.reduced_product_structure_ready);
    assert_eq!(
        summary.reduced_product_axes,
        vec!["prefix", "suffix", "charInclusion", "lengthLowerBound"]
    );
    assert!(
        summary
            .reduced_product_operations
            .contains(&"matchesString")
    );
    assert!(
        summary
            .reduced_product_consumers
            .contains(&"semanticReachability")
    );
    assert!(
        summary
            .selector_projection_certainties
            .contains(&"inferred")
    );
    assert!(summary.provenance_tree_ready);
    assert!(summary.provenance_tree_scopes.contains(&"reducedProduct"));
    assert!(summary.provenance_tree_scopes.contains(&"flowResult"));
    assert!(summary.cascade_family_substrate_ready);
    assert_eq!(
        summary.cascade_family_framing,
        "framingNeutralCascadeFamily"
    );
    assert_eq!(
        summary.cascade_family_claim_level,
        "researchStagedSubstrate"
    );

    let flow_summary = summarize_omena_abstract_value_flow_analysis();
    assert_eq!(flow_summary.schema_version, "0");
    assert_eq!(flow_summary.product, "omena-abstract-value.flow-analysis");
    assert_eq!(flow_summary.context_sensitivity, "1-cfa");
    assert_eq!(flow_summary.incremental_engine, "omena-incremental");
    assert!(flow_summary.analysis_scopes.contains(&"multiContextBatch"));
    assert!(flow_summary.analysis_scopes.contains(&"callSiteBatch"));
    assert!(
        flow_summary
            .analysis_scopes
            .contains(&"kLimitedCallSiteBatch")
    );
    assert!(flow_summary.analysis_scopes.contains(&"controlFlowGraph"));
    assert_eq!(
        flow_summary.reuse_policy,
        "reuse previous context analysis when its omena-incremental plan is clean"
    );
    assert!(flow_summary.transfer_kinds.contains(&"join"));
    assert!(flow_summary.transfer_kinds.contains(&"concatFacts"));
}

#[test]
fn summarizes_framing_neutral_cascade_value_family_substrate() {
    let members = vec![
        CascadeValueFamilyMemberV0 {
            context: CascadeContextV0 {
                id: "root".to_string(),
                parent_id: None,
                selectors: vec![".card".to_string()],
                conditions: Vec::new(),
                layers: vec!["components".to_string()],
            },
            value: AbstractPropertyValueV0::Exact {
                property_name: "color".to_string(),
                value: "black".to_string(),
                pseudo_state: None,
            },
        },
        CascadeValueFamilyMemberV0 {
            context: CascadeContextV0 {
                id: "hover".to_string(),
                parent_id: Some("root".to_string()),
                selectors: vec![".card:hover".to_string()],
                conditions: vec![":hover".to_string()],
                layers: vec!["components".to_string()],
            },
            value: AbstractPropertyValueV0::Exact {
                property_name: "color".to_string(),
                value: "white".to_string(),
                pseudo_state: Some("hover".to_string()),
            },
        },
    ];
    let restrictions = derive_cascade_restriction_maps_v0(members.as_slice());
    let family = summarize_cascade_value_family_v0("color", members, restrictions);

    assert_eq!(family.product, "omena-abstract-value.cascade-value-family");
    assert_eq!(family.framing, "framingNeutralCascadeFamily");
    assert_eq!(family.claim_level, "researchStagedSubstrate");
    assert_eq!(
        family.supported_readings,
        vec!["presheafCompatible", "cosheafCompatible"]
    );
    assert_eq!(family.context_value_count, 2);
    assert_eq!(family.restriction_map_count, 1);
    assert!(family.property_consistent);
    assert_eq!(family.dangling_restriction_count, 0);
    assert!(!family.theorem_claimed);
    assert_eq!(
        cascade_value_for_context(&family, "hover"),
        Some(&AbstractPropertyValueV0::Exact {
            property_name: "color".to_string(),
            value: "white".to_string(),
            pseudo_state: Some("hover".to_string()),
        })
    );
    assert_eq!(cascade_family_context_values(&family).len(), 2);
}

#[test]
fn normalizes_finite_sets_to_bottom_exact_or_sorted_unique_values() {
    assert_eq!(
        finite_set_class_value(Vec::<String>::new()),
        AbstractClassValueV0::Bottom
    );
    assert_eq!(
        finite_set_class_value(["button"]),
        exact_class_value("button")
    );
    assert_eq!(
        finite_set_class_value(["card", "button", "card"]),
        AbstractClassValueV0::FiniteSet {
            values: vec!["button".to_string(), "card".to_string()]
        }
    );
}

#[test]
fn maps_external_string_facts_to_stable_value_certainty_labels() {
    let exact = external_facts("exact").with_values(["button"]);
    assert_eq!(
        abstract_class_value_from_facts(&exact),
        exact_class_value("button")
    );
    assert_eq!(value_certainty_from_facts(&exact), Some("exact"));
    assert_eq!(value_certainty_shape_kind_from_facts(&exact), "exact");
    assert_eq!(value_certainty_shape_label_from_facts(&exact), "exact");
    assert_eq!(
        finite_values_from_facts(&exact),
        Some(vec!["button".to_string()])
    );

    let finite = external_facts("finiteSet").with_values(["card", "button", "card"]);
    assert_eq!(value_certainty_from_facts(&finite), Some("inferred"));
    assert_eq!(
        value_certainty_shape_kind_from_facts(&finite),
        "boundedFinite"
    );
    assert_eq!(
        value_certainty_shape_label_from_facts(&finite),
        "bounded finite (2)"
    );
    assert_eq!(selector_certainty_from_facts(&finite, 1, 3), "inferred");
    assert_eq!(
        selector_certainty_shape_label_from_facts(&finite, 1, 3),
        "bounded selector set (1)"
    );
}

#[test]
fn maps_constrained_external_string_facts_to_stable_shape_labels() {
    let edge = external_facts("constrained")
        .with_constraint_kind("prefixSuffix")
        .with_prefix("btn-")
        .with_suffix("-active")
        .with_min_len(11);

    assert_eq!(
        abstract_class_value_from_facts(&edge),
        AbstractClassValueV0::PrefixSuffix {
            prefix: "btn-".to_string(),
            suffix: "-active".to_string(),
            min_length: 11,
            provenance: None,
        }
    );
    assert_eq!(value_certainty_from_facts(&edge), Some("inferred"));
    assert_eq!(value_certainty_shape_kind_from_facts(&edge), "constrained");
    assert_eq!(
        value_certainty_shape_label_from_facts(&edge),
        "constrained prefix `btn-` + suffix `-active`"
    );
    assert_eq!(selector_certainty_from_facts(&edge, 1, 3), "inferred");
    assert_eq!(selector_certainty_from_facts(&edge, 3, 3), "inferred");
    assert_eq!(
        selector_certainty_from_facts(&external_facts("top"), 3, 3),
        "possible"
    );
    assert_eq!(
        selector_certainty_shape_kind_from_facts(&edge, 1, 3),
        "constrained"
    );
    assert_eq!(
        selector_certainty_shape_label_from_facts(&edge, 1, 3),
        "constrained edge selector set (1)"
    );
}

#[test]
fn narrows_property_values_to_single_stylesheet_pseudo_state() {
    let candidates = vec![
        property_candidate("color", "black", None),
        property_candidate("color", "white", Some("hover")),
        property_candidate("background", "var(--surface)", Some("hover")),
    ];

    let narrowed =
        narrow_abstract_property_value_for_pseudo_state("color", Some("hover"), &candidates);

    assert_eq!(narrowed.schema_version, "0");
    assert_eq!(
        narrowed.product,
        "omena-abstract-value.property-value-narrowing"
    );
    assert_eq!(narrowed.stylesheet_scope, "singleStylesheet");
    assert_eq!(narrowed.candidate_count, 3);
    assert_eq!(narrowed.matched_candidate_count, 2);
    assert_eq!(
        narrowed.value,
        AbstractPropertyValueV0::FiniteSet {
            property_name: "color".to_string(),
            values: vec!["black".to_string(), "white".to_string()],
            pseudo_states: vec!["hover".to_string()],
        }
    );
}

#[test]
fn narrows_property_values_to_custom_property_reference_annotation_target() {
    let candidates = vec![property_candidate(
        "background",
        "var(--surface)",
        Some("focus"),
    )];

    let narrowed =
        narrow_abstract_property_value_for_pseudo_state("background", Some("focus"), &candidates);

    assert_eq!(
        narrowed.value,
        AbstractPropertyValueV0::CustomPropertyReference {
            property_name: "background".to_string(),
            custom_property_name: "--surface".to_string(),
            pseudo_state: Some("focus".to_string()),
        }
    );
}

#[test]
fn widens_large_finite_sets_to_composite_when_edges_survive() {
    let values = (0..=MAX_FINITE_CLASS_VALUES)
        .map(|index| format!("btn-{index}-active"))
        .collect::<Vec<_>>();

    let value = finite_set_class_value(values);

    assert_eq!(
        value,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-0-active".len()),
            must_chars: "-abceintv".to_string(),
            may_chars: "-012345678abceintv".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::FiniteSetWideningComposite),
        }
    );
}

#[test]
fn builds_char_inclusion_and_composite_values_with_normalized_chars() {
    assert_eq!(
        char_inclusion_class_value(
            "ba",
            "cad",
            Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars),
            false,
        ),
        AbstractClassValueV0::CharInclusion {
            must_chars: "ab".to_string(),
            may_chars: "abcd".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::FiniteSetWideningChars),
        }
    );

    assert_eq!(
        composite_class_value(CompositeClassValueInputV0 {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: None,
            must_chars: "z".to_string(),
            may_chars: "za".to_string(),
            may_include_other_chars: true,
            provenance: None,
        }),
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-z-active".len()),
            must_chars: "-abceintvz".to_string(),
            may_chars: "-abceintvz".to_string(),
            may_include_other_chars: true,
            provenance: None,
        }
    );
}

#[test]
fn intersects_finite_values_with_constrained_domains() {
    let finite = finite_set_class_value(["btn-primary", "card", "btn-secondary"]);
    let prefix = prefix_class_value("btn-", None);

    assert_eq!(
        intersect_abstract_class_values(&finite, &prefix),
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );

    assert_eq!(
        intersect_abstract_class_values(
            &exact_class_value("card"),
            &prefix_class_value("btn-", None),
        ),
        AbstractClassValueV0::Bottom
    );
}

#[test]
fn intersects_prefix_suffix_and_char_constraints_into_reduced_product() {
    let edge = intersect_abstract_class_values(
        &prefix_class_value("btn-", None),
        &suffix_class_value("-active", None),
    );

    assert_eq!(
        edge,
        AbstractClassValueV0::PrefixSuffix {
            prefix: "btn-".to_string(),
            suffix: "-active".to_string(),
            min_length: "btn-active".len(),
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );

    let reduced = intersect_abstract_class_values(
        &edge,
        &char_inclusion_class_value("ab", "-abceintv", None, false),
    );

    assert_eq!(
        reduced,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-active".len()),
            must_chars: "-abceintv".to_string(),
            may_chars: "-abceintv".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );

    let reduced_with_extra_required_char = intersect_abstract_class_values(
        &edge,
        &char_inclusion_class_value("z", "-abceintvz", None, false),
    );

    assert_eq!(
        reduced_with_extra_required_char,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-z-active".len()),
            must_chars: "-abceintvz".to_string(),
            may_chars: "-abceintvz".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );
}

#[test]
fn preserves_overlapping_prefix_suffix_reduced_product_targets() {
    let edge = intersect_abstract_class_values(
        &prefix_class_value("btn-primary", None),
        &suffix_class_value("primary", None),
    );

    assert_eq!(
        edge,
        AbstractClassValueV0::PrefixSuffix {
            prefix: "btn-primary".to_string(),
            suffix: "primary".to_string(),
            min_length: "btn-primary".len(),
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );

    let selectors = selector_universe(["btn-primary", "btn-secondary", "card-primary"]);
    assert_eq!(
        projected_names(&edge, &selectors),
        vec!["btn-primary".to_string()]
    );
}

#[test]
fn rejects_incompatible_reduced_product_constraints() {
    assert_eq!(
        intersect_abstract_class_values(
            &prefix_class_value("btn-", None),
            &prefix_class_value("card-", None),
        ),
        AbstractClassValueV0::Bottom
    );

    assert_eq!(
        intersect_abstract_class_values(
            &prefix_class_value("btn-", None),
            &char_inclusion_class_value("", "abc", None, false),
        ),
        AbstractClassValueV0::Bottom
    );
}

#[test]
fn treats_closed_empty_character_domain_as_bottom() {
    assert_eq!(
        char_inclusion_class_value("", "", None, false),
        AbstractClassValueV0::Bottom
    );
    assert_eq!(
        intersect_abstract_class_values(
            &prefix_class_value("btn-", None),
            &char_inclusion_class_value("", "", None, false),
        ),
        AbstractClassValueV0::Bottom
    );
}

#[test]
fn reduced_product_laws_hold_over_selector_projection() {
    let selectors = selector_universe([
        "btn-primary",
        "btn-secondary",
        "btn-active",
        "card",
        "card-active",
        "nav-active",
    ]);
    let finite = finite_set_class_value([
        "btn-primary",
        "btn-secondary",
        "card",
        "card-active",
        "missing",
    ]);
    let prefix = prefix_class_value("btn-", None);
    let suffix = suffix_class_value("-active", None);
    let chars = char_inclusion_class_value("ab", "-abceintv", None, false);
    let composite = composite_class_value(CompositeClassValueInputV0 {
        prefix: Some("btn-".to_string()),
        suffix: Some("-active".to_string()),
        min_length: Some("btn-active".len()),
        must_chars: "ab".to_string(),
        may_chars: "-abceintv".to_string(),
        may_include_other_chars: false,
        provenance: None,
    });

    for (left, right) in [
        (&finite, &prefix),
        (&prefix, &suffix),
        (&suffix, &chars),
        (&prefix, &composite),
    ] {
        assert_projection_equivalent(
            &intersect_abstract_class_values(left, right),
            &intersect_abstract_class_values(right, left),
            &selectors,
        );
    }

    for value in [&finite, &prefix, &suffix, &chars, &composite] {
        assert_projection_equivalent(
            &intersect_abstract_class_values(value, value),
            value,
            &selectors,
        );
    }

    assert_eq!(
        intersect_abstract_class_values(&top_class_value(), &finite),
        finite
    );
    assert_eq!(
        intersect_abstract_class_values(&finite, &top_class_value()),
        finite
    );
    assert_eq!(
        intersect_abstract_class_values(&bottom_class_value(), &finite),
        bottom_class_value()
    );
    assert_eq!(
        intersect_abstract_class_values(&finite, &bottom_class_value()),
        bottom_class_value()
    );
}

#[test]
fn exposes_reduced_product_subset_relation_for_composite_domains() {
    let composite = composite_class_value(CompositeClassValueInputV0 {
        prefix: Some("btn-".to_string()),
        suffix: Some("-active".to_string()),
        min_length: Some("btn-active".len()),
        must_chars: "ab".to_string(),
        may_chars: "-abceintv".to_string(),
        may_include_other_chars: false,
        provenance: None,
    });
    let prefix = prefix_class_value("btn-", None);
    let suffix = suffix_class_value("-active", None);
    let chars = char_inclusion_class_value("ab", "-abceintv", None, false);
    let looser_chars = char_inclusion_class_value("a", "-abceintvz", None, false);
    let incompatible_chars = char_inclusion_class_value("z", "-abceintvz", None, false);

    assert!(abstract_class_value_is_subset(&composite, &prefix));
    assert!(abstract_class_value_is_subset(&composite, &suffix));
    assert!(abstract_class_value_is_subset(&composite, &chars));
    assert!(abstract_class_value_is_subset(&composite, &looser_chars));
    assert!(!abstract_class_value_is_subset(
        &composite,
        &incompatible_chars
    ));
    assert!(!abstract_class_value_is_subset(&prefix, &composite));
}

#[test]
fn exposes_reduced_product_as_explicit_axes() {
    let composite = composite_class_value(CompositeClassValueInputV0 {
        prefix: Some("btn-".to_string()),
        suffix: Some("-active".to_string()),
        min_length: Some("btn-active".len()),
        must_chars: "ab".to_string(),
        may_chars: "-abceintv".to_string(),
        may_include_other_chars: false,
        provenance: None,
    });

    let product = summarize_reduced_class_value_product(&composite);

    assert!(product.is_some());
    assert_eq!(
        product.as_ref().map(|summary| summary.schema_version),
        Some("0")
    );
    assert_eq!(
        product.as_ref().map(|summary| summary.product),
        Some("omena-abstract-value.reduced-product")
    );
    assert_eq!(
        product.as_ref().map(|summary| summary.source_value_kind),
        Some("composite")
    );
    assert_eq!(
        product
            .as_ref()
            .and_then(|summary| summary.prefix.as_ref())
            .map(|axis| axis.prefix.as_str()),
        Some("btn-")
    );
    assert_eq!(
        product
            .as_ref()
            .and_then(|summary| summary.suffix.as_ref())
            .map(|axis| axis.suffix.as_str()),
        Some("-active")
    );
    assert_eq!(
        product
            .as_ref()
            .map(|summary| summary.char_inclusion.must_chars.as_str()),
        Some("-abceintv")
    );
    assert_eq!(
        product
            .as_ref()
            .and_then(|summary| summary.char_inclusion.allowed_chars.as_deref()),
        Some("-abceintv")
    );
    assert_eq!(
        product
            .as_ref()
            .map(|summary| summary.char_inclusion.may_include_other_chars),
        Some(false)
    );
    assert_eq!(
        product.as_ref().and_then(|summary| summary.min_length),
        Some("btn-active".len())
    );
    assert_eq!(
        product.as_ref().map(|summary| summary.lower_bound_length),
        Some("btn-active".len())
    );
}

#[test]
fn exposes_reduced_product_domain_algebra() -> Result<(), &'static str> {
    let prefix =
        reduce_class_value_product(&prefix_class_value("btn-", None)).ok_or("prefix product")?;
    let suffix =
        reduce_class_value_product(&suffix_class_value("-active", None)).ok_or("suffix product")?;
    let chars =
        reduce_class_value_product(&char_inclusion_class_value("ab", "-abceintv", None, false))
            .ok_or("char-inclusion product")?;

    let edge = intersect_reduced_class_value_products(&prefix, &suffix)
        .ok_or("prefix-suffix intersection")?;
    let constrained = intersect_reduced_class_value_products(&edge, &chars)
        .ok_or("reduced-product intersection")?;

    assert_eq!(constrained.prefix.as_deref(), Some("btn-"));
    assert_eq!(constrained.suffix.as_deref(), Some("-active"));
    assert_eq!(constrained.allowed_chars.as_deref(), Some("-abceintv"));
    assert!(reduced_class_value_product_matches_string(
        &constrained,
        "btn-active"
    ));
    assert!(!reduced_class_value_product_matches_string(
        &constrained,
        "btn-secondary"
    ));
    assert!(reduced_class_value_product_is_subset(&constrained, &prefix));

    let primary_prefix = reduce_class_value_product(&prefix_class_value("btn-primary-", None))
        .ok_or("primary prefix product")?;
    let secondary_prefix = reduce_class_value_product(&prefix_class_value("btn-secondary-", None))
        .ok_or("secondary prefix product")?;
    let joined = join_reduced_class_value_products(&primary_prefix, &secondary_prefix)
        .ok_or("reduced-product join")?;
    assert_eq!(joined.prefix.as_deref(), Some("btn-"));

    let concatenated = concatenate_reduced_class_value_products(&prefix, &suffix)
        .ok_or("reduced-product concatenation")?;
    assert_eq!(concatenated.prefix.as_deref(), Some("btn-"));
    assert_eq!(concatenated.suffix.as_deref(), Some("-active"));
    Ok(())
}

#[test]
fn iterates_reduced_product_constraints_with_monotone_witness() {
    let summary = iterate_reduced_class_value_product_constraints(&[
        prefix_class_value("btn-", None),
        suffix_class_value("-active", None),
        char_inclusion_class_value("a", "abcde-5intv", None, false),
    ]);

    assert_eq!(summary.schema_version, "0");
    assert_eq!(
        summary.product,
        "omena-abstract-value.reduced-product-iteration"
    );
    assert_eq!(summary.input_count, 3);
    assert_eq!(summary.applied_constraint_count, 3);
    assert_eq!(summary.iteration_count, 3);
    assert!(summary.converged);
    assert!(summary.monotone_witness_valid);
    assert_eq!(summary.result_kind, "composite");
    assert_eq!(
        summary.result_value,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-active".len()),
            must_chars: "-abceintv".to_string(),
            may_chars: "-5abcdeintv".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );
    assert!(summary.final_product.is_some());
    assert!(
        summary
            .steps
            .iter()
            .all(|step| step.operation == "meetReducedProductConstraint")
    );
    assert!(summary.steps.iter().all(|step| step.monotone_with_previous));
}

#[test]
fn belief_propagation_iteration_is_strict_superset_of_reduced_product_iteration() {
    let summary = summarize_belief_propagation_iteration_v0(&[
        prefix_class_value("btn-", None),
        suffix_class_value("-active", None),
        char_inclusion_class_value("a", "abcde-5intv", None, false),
    ]);

    assert_eq!(summary.schema_version, "0");
    assert_eq!(
        summary.product,
        "omena-abstract-value.belief-propagation-iteration"
    );
    assert_eq!(
        summary.algorithm_view,
        "reducedProductConstraintMessagePassing"
    );
    assert_eq!(
        summary.substrate,
        "omena-abstract-value.reduced-product-iteration"
    );
    assert_eq!(summary.input_count, 3);
    assert_eq!(summary.message_count, summary.source_iteration.steps.len());
    assert_eq!(
        summary.iteration_count,
        summary.source_iteration.iteration_count
    );
    assert!(summary.converged);
    assert!(summary.fixed_point_reached);
    assert!(summary.monotone_witness_valid);
    assert_eq!(summary.source_iteration.result_kind, "composite");
    assert!(
        summary
            .messages
            .iter()
            .all(|message| message.operation == "meetReducedProductConstraint")
    );
}

#[test]
fn reduced_product_iteration_collapses_incompatible_axes_to_bottom() {
    let summary = iterate_reduced_class_value_product_constraints(&[
        prefix_class_value("btn-", None),
        prefix_class_value("card-", None),
    ]);

    assert_eq!(summary.result_kind, "bottom");
    assert_eq!(summary.result_value, bottom_class_value());
    assert_eq!(summary.applied_constraint_count, 2);
    assert_eq!(summary.iteration_count, 2);
    assert!(summary.converged);
    assert!(summary.monotone_witness_valid);
    assert!(summary.final_product.is_none());
    assert_eq!(
        summary.steps[1].reason,
        "incompatible reduced-product axes collapse to bottom"
    );
}

#[test]
fn reduced_product_projection_matches_intersected_projection_sets() {
    let selectors = selector_universe([
        "btn-primary",
        "btn-secondary",
        "btn-active",
        "card",
        "card-active",
        "nav-active",
    ]);
    let finite = finite_set_class_value([
        "btn-primary",
        "btn-secondary",
        "card",
        "card-active",
        "missing",
    ]);
    let prefix = prefix_class_value("btn-", None);
    let suffix = suffix_class_value("-active", None);
    let prefix_suffix = intersect_abstract_class_values(&prefix, &suffix);

    assert_eq!(
        projected_names(
            &intersect_abstract_class_values(&finite, &prefix),
            &selectors
        ),
        vec!["btn-primary".to_string(), "btn-secondary".to_string()]
    );
    assert_eq!(
        projected_names(
            &intersect_abstract_class_values(&finite, &prefix),
            &selectors
        ),
        intersect_projected_names(&finite, &prefix, &selectors)
    );
    assert_eq!(
        projected_names(
            &intersect_abstract_class_values(&finite, &prefix_suffix),
            &selectors,
        ),
        intersect_projected_names(&finite, &prefix_suffix, &selectors)
    );
}

#[test]
fn joins_abstract_values_for_branch_merges() {
    assert_eq!(
        join_abstract_class_values(
            &exact_class_value("btn-primary"),
            &exact_class_value("btn-secondary"),
        ),
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );

    assert_eq!(
        join_abstract_class_values(
            &prefix_class_value("btn-primary-", None),
            &prefix_class_value("btn-secondary-", None),
        ),
        prefix_class_value("btn-", Some(AbstractClassValueProvenanceV0::PrefixJoinLcp))
    );

    assert_eq!(
        join_abstract_class_values(
            &prefix_class_value("btn-", None),
            &exact_class_value("btn-primary"),
        ),
        prefix_class_value("btn-", None)
    );
}

#[test]
fn joins_reduced_product_constraints_for_branch_merges() {
    let primary = prefix_suffix_class_value(
        "btn-primary-",
        "-active",
        Some("btn-primary--active".len()),
        None,
    );
    let secondary = prefix_suffix_class_value(
        "btn-secondary-",
        "-active",
        Some("btn-secondary--active".len()),
        None,
    );

    let joined = join_abstract_class_values(&primary, &secondary);

    assert_eq!(
        joined,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-primary--active".len()),
            must_chars: "-abceinrtvy".to_string(),
            may_chars: "-abceinrtvy".to_string(),
            may_include_other_chars: true,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        }
    );
    assert_eq!(
        projected_names(
            &joined,
            &selector_universe([
                "btn--active",
                "btn-primary--active",
                "btn-secondary--active",
                "card-active",
            ]),
        ),
        vec![
            "btn-primary--active".to_string(),
            "btn-secondary--active".to_string(),
        ]
    );
}

#[test]
fn flow_join_preserves_reduced_product_branch_shape() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@variant-active".to_string()),
        nodes: vec![
            flow_assign_node(
                "primary",
                external_facts("constrained")
                    .with_constraint_kind("prefixSuffix")
                    .with_prefix("btn-primary-")
                    .with_suffix("-active")
                    .with_min_len("btn-primary--active".len()),
            ),
            flow_assign_node(
                "secondary",
                external_facts("constrained")
                    .with_constraint_kind("prefixSuffix")
                    .with_prefix("btn-secondary-")
                    .with_suffix("-active")
                    .with_min_len("btn-secondary--active".len()),
            ),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["primary".to_string(), "secondary".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };

    let analysis = analyze_class_value_flow(&graph);

    assert_eq!(
        flow_value(&analysis, "merge"),
        Some(&AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: Some("-active".to_string()),
            min_length: Some("btn-primary--active".len()),
            must_chars: "-abceinrtvy".to_string(),
            may_chars: "-abceinrtvy".to_string(),
            may_include_other_chars: true,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeJoin),
        })
    );
}

#[test]
fn concatenates_abstract_values_for_template_edges() {
    assert_eq!(
        concatenate_abstract_class_values(
            &exact_class_value("btn-"),
            &finite_set_class_value(["primary", "secondary"]),
        ),
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );

    assert_eq!(
        concatenate_abstract_class_values(
            &prefix_class_value("btn-", None),
            &suffix_class_value("-active", None),
        ),
        prefix_suffix_class_value("btn-", "-active", Some("btn--active".len()), None)
    );

    assert_eq!(
        concatenate_abstract_class_values(
            &finite_set_class_value(["card-primary", "card-secondary"]),
            &prefix_class_value("--", None),
        ),
        prefix_class_value("card-", None)
    );
}

#[test]
fn concatenates_reduced_product_constraints_without_widening_to_top() {
    let left = composite_class_value(CompositeClassValueInputV0 {
        prefix: Some("btn-".to_string()),
        suffix: None,
        min_length: Some("btn-primary".len()),
        must_chars: "r".to_string(),
        may_chars: "btn-primary".to_string(),
        may_include_other_chars: true,
        provenance: None,
    });
    let right = char_inclusion_class_value("ae", "active", None, false);

    let concatenated = concatenate_abstract_class_values(&left, &right);

    assert_eq!(
        concatenated,
        AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: None,
            min_length: Some("btn-primary".len() + 2),
            must_chars: "-abenrt".to_string(),
            may_chars: "-abenrt".to_string(),
            may_include_other_chars: true,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeConcat),
        }
    );
    assert_eq!(
        projected_names(
            &concatenated,
            &selector_universe(["btn-primary-active", "btn-icon", "card-active"]),
        ),
        vec!["btn-primary-active".to_string()]
    );
}

#[test]
fn flow_concat_preserves_reduced_product_shape() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@composite-concat".to_string()),
        nodes: vec![
            ClassValueFlowNodeV0 {
                id: "base".to_string(),
                predecessors: Vec::new(),
                transfer: ClassValueFlowTransferV0::AssignFacts(
                    external_facts("constrained")
                        .with_constraint_kind("composite")
                        .with_prefix("btn-")
                        .with_char_must("r")
                        .with_min_len("btn-primary".len()),
                ),
            },
            ClassValueFlowNodeV0 {
                id: "active".to_string(),
                predecessors: vec!["base".to_string()],
                transfer: ClassValueFlowTransferV0::ConcatFacts(
                    external_facts("constrained")
                        .with_constraint_kind("charInclusion")
                        .with_char_must("ae")
                        .with_char_may("active"),
                ),
            },
        ],
    };

    let analysis = analyze_class_value_flow(&graph);

    assert_eq!(
        flow_value(&analysis, "active"),
        Some(&AbstractClassValueV0::Composite {
            prefix: Some("btn-".to_string()),
            suffix: None,
            min_length: Some("btn-primary".len() + 2),
            must_chars: "-abenrt".to_string(),
            may_chars: "-abceinrtv".to_string(),
            may_include_other_chars: false,
            provenance: Some(AbstractClassValueProvenanceV0::CompositeConcat),
        })
    );
}

#[test]
fn analyzes_flow_concat_facts_before_refinement() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@concat".to_string()),
        nodes: vec![
            flow_assign_node("base", external_facts("exact").with_values(["btn-"])),
            ClassValueFlowNodeV0 {
                id: "variant".to_string(),
                predecessors: vec!["base".to_string()],
                transfer: ClassValueFlowTransferV0::ConcatFacts(
                    external_facts("finiteSet").with_values(["primary", "secondary", "icon"]),
                ),
            },
            ClassValueFlowNodeV0 {
                id: "btn-only".to_string(),
                predecessors: vec!["variant".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("constrained")
                        .with_constraint_kind("suffix")
                        .with_suffix("primary"),
                ),
            },
        ],
    };

    let analysis = analyze_class_value_flow(&graph);

    assert_eq!(
        flow_value(&analysis, "variant"),
        Some(&AbstractClassValueV0::FiniteSet {
            values: vec![
                "btn-icon".to_string(),
                "btn-primary".to_string(),
                "btn-secondary".to_string(),
            ]
        })
    );
    assert_eq!(
        flow_value(&analysis, "btn-only"),
        Some(&AbstractClassValueV0::Exact {
            value: "btn-primary".to_string()
        })
    );
    assert_eq!(analysis.nodes[1].transfer_kind, "concatFacts");
}

#[test]
fn analyzes_one_cfa_class_value_flow_with_branch_merge_and_refinement() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
            flow_assign_node(
                "else-if",
                external_facts("exact").with_values(["btn-secondary"]),
            ),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec![
                    "then".to_string(),
                    "else-if".to_string(),
                    "else".to_string(),
                ],
                transfer: ClassValueFlowTransferV0::Join,
            },
            ClassValueFlowNodeV0 {
                id: "btn-only".to_string(),
                predecessors: vec!["merge".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("constrained")
                        .with_constraint_kind("prefix")
                        .with_prefix("btn-"),
                ),
            },
        ],
    };

    let analysis = analyze_class_value_flow(&graph);

    assert_eq!(analysis.schema_version, "0");
    assert_eq!(analysis.product, "omena-abstract-value.flow-analysis");
    assert_eq!(analysis.context_sensitivity, "1-cfa");
    assert_eq!(
        analysis.context_key.as_deref(),
        Some("Button.tsx:render@primary")
    );
    assert!(analysis.converged);

    assert_eq!(
        flow_value(&analysis, "merge"),
        Some(&AbstractClassValueV0::FiniteSet {
            values: vec![
                "btn-primary".to_string(),
                "btn-secondary".to_string(),
                "card".to_string(),
            ]
        })
    );
    assert_eq!(
        flow_value(&analysis, "btn-only"),
        Some(&AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        })
    );
}

#[test]
fn analyzes_one_cfa_call_site_flows_with_context_discrimination() {
    let primary_graph = ClassValueFlowGraphV0 {
        context_key: None,
        nodes: vec![
            flow_assign_node(
                "variant",
                external_facts("exact").with_values(["btn-primary"]),
            ),
            ClassValueFlowNodeV0 {
                id: "exit".to_string(),
                predecessors: vec!["variant".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("constrained")
                        .with_constraint_kind("prefix")
                        .with_prefix("btn-"),
                ),
            },
        ],
    };
    let secondary_graph = ClassValueFlowGraphV0 {
        context_key: None,
        nodes: vec![
            flow_assign_node(
                "variant",
                external_facts("exact").with_values(["btn-secondary"]),
            ),
            ClassValueFlowNodeV0 {
                id: "exit".to_string(),
                predecessors: vec!["variant".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("constrained")
                        .with_constraint_kind("prefix")
                        .with_prefix("btn-"),
                ),
            },
        ],
    };

    let analysis = analyze_one_cfa_call_site_flows(&[
        OneCfaCallSiteFlowInputV0 {
            callee_key: "classForVariant".to_string(),
            call_site_id: "Button.tsx:10:className".to_string(),
            graph: primary_graph,
            exit_node_id: "exit".to_string(),
        },
        OneCfaCallSiteFlowInputV0 {
            callee_key: "classForVariant".to_string(),
            call_site_id: "Card.tsx:22:className".to_string(),
            graph: secondary_graph,
            exit_node_id: "exit".to_string(),
        },
    ]);

    assert_eq!(analysis.schema_version, "0");
    assert_eq!(
        analysis.product,
        "omena-abstract-value.one-cfa-call-site-flow"
    );
    assert_eq!(analysis.context_sensitivity, "1-cfa");
    assert_eq!(analysis.call_site_count, 2);
    assert_eq!(analysis.callee_count, 1);
    assert_eq!(
        analysis.entries[0].context_key,
        "classForVariant@Button.tsx:10:className"
    );
    assert_eq!(
        analysis.entries[1].context_key,
        "classForVariant@Card.tsx:22:className"
    );
    assert_eq!(
        analysis.entries[0].exit_value,
        exact_class_value("btn-primary")
    );
    assert_eq!(
        analysis.entries[1].exit_value,
        exact_class_value("btn-secondary")
    );
    assert_eq!(
        analysis.callee_summaries[0].joined_exit_value,
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );
    assert_eq!(analysis.entries[0].derivation.steps.len(), 3);
    assert_eq!(
        analysis.entries[0].derivation.steps[0].operation,
        "contextFromCallSite"
    );
    assert_eq!(
        analysis.entries[0].derivation.steps[2].operation,
        "projectExitNode"
    );
    assert_eq!(analysis.entries[0].derivation.steps[2].result_kind, "exact");
}

#[test]
fn analyzes_k_cfa_limited_call_site_flows_with_context_stack_discrimination() {
    let analysis = analyze_k_limited_call_site_flows(
        &[
            KLimitedCallSiteFlowInputV0 {
                callee_key: "classForVariant".to_string(),
                call_site_stack: vec![
                    "RouteA.tsx:render".to_string(),
                    "Button.tsx:className".to_string(),
                ],
                graph: flow_exit_graph("btn-primary"),
                exit_node_id: "exit".to_string(),
            },
            KLimitedCallSiteFlowInputV0 {
                callee_key: "classForVariant".to_string(),
                call_site_stack: vec![
                    "RouteB.tsx:render".to_string(),
                    "Button.tsx:className".to_string(),
                ],
                graph: flow_exit_graph("btn-secondary"),
                exit_node_id: "exit".to_string(),
            },
        ],
        2,
    );

    assert_eq!(
        analysis.product,
        "omena-abstract-value.k-limited-call-site-flow"
    );
    assert_eq!(analysis.context_sensitivity, "2-cfa");
    assert_eq!(analysis.max_context_depth, 2);
    assert_eq!(
        analysis.entries[0].context_key,
        "classForVariant@RouteA.tsx:render > Button.tsx:className"
    );
    assert_eq!(
        analysis.entries[1].context_key,
        "classForVariant@RouteB.tsx:render > Button.tsx:className"
    );
    assert_eq!(
        analysis.callee_summaries[0].joined_exit_value,
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );
}

#[test]
fn distinguishes_zero_cfa_and_one_cfa_call_site_abstractions() {
    let inputs = [
        KLimitedCallSiteFlowInputV0 {
            callee_key: "classForVariant".to_string(),
            call_site_stack: vec![
                "RouteA.tsx:render".to_string(),
                "PrimaryButton.tsx:className".to_string(),
            ],
            graph: flow_exit_graph("btn-primary"),
            exit_node_id: "exit".to_string(),
        },
        KLimitedCallSiteFlowInputV0 {
            callee_key: "classForVariant".to_string(),
            call_site_stack: vec![
                "RouteB.tsx:render".to_string(),
                "SecondaryButton.tsx:className".to_string(),
            ],
            graph: flow_exit_graph("btn-secondary"),
            exit_node_id: "exit".to_string(),
        },
    ];

    let zero_cfa = analyze_k_limited_call_site_flows(&inputs, 0);
    let one_cfa = analyze_k_limited_call_site_flows(&inputs, 1);

    assert_eq!(zero_cfa.context_sensitivity, "0-cfa");
    assert_eq!(zero_cfa.max_context_depth, 0);
    assert_eq!(one_cfa.context_sensitivity, "1-cfa");
    assert_eq!(one_cfa.max_context_depth, 1);
    assert_eq!(zero_cfa.entries[0].context_key, "classForVariant@<root>");
    assert_eq!(zero_cfa.entries[1].context_key, "classForVariant@<root>");
    assert_eq!(
        zero_cfa.entries[0].exit_value,
        AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "btn-secondary".to_string()]
        }
    );
    assert_eq!(
        zero_cfa.entries[1].exit_value,
        zero_cfa.entries[0].exit_value
    );
    assert_eq!(
        one_cfa.entries[0].context_key,
        "classForVariant@PrimaryButton.tsx:className"
    );
    assert_eq!(
        one_cfa.entries[1].context_key,
        "classForVariant@SecondaryButton.tsx:className"
    );
    assert_eq!(
        one_cfa.entries[0].exit_value,
        exact_class_value("btn-primary")
    );
    assert_eq!(
        one_cfa.entries[1].exit_value,
        exact_class_value("btn-secondary")
    );
}

#[test]
fn analyzes_control_flow_graph_with_reachability_pruning() {
    let graph = ClassValueControlFlowGraphV0 {
        context_key: Some("Button.tsx:render@cfg".to_string()),
        entry_block_id: "entry".to_string(),
        blocks: vec![
            ClassValueControlFlowBlockV0 {
                id: "entry".to_string(),
                nodes: vec![flow_assign_node(
                    "base",
                    external_facts("exact").with_values(["btn-primary"]),
                )],
                successor_block_ids: vec!["merge".to_string()],
            },
            ClassValueControlFlowBlockV0 {
                id: "dead".to_string(),
                nodes: vec![flow_assign_node(
                    "ghost",
                    external_facts("exact").with_values(["card"]),
                )],
                successor_block_ids: vec!["merge".to_string()],
            },
            ClassValueControlFlowBlockV0 {
                id: "merge".to_string(),
                nodes: vec![ClassValueFlowNodeV0 {
                    id: "exit".to_string(),
                    predecessors: vec!["base".to_string(), "ghost".to_string()],
                    transfer: ClassValueFlowTransferV0::Join,
                }],
                successor_block_ids: Vec::new(),
            },
        ],
    };

    let analysis = analyze_class_value_control_flow_graph(&graph);

    assert_eq!(
        analysis.product,
        "omena-abstract-value.control-flow-analysis"
    );
    assert_eq!(analysis.block_count, 3);
    assert_eq!(analysis.edge_count, 2);
    assert_eq!(analysis.reachable_block_count, 2);
    assert_eq!(analysis.unreachable_block_ids, vec!["dead".to_string()]);
    assert!(analysis.branch_block_ids.is_empty());
    assert_eq!(analysis.join_block_ids, vec!["merge".to_string()]);
    assert_eq!(
        flow_value(&analysis.flow_analysis, "exit"),
        Some(&exact_class_value("btn-primary"))
    );
    assert_eq!(
        analysis
            .blocks
            .iter()
            .find(|block| block.block_id == "dead")
            .map(|block| (&block.reachable, &block.exit_value)),
        Some((&false, &bottom_class_value()))
    );
}

#[test]
fn analyzes_class_value_flow_on_incremental_plan() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["then".to_string(), "else".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };

    let first = analyze_class_value_flow_incremental(&graph, None, 1);
    assert_eq!(
        first.product,
        "omena-abstract-value.incremental-flow-analysis"
    );
    assert!(!first.reused_previous_analysis);
    assert_eq!(first.incremental_plan.dirty_node_count, 3);
    assert_eq!(first.incremental_plan.new_node_count, 3);
    assert_eq!(
        flow_value(&first.analysis, "merge"),
        Some(&AbstractClassValueV0::FiniteSet {
            values: vec!["btn-primary".to_string(), "card".to_string()]
        })
    );

    let unchanged = analyze_class_value_flow_incremental(&graph, Some(&first.next_snapshot), 2);
    assert_eq!(unchanged.incremental_plan.dirty_node_count, 0);
    assert!(!unchanged.reused_previous_analysis);
    assert!(unchanged.analysis.converged);

    let changed_graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node(
                "then",
                external_facts("exact").with_values(["btn-secondary"]),
            ),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["then".to_string(), "else".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };
    let changed =
        analyze_class_value_flow_incremental(&changed_graph, Some(&first.next_snapshot), 3);

    assert_eq!(changed.incremental_plan.changed_input_count, 1);
    assert_eq!(changed.incremental_plan.dependency_dirty_count, 1);
    assert_eq!(
        flow_value(&changed.analysis, "merge"),
        Some(&AbstractClassValueV0::FiniteSet {
            values: vec!["btn-secondary".to_string(), "card".to_string()]
        })
    );
}

#[test]
fn reuses_previous_class_value_flow_analysis_when_incremental_plan_is_clean() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["then".to_string(), "else".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };
    let first = analyze_class_value_flow_incremental(&graph, None, 1);

    let reused = analyze_class_value_flow_incremental_with_reuse(
        &graph,
        Some(&first.next_snapshot),
        Some(&first.analysis),
        2,
    );

    assert_eq!(reused.incremental_plan.dirty_node_count, 0);
    assert!(reused.reused_previous_analysis);
    assert_eq!(reused.analysis, first.analysis);
}

#[test]
fn reuses_previous_class_value_flow_analysis_through_salsa_database() {
    let graph = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["then".to_string(), "else".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };
    let mut incremental_database = OmenaIncrementalDatabaseV0::default();
    let first = analyze_class_value_flow_incremental_with_database(
        &graph,
        &mut incremental_database,
        None,
        1,
    );

    assert_eq!(
        first.next_snapshot.product,
        "omena-incremental.salsa-snapshot"
    );
    assert_eq!(first.incremental_plan.dirty_node_count, 3);
    assert!(!first.reused_previous_analysis);

    let reused = analyze_class_value_flow_incremental_with_database(
        &graph,
        &mut incremental_database,
        Some(&first.analysis),
        2,
    );

    assert_eq!(reused.incremental_plan.dirty_node_count, 0);
    assert!(reused.reused_previous_analysis);
    assert_eq!(reused.analysis, first.analysis);
}

#[test]
fn reuses_clean_contexts_in_incremental_flow_batch() {
    let primary = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@primary".to_string()),
        nodes: vec![
            flow_assign_node("then", external_facts("exact").with_values(["btn-primary"])),
            flow_assign_node("else", external_facts("exact").with_values(["card"])),
            ClassValueFlowNodeV0 {
                id: "merge".to_string(),
                predecessors: vec!["then".to_string(), "else".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    };
    let secondary = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@secondary".to_string()),
        nodes: vec![
            flow_assign_node(
                "base",
                external_facts("exact").with_values(["btn-secondary"]),
            ),
            ClassValueFlowNodeV0 {
                id: "refined".to_string(),
                predecessors: vec!["base".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("prefix").with_prefix("btn-"),
                ),
            },
        ],
    };
    let first = analyze_class_value_flow_incremental_batch_with_reuse(
        &[primary.clone(), secondary.clone()],
        &BTreeMap::new(),
        &BTreeMap::new(),
        1,
    );
    let previous_snapshots = first
        .entries
        .iter()
        .map(|entry| {
            (
                entry.context_key.clone(),
                entry.analysis.next_snapshot.clone(),
            )
        })
        .collect::<BTreeMap<_, _>>();
    let previous_analyses = first
        .entries
        .iter()
        .map(|entry| (entry.context_key.clone(), entry.analysis.analysis.clone()))
        .collect::<BTreeMap<_, _>>();
    let changed_secondary = ClassValueFlowGraphV0 {
        context_key: Some("Button.tsx:render@secondary".to_string()),
        nodes: vec![
            flow_assign_node(
                "base",
                external_facts("exact").with_values(["btn-tertiary"]),
            ),
            ClassValueFlowNodeV0 {
                id: "refined".to_string(),
                predecessors: vec!["base".to_string()],
                transfer: ClassValueFlowTransferV0::RefineFacts(
                    external_facts("prefix").with_prefix("btn-"),
                ),
            },
        ],
    };

    let second = analyze_class_value_flow_incremental_batch_with_reuse(
        &[primary, changed_secondary],
        &previous_snapshots,
        &previous_analyses,
        2,
    );

    assert_eq!(
        second.product,
        "omena-abstract-value.incremental-flow-analysis-batch"
    );
    assert_eq!(second.context_count, 2);
    assert_eq!(second.reused_context_count, 1);
    assert_eq!(second.dirty_context_count, 1);
    assert!(second.entries[0].analysis.reused_previous_analysis);
    assert!(!second.entries[1].analysis.reused_previous_analysis);
    assert_eq!(
        flow_value(&second.entries[1].analysis.analysis, "refined"),
        Some(&AbstractClassValueV0::Exact {
            value: "btn-tertiary".to_string()
        })
    );
}

#[test]
fn reduces_external_facts_before_reporting_domain_kind() {
    let finite_with_prefix = external_facts("finiteSet")
        .with_values(["btn-primary", "card"])
        .with_constraint_kind("prefix")
        .with_prefix("btn-");

    assert_eq!(
        reduced_abstract_class_value_from_facts(&finite_with_prefix),
        exact_class_value("btn-primary")
    );
    assert_eq!(
        reduced_value_domain_kind_from_facts(&finite_with_prefix),
        "exact"
    );

    let constrained_with_values = external_facts("constrained")
        .with_values(["btn-primary", "card"])
        .with_constraint_kind("prefix")
        .with_prefix("btn-");

    assert_eq!(
        reduced_abstract_class_value_from_facts(&constrained_with_values),
        exact_class_value("btn-primary")
    );

    let finite_with_conflicting_prefix = external_facts("finiteSet")
        .with_values(["btn-primary", "card"])
        .with_constraint_kind("prefix")
        .with_prefix("nav-");

    assert_eq!(
        reduced_abstract_class_value_from_facts(&finite_with_conflicting_prefix),
        bottom_class_value()
    );
    assert_eq!(
        reduced_value_domain_kind_from_facts(&finite_with_conflicting_prefix),
        "bottom"
    );
    assert_eq!(
        reduced_value_domain_kind_from_facts(&external_facts("unknown")),
        "none"
    );
}

#[test]
fn explains_reduced_external_fact_derivation_steps() {
    let finite_with_prefix = external_facts("finiteSet")
        .with_values(["btn-primary", "card"])
        .with_constraint_kind("prefix")
        .with_prefix("btn-");

    let derivation = reduced_class_value_derivation_from_facts(&finite_with_prefix);

    assert_eq!(derivation.schema_version, "0");
    assert_eq!(
        derivation.product,
        "omena-abstract-value.reduced-class-value-derivation"
    );
    assert_eq!(derivation.input_fact_kind, "finiteSet");
    assert_eq!(derivation.input_constraint_kind.as_deref(), Some("prefix"));
    assert_eq!(derivation.input_value_count, 2);
    assert_eq!(derivation.reduced_kind, "exact");
    assert_eq!(derivation.steps.len(), 2);
    assert_eq!(derivation.steps[0].operation, "baseFromFacts");
    assert_eq!(derivation.steps[0].result_kind, "finiteSet");
    assert_eq!(derivation.steps[1].operation, "intersectConstraint");
    assert_eq!(derivation.steps[1].input_kind, Some("finiteSet"));
    assert_eq!(derivation.steps[1].refinement_kind, Some("prefix"));
    assert_eq!(derivation.steps[1].result_kind, "exact");
}

#[test]
fn explains_constrained_finite_value_derivation_steps() {
    let constrained_with_values = external_facts("constrained")
        .with_values(["btn-primary", "btn-secondary", "card"])
        .with_constraint_kind("prefix")
        .with_prefix("btn-");

    let derivation = reduced_class_value_derivation_from_facts(&constrained_with_values);

    assert_eq!(derivation.input_fact_kind, "constrained");
    assert_eq!(derivation.input_constraint_kind.as_deref(), Some("prefix"));
    assert_eq!(derivation.input_value_count, 3);
    assert_eq!(derivation.reduced_kind, "finiteSet");
    assert_eq!(derivation.steps.len(), 2);
    assert_eq!(derivation.steps[0].operation, "baseFromFacts");
    assert_eq!(derivation.steps[0].result_kind, "prefix");
    assert_eq!(derivation.steps[1].operation, "intersectFiniteValues");
    assert_eq!(derivation.steps[1].input_kind, Some("prefix"));
    assert_eq!(derivation.steps[1].refinement_kind, Some("finiteSet"));
    assert_eq!(derivation.steps[1].result_kind, "finiteSet");
}

#[test]
fn carries_result_provenance_in_reduced_derivation_steps() {
    let widened = external_facts("finiteSet").with_values([
        "btn-alpha-active",
        "btn-beta-active",
        "btn-gamma-active",
        "btn-delta-active",
        "btn-epsilon-active",
        "btn-zeta-active",
        "btn-eta-active",
        "btn-theta-active",
        "btn-iota-active",
    ]);

    let derivation = reduced_class_value_derivation_from_facts(&widened);

    assert_eq!(derivation.reduced_kind, "composite");
    assert_eq!(derivation.steps.len(), 1);
    assert_eq!(derivation.steps[0].operation, "baseFromFacts");
    assert_eq!(derivation.steps[0].result_kind, "composite");
    assert_eq!(
        derivation.steps[0].result_provenance,
        Some(AbstractClassValueProvenanceV0::FiniteSetWideningComposite)
    );
}

#[test]
fn summarizes_exact_value_provenance_tree() {
    let value = exact_class_value("button");
    let tree = summarize_abstract_class_value_provenance_tree(&value);

    assert_eq!(tree.schema_version, "0");
    assert_eq!(tree.product, "omena-abstract-value.provenance-tree");
    assert_eq!(tree.value_kind, "exact");
    assert_eq!(tree.value, value);
    assert_eq!(tree.value_provenance, None);
    assert_eq!(tree.root.operation, "exactLiteral");
    assert_eq!(tree.root.result_kind, "exact");
    assert_eq!(tree.root.detail.as_deref(), Some("value=button"));
    assert!(tree.root.children.is_empty());
}

#[test]
fn linear_provenance_round_trips_static_labels() {
    let labels = [
        "omena-parser.custom-property-facts",
        "omena-query.style-diagnostics",
    ];
    let provenance = LinearProvenanceV0::<Lin01ProvenanceSemiringV0>::from_static_labels(&labels);

    assert_eq!(provenance.schema_version, "0");
    assert_eq!(provenance.product, "omena-abstract-value.linear-provenance");
    assert_eq!(provenance.layer_marker, "qtt-graded");
    assert_eq!(provenance.feature_gate, "qtt-provenance");
    assert_eq!(provenance.semiring, Lin01ProvenanceSemiringV0::new());
    assert_eq!(provenance.semiring_identifier(), "lin01");
    assert_eq!(provenance.term_count, 2);
    assert_eq!(provenance.labels(), labels.to_vec());
    assert!(provenance.terms.iter().all(|term| term.coefficient == 1));
}

#[test]
fn linear_provenance_preserves_ordered_legacy_labels_as_strict_superset() {
    let labels = [
        "omena-query.cross-file-summary",
        "omena-query.cross-file-summary",
        "omena-lsp-server.definition",
    ];
    let provenance = LinearProvenanceV0::<Lin01ProvenanceSemiringV0>::from_static_labels(&labels);

    assert_eq!(provenance.schema_version, "0");
    assert_eq!(provenance.labels(), labels.to_vec());
    assert_eq!(provenance.term_count, labels.len());
    assert_eq!(provenance.semiring_identifier(), "lin01");
    assert_eq!(
        provenance
            .terms
            .iter()
            .map(|term| (term.coefficient, term.label))
            .collect::<Vec<_>>(),
        labels.iter().map(|label| (1, *label)).collect::<Vec<_>>()
    );
}

#[test]
fn provenance_semiring_identifiers_are_stable_and_unique() {
    let identifiers = [
        Lin01ProvenanceSemiringV0::new().semiring_identifier(),
        NaturalCountProvenanceSemiringV0::new().semiring_identifier(),
        TropicalProvenanceSemiringV0::new().semiring_identifier(),
        ViterbiProvenanceSemiringV0::new().semiring_identifier(),
        SecurityLabelProvenanceSemiringV0::new().semiring_identifier(),
    ];

    assert_eq!(
        identifiers,
        [
            "lin01",
            "naturalCount",
            "tropical",
            "viterbi",
            "securityLabel"
        ]
    );
    assert_eq!(
        identifiers
            .iter()
            .collect::<std::collections::BTreeSet<_>>()
            .len(),
        identifiers.len()
    );
}

#[test]
fn provenance_semiring_fixture_laws_are_enforced() {
    let reports = m4_alpha_provenance_semiring_law_reports_v0();

    assert_eq!(reports.len(), 5);
    assert!(
        reports.iter().all(|report| report.all_fixture_laws_hold),
        "{reports:#?}"
    );
    assert_eq!(
        reports
            .iter()
            .map(|report| report.semiring_identifier)
            .collect::<Vec<_>>(),
        vec![
            "lin01",
            "naturalCount",
            "tropical",
            "viterbi",
            "securityLabel"
        ]
    );
    assert!(reports.iter().all(|report| report.schema_version == "0"));
    assert!(
        reports
            .iter()
            .all(|report| report.product == "omena-abstract-value.provenance-semiring-law-report")
    );
}

#[test]
fn summarizes_finite_widening_provenance_tree() {
    let value = finite_set_class_value([
        "btn-alpha-active",
        "btn-beta-active",
        "btn-gamma-active",
        "btn-delta-active",
        "btn-epsilon-active",
        "btn-zeta-active",
        "btn-eta-active",
        "btn-theta-active",
        "btn-iota-active",
    ]);

    let tree = summarize_abstract_class_value_provenance_tree(&value);

    assert_eq!(tree.value_kind, "composite");
    assert_eq!(
        tree.value_provenance,
        Some(AbstractClassValueProvenanceV0::FiniteSetWideningComposite)
    );
    assert_eq!(tree.root.operation, "finiteSetWidening");
    assert_eq!(
        tree.root.reason,
        "large finite set widened to preserved edge and character constraints"
    );
    assert_provenance_child(&tree.root.children, "prefixConstraint", "prefix=btn-");
    assert_provenance_child(&tree.root.children, "suffixConstraint", "suffix=-active");
    assert_provenance_child(&tree.root.children, "lengthConstraint", "minLength=14");
    assert_provenance_child(
        &tree.root.children,
        "characterMustConstraint",
        "mustChars=-abceintv",
    );
}

#[test]
fn summarizes_reduced_product_join_provenance_tree() {
    let value = intersect_abstract_class_values(
        &prefix_class_value("btn-", None),
        &suffix_class_value("-active", None),
    );

    let tree = summarize_abstract_class_value_provenance_tree(&value);

    assert_eq!(tree.value_kind, "prefixSuffix");
    assert_eq!(
        tree.value_provenance,
        Some(AbstractClassValueProvenanceV0::CompositeJoin)
    );
    assert_eq!(tree.root.operation, "reducedProductJoin");
    assert_eq!(
        tree.root.reason,
        "reduced product combined compatible constraints from multiple domains"
    );
    assert_provenance_child(&tree.root.children, "prefixConstraint", "prefix=btn-");
    assert_provenance_child(&tree.root.children, "suffixConstraint", "suffix=-active");
    assert_provenance_child(&tree.root.children, "lengthConstraint", "minLength=10");
}

#[test]
fn projects_exact_and_finite_values_into_selector_universe() {
    let selectors = selector_universe(["button", "card", "link"]);

    let exact = project_abstract_value_selectors(&exact_class_value("button"), &selectors);
    assert_eq!(exact.selector_names, vec!["button".to_string()]);
    assert_eq!(exact.certainty, SelectorProjectionCertaintyV0::Exact);

    let finite = project_abstract_value_selectors(
        &finite_set_class_value(["button", "missing"]),
        &selectors,
    );
    assert_eq!(finite.selector_names, vec!["button".to_string()]);
    assert_eq!(finite.certainty, SelectorProjectionCertaintyV0::Inferred);
}

#[test]
fn projects_constrained_values_into_selector_universe() {
    let selectors = selector_universe(["btn-primary", "btn-secondary", "card", "link-active"]);

    let prefix = project_abstract_value_selectors(
        &prefix_class_value("btn-", Some(AbstractClassValueProvenanceV0::PrefixJoinLcp)),
        &selectors,
    );
    assert_eq!(
        prefix.selector_names,
        vec!["btn-primary".to_string(), "btn-secondary".to_string()]
    );
    assert_eq!(prefix.certainty, SelectorProjectionCertaintyV0::Inferred);

    let edge = project_abstract_value_selectors(
        &prefix_suffix_class_value("btn-", "primary", None, None),
        &selectors,
    );
    assert_eq!(edge.selector_names, vec!["btn-primary".to_string()]);
    assert_eq!(edge.certainty, SelectorProjectionCertaintyV0::Inferred);

    let chars = project_abstract_value_selectors(
        &char_inclusion_class_value("ac", "acdr", None, false),
        &selectors,
    );
    assert_eq!(chars.selector_names, vec!["card".to_string()]);
    assert_eq!(chars.certainty, SelectorProjectionCertaintyV0::Inferred);
}

#[test]
fn derives_projection_certainty_from_domain_and_selector_coverage() {
    assert_eq!(
        derive_selector_projection_certainty(&AbstractClassValueV0::Bottom, 0, 3),
        SelectorProjectionCertaintyV0::Possible
    );
    assert_eq!(
        derive_selector_projection_certainty(&prefix_class_value("btn-", None), 3, 3,),
        SelectorProjectionCertaintyV0::Inferred
    );
    assert_eq!(
        derive_selector_projection_certainty(&AbstractClassValueV0::Top, 3, 3),
        SelectorProjectionCertaintyV0::Possible
    );
}

fn selector_universe(values: impl IntoIterator<Item = &'static str>) -> Vec<String> {
    values.into_iter().map(str::to_string).collect()
}

fn assert_provenance_child(
    children: &[AbstractClassValueProvenanceNodeV0],
    operation: &str,
    detail: &str,
) {
    assert!(
        children.iter().any(|child| {
            child.operation == operation && child.detail.as_deref() == Some(detail)
        }),
        "missing provenance child operation={operation} detail={detail}: {children:#?}"
    );
}

fn assert_projection_equivalent(
    left: &AbstractClassValueV0,
    right: &AbstractClassValueV0,
    selectors: &[String],
) {
    assert_eq!(
        projected_names(left, selectors),
        projected_names(right, selectors)
    );
}

fn projected_names(value: &AbstractClassValueV0, selectors: &[String]) -> Vec<String> {
    project_abstract_value_selectors(value, selectors).selector_names
}

fn intersect_projected_names(
    left: &AbstractClassValueV0,
    right: &AbstractClassValueV0,
    selectors: &[String],
) -> Vec<String> {
    let right_names = projected_names(right, selectors)
        .into_iter()
        .collect::<std::collections::BTreeSet<_>>();
    projected_names(left, selectors)
        .into_iter()
        .filter(|name| right_names.contains(name))
        .collect()
}

fn flow_assign_node(id: &str, facts: ExternalStringTypeFactsV0) -> ClassValueFlowNodeV0 {
    ClassValueFlowNodeV0 {
        id: id.to_string(),
        predecessors: Vec::new(),
        transfer: ClassValueFlowTransferV0::AssignFacts(facts),
    }
}

fn property_candidate(
    property_name: &str,
    value: &str,
    pseudo_state: Option<&str>,
) -> AbstractPropertyValueCandidateV0 {
    AbstractPropertyValueCandidateV0 {
        property_name: property_name.to_string(),
        value: value.to_string(),
        pseudo_state: pseudo_state.map(str::to_string),
    }
}

fn flow_exit_graph(value: &str) -> ClassValueFlowGraphV0 {
    ClassValueFlowGraphV0 {
        context_key: None,
        nodes: vec![
            flow_assign_node(
                "value",
                ExternalStringTypeFactsV0 {
                    kind: "exact".to_string(),
                    constraint_kind: None,
                    values: Some(vec![value.to_string()]),
                    prefix: None,
                    suffix: None,
                    min_len: None,
                    max_len: None,
                    char_must: None,
                    char_may: None,
                    may_include_other_chars: None,
                },
            ),
            ClassValueFlowNodeV0 {
                id: "exit".to_string(),
                predecessors: vec!["value".to_string()],
                transfer: ClassValueFlowTransferV0::Join,
            },
        ],
    }
}

fn flow_value<'a>(
    analysis: &'a super::ClassValueFlowAnalysisV0,
    id: &str,
) -> Option<&'a AbstractClassValueV0> {
    analysis
        .nodes
        .iter()
        .find(|node| node.id == id)
        .map(|node| &node.value)
}

fn external_facts(kind: &str) -> ExternalStringTypeFactsV0 {
    ExternalStringTypeFactsV0 {
        kind: kind.to_string(),
        constraint_kind: None,
        values: None,
        prefix: None,
        suffix: None,
        min_len: None,
        max_len: None,
        char_must: None,
        char_may: None,
        may_include_other_chars: None,
    }
}

trait ExternalFactsTestExt {
    fn with_values(self, values: impl IntoIterator<Item = &'static str>) -> Self;
    fn with_constraint_kind(self, value: &'static str) -> Self;
    fn with_prefix(self, value: &'static str) -> Self;
    fn with_suffix(self, value: &'static str) -> Self;
    fn with_min_len(self, value: usize) -> Self;
    fn with_char_must(self, value: &'static str) -> Self;
    fn with_char_may(self, value: &'static str) -> Self;
}

impl ExternalFactsTestExt for ExternalStringTypeFactsV0 {
    fn with_values(mut self, values: impl IntoIterator<Item = &'static str>) -> Self {
        self.values = Some(values.into_iter().map(str::to_string).collect());
        self
    }

    fn with_constraint_kind(mut self, value: &'static str) -> Self {
        self.constraint_kind = Some(value.to_string());
        self
    }

    fn with_prefix(mut self, value: &'static str) -> Self {
        self.prefix = Some(value.to_string());
        self
    }

    fn with_suffix(mut self, value: &'static str) -> Self {
        self.suffix = Some(value.to_string());
        self
    }

    fn with_min_len(mut self, value: usize) -> Self {
        self.min_len = Some(value);
        self
    }

    fn with_char_must(mut self, value: &'static str) -> Self {
        self.char_must = Some(value.to_string());
        self
    }

    fn with_char_may(mut self, value: &'static str) -> Self {
        self.char_may = Some(value.to_string());
        self
    }
}
