use super::super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, Design, Edit, TierTarget};
#[test]
fn tier_items_reflects_position_and_fields_in_order() {
let mut state = EditorState::fresh();
state
.apply(Edit::AddTier {
index: 0,
tier: ConstraintTier {
angle_deg: -41.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::ScaleReference(0.65),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
},
})
.unwrap();
let items = tier_items(&state.design, state.design.effective_refractive_index());
assert_eq!(items.len(), 1);
assert_eq!(items[0].index, 0);
assert_eq!(items[0].angle_deg.as_str(), "-41.00");
assert_eq!(items[0].mast.as_str(), "0.6500");
assert_eq!(items[0].name.as_str(), "P1");
assert_eq!(items[0].indices.as_str(), "0, 24");
}
#[test]
fn tier_items_reports_orbit_status_and_detached_state() {
let mut state = EditorState::fresh(); state
.apply(Edit::AddTier {
index: 0,
tier: ConstraintTier {
angle_deg: -41.0,
name: "P1".to_string(),
indices: vec![0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0],
constraint: MeetConstraint::ScaleReference(0.65),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
},
})
.unwrap();
let items = tier_items_stale(&state.design, state.design.effective_refractive_index());
assert_eq!(items[0].orbit_status.as_str(), "orbit x8");
assert!(!items[0].orbit_incomplete);
assert!(!items[0].is_detached);
let edit = state.design.detach_all_in_tier(0).unwrap();
state.apply(edit).unwrap();
let items = tier_items_stale(&state.design, state.design.effective_refractive_index());
assert!(items[0].is_detached);
assert_eq!(
items[0].orbit_status.as_str(),
"orbit x8",
"detaching must not change the reported orbit shape, only is_detached"
);
}
#[test]
fn tier_items_stale_reflects_authored_fields_without_solving() {
let mut state = EditorState::fresh();
state
.apply(Edit::AddTier {
index: 0,
tier: ConstraintTier {
angle_deg: -41.0,
name: "P1".to_string(),
indices: vec![0.0, 24.0],
constraint: MeetConstraint::ScaleReference(0.65),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
},
})
.unwrap();
let items = tier_items_stale(&state.design, state.design.effective_refractive_index());
assert_eq!(items.len(), 1);
assert_eq!(items[0].index, 0);
assert_eq!(items[0].angle_deg.as_str(), "-41.00");
assert_eq!(items[0].name.as_str(), "P1");
assert_eq!(items[0].indices.as_str(), "0, 24");
assert_eq!(items[0].mast.as_str(), "-");
assert!(items[0].strategy_is_uncertain);
}
#[test]
fn adopting_a_suggested_meet_keeps_the_design_solved_at_the_same_mast() {
let mut state = EditorState::fresh();
state.design = Design::fresh(
indicatrix_cut_core::PreformSpec::block(1.0, 1.0, 2.0),
96,
4,
1.62,
);
state
.apply(Edit::AddTier {
index: 0,
tier: ConstraintTier {
angle_deg: 30.0,
name: "A".to_string(),
indices: vec![0.0, 24.0, 48.0, 72.0],
constraint: MeetConstraint::ScaleReference(0.5),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
},
})
.unwrap();
let mut derived = state.design.clone();
derived.tiers.push(ConstraintTier {
angle_deg: 45.0,
name: "B".to_string(),
indices: vec![0.0, 24.0, 48.0, 72.0],
constraint: MeetConstraint::MeetNamed(vec!["A".to_string()]),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
});
let derived_mast = derived
.solve()
.expect("A anchors the crown; B must solve against it")[1]
.mast;
state
.apply(Edit::AddTier {
index: 1,
tier: ConstraintTier {
angle_deg: 45.0,
name: "B".to_string(),
indices: vec![0.0, 24.0, 48.0, 72.0],
constraint: MeetConstraint::ScaleReference(derived_mast),
imported_meet: Some(MeetConstraint::MeetNamed(vec!["A".to_string()])),
original_notes: None,
detached: Vec::new(),
},
})
.unwrap();
let items = tier_items(&state.design, state.design.effective_refractive_index());
assert_eq!(items[1].mast.as_str(), format!("{derived_mast:.4}"));
let (status, is_problem) = status_text_and_is_problem(&state.design);
assert!(
!is_problem,
"pinned design must already read as solved: {status}"
);
let constraint = state.design.tiers[1].imported_meet.clone().unwrap();
state
.apply(Edit::SetConstraint {
index: 1,
constraint,
})
.expect("adopt must apply");
let (status, is_problem) = status_text_and_is_problem(&state.design);
assert!(!is_problem, "adopted design must still solve: {status}");
let items = tier_items(&state.design, state.design.effective_refractive_index());
assert!(
!items[1].strategy_is_uncertain,
"the adopted meet must resolve to a real, trusted strategy, not an estimate"
);
let adopted_mast = state
.design
.solve()
.expect("the adopted design must still solve")[1]
.mast;
assert!(
(adopted_mast - derived_mast).abs() < 1e-9,
"adopting should reproduce the exact same mast the suggestion was showing: \
{adopted_mast} vs {derived_mast}"
);
assert_eq!(
items[1].mast.as_str(),
format!("{derived_mast:.4}"),
"and the row must display that same mast, rounded for the table"
);
}
#[test]
fn tier_margin_and_risk_is_not_applicable_for_crown_and_girdle_angles() {
assert_eq!(tier_margin_and_risk(30.0, 2.417, None), (String::new(), -1));
assert_eq!(tier_margin_and_risk(0.0, 2.417, None), (String::new(), -1));
}
#[test]
fn tier_margin_and_risk_classifies_a_comfortably_safe_pavilion_tier() {
let (text, risk) = tier_margin_and_risk(-40.0, 2.417, None);
assert_eq!(risk, 0);
assert!(text.starts_with('+'), "{text}");
}
#[test]
fn tier_margin_and_risk_classifies_a_windowing_pavilion_tier() {
let (text, risk) = tier_margin_and_risk(-20.0, 2.417, None);
assert_eq!(risk, 2);
assert!(text.starts_with('-'), "{text}");
}
#[test]
fn tier_margin_and_risk_classifies_a_marginal_pavilion_tier() {
let n_d = 2.417;
let critical = indicatrix_cut_core::critical_angle_deg(n_d);
let (_, risk) = tier_margin_and_risk(-critical, n_d, None);
assert_eq!(risk, 1);
}
#[test]
fn tier_margin_and_risk_classifies_a_crown_tier_as_an_estimate_against_a_pavilion_partner() {
let (text, risk) = tier_margin_and_risk(34.0, 2.417, Some(41.0));
assert_eq!(risk, 0);
assert!(text.ends_with("(est.)"), "{text}");
}
#[test]
fn constraint_kind_and_text_reads_the_three_plain_constraint_kinds_with_no_target() {
assert_eq!(
constraint_kind_and_text(&MeetConstraint::MeetExisting, None),
(0, String::new())
);
assert_eq!(
constraint_kind_and_text(
&MeetConstraint::MeetNamed(vec!["P1".to_string(), "G1".to_string()]),
None
),
(1, "P1, G1".to_string())
);
assert_eq!(
constraint_kind_and_text(&MeetConstraint::ScaleReference(0.65), None),
(2, "0.65".to_string())
);
}
#[test]
fn constraint_kind_and_text_prefers_a_target_over_the_scale_reference_placeholder() {
let placeholder = MeetConstraint::ScaleReference(0.0);
assert_eq!(
constraint_kind_and_text(&placeholder, Some(TierTarget::DepthMm(3.2))),
(3, "3.2".to_string())
);
assert_eq!(
constraint_kind_and_text(&placeholder, Some(TierTarget::GirdleThicknessMm(0.25))),
(4, "0.25".to_string())
);
assert_eq!(
constraint_kind_and_text(&placeholder, Some(TierTarget::TableWidthMm(4.1))),
(5, "4.1".to_string())
);
}
#[test]
fn index_chip_items_builds_one_chip_per_index_flagging_only_the_detached_ones() {
let indices = vec![0.0, 24.0, 48.0, 72.0];
let detached = vec![24.0];
let chips = index_chip_items(&indices, &detached);
assert_eq!(chips.len(), 4);
assert_eq!(chips[0].position, 0.0);
assert_eq!(chips[0].label.as_str(), "0");
assert!(!chips[0].detached);
assert_eq!(chips[1].position, 24.0);
assert!(chips[1].detached);
assert!(!chips[2].detached);
assert!(!chips[3].detached);
}
#[test]
fn index_chip_items_formats_a_fractional_position_with_two_decimals() {
let chips = index_chip_items(&[12.5], &[]);
assert_eq!(chips[0].label.as_str(), "12.50");
}
#[test]
fn index_chip_items_is_empty_for_an_empty_tier() {
assert_eq!(index_chip_items(&[], &[]).len(), 0);
}
#[test]
fn tier_matches_filter_is_case_insensitive_and_matches_a_substring_anywhere() {
assert!(tier_matches_filter("Girdle Facet G1", "girdle"));
assert!(tier_matches_filter("Girdle Facet G1", "FACET"));
assert!(tier_matches_filter("Girdle Facet G1", "g1"));
assert!(!tier_matches_filter("Girdle Facet G1", "pavilion"));
}
#[test]
fn tier_matches_filter_treats_a_blank_or_whitespace_only_filter_as_matching_everything() {
assert!(tier_matches_filter("anything", ""));
assert!(tier_matches_filter("anything", " "));
assert!(tier_matches_filter("", ""));
}