use geo::{Coord, LineString};
use tp_lib_core::models::{NetRelation, Netelement};
use tp_lib_core::path::{build_topology_graph, validate_netrelation_references, NetelementSide};
#[cfg(test)]
mod tests {
use super::*;
fn create_test_netelement(id: &str) -> Netelement {
Netelement {
id: id.to_string(),
geometry: LineString::new(vec![Coord { x: 0.0, y: 0.0 }, Coord { x: 1.0, y: 1.0 }]),
crs: "EPSG:4326".to_string(),
}
}
#[test]
fn test_netelement_side_node_creation() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelations = vec![];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, node_map) = result.unwrap();
assert_eq!(graph.node_count(), 4);
let ne_a_start = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let ne_a_end = NetelementSide::new("NE_A".to_string(), 1).unwrap();
let ne_b_start = NetelementSide::new("NE_B".to_string(), 0).unwrap();
let ne_b_end = NetelementSide::new("NE_B".to_string(), 1).unwrap();
assert!(node_map.contains_key(&ne_a_start));
assert!(node_map.contains_key(&ne_a_end));
assert!(node_map.contains_key(&ne_b_start));
assert!(node_map.contains_key(&ne_b_end));
}
#[test]
fn test_internal_edge_creation() {
let netelements = vec![create_test_netelement("NE_A")];
let netrelations = vec![];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, node_map) = result.unwrap();
assert_eq!(graph.edge_count(), 2);
let start_side = NetelementSide::new("NE_A".to_string(), 0).unwrap();
let end_side = NetelementSide::new("NE_A".to_string(), 1).unwrap();
let start_node = node_map[&start_side];
let end_node = node_map[&end_side];
assert!(graph.contains_edge(start_node, end_node));
assert!(graph.contains_edge(end_node, start_node));
}
#[test]
fn test_netrelation_connection_edge_creation() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1, 0, true, true, )
.unwrap();
let netrelations = vec![netrelation];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, node_map) = result.unwrap();
assert_eq!(graph.edge_count(), 6);
let ne_a_end = NetelementSide::new("NE_A".to_string(), 1).unwrap();
let ne_b_start = NetelementSide::new("NE_B".to_string(), 0).unwrap();
let a_end_node = node_map[&ne_a_end];
let b_start_node = node_map[&ne_b_start];
assert!(graph.contains_edge(a_end_node, b_start_node));
assert!(graph.contains_edge(b_start_node, a_end_node));
}
#[test]
fn test_netrelation_unidirectional_edge() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1, 0, true, false, )
.unwrap();
let netrelations = vec![netrelation];
let result = build_topology_graph(&netelements, &netrelations);
assert!(result.is_ok());
let (graph, node_map) = result.unwrap();
assert_eq!(graph.edge_count(), 5);
let ne_a_end = NetelementSide::new("NE_A".to_string(), 1).unwrap();
let ne_b_start = NetelementSide::new("NE_B".to_string(), 0).unwrap();
let a_end_node = node_map[&ne_a_end];
let b_start_node = node_map[&ne_b_start];
assert!(graph.contains_edge(a_end_node, b_start_node));
assert!(!graph.contains_edge(b_start_node, a_end_node));
}
#[test]
fn test_netrelation_valid_bidirectional() {
let relation = NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1, 0, true,
true,
);
assert!(relation.is_ok());
let rel = relation.unwrap();
assert!(rel.is_bidirectional());
assert!(rel.is_navigable_forward());
assert!(rel.is_navigable_backward());
}
#[test]
fn test_netrelation_valid_unidirectional() {
let relation = NetRelation::new(
"NR002".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
false,
);
assert!(relation.is_ok());
let rel = relation.unwrap();
assert!(!rel.is_bidirectional());
assert!(rel.is_navigable_forward());
assert!(!rel.is_navigable_backward());
}
#[test]
fn test_netrelation_invalid_position_on_a() {
let relation = NetRelation::new(
"NR003".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
2, 0,
true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_netrelation_invalid_position_on_b() {
let relation = NetRelation::new(
"NR004".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
5, true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_netrelation_self_reference() {
let relation = NetRelation::new(
"NR005".to_string(),
"NE_A".to_string(),
"NE_A".to_string(), 1,
0,
true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_netrelation_empty_id() {
let relation = NetRelation::new(
"".to_string(), "NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_netrelation_empty_from_id() {
let relation = NetRelation::new(
"NR006".to_string(),
"".to_string(), "NE_B".to_string(),
1,
0,
true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_netrelation_empty_to_id() {
let relation = NetRelation::new(
"NR007".to_string(),
"NE_A".to_string(),
"".to_string(), 1,
0,
true,
false,
);
assert!(relation.is_err());
}
#[test]
fn test_validate_netrelation_references_all_valid() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
create_test_netelement("NE_C"),
];
let netrelations = vec![
NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
true,
)
.unwrap(),
NetRelation::new(
"NR002".to_string(),
"NE_B".to_string(),
"NE_C".to_string(),
1,
0,
true,
false,
)
.unwrap(),
];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 0);
}
#[test]
fn test_validate_netrelation_references_invalid_from() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelations = vec![NetRelation::new(
"NR001".to_string(),
"NE_MISSING".to_string(), "NE_B".to_string(),
1,
0,
true,
true,
)
.unwrap()];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 1);
assert_eq!(invalid[0], "NR001");
}
#[test]
fn test_validate_netrelation_references_invalid_to() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelations = vec![NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_MISSING".to_string(), 1,
0,
true,
true,
)
.unwrap()];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 1);
assert_eq!(invalid[0], "NR001");
}
#[test]
fn test_validate_netrelation_references_both_invalid() {
let netelements = vec![create_test_netelement("NE_A")];
let netrelations = vec![NetRelation::new(
"NR001".to_string(),
"NE_MISSING1".to_string(), "NE_MISSING2".to_string(), 1,
0,
true,
true,
)
.unwrap()];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 1);
assert_eq!(invalid[0], "NR001");
}
#[test]
fn test_validate_netrelation_references_mixed() {
let netelements = vec![
create_test_netelement("NE_A"),
create_test_netelement("NE_B"),
];
let netrelations = vec![
NetRelation::new(
"NR001".to_string(),
"NE_A".to_string(),
"NE_B".to_string(),
1,
0,
true,
true,
)
.unwrap(),
NetRelation::new(
"NR002".to_string(),
"NE_A".to_string(),
"NE_MISSING".to_string(), 1,
0,
true,
false,
)
.unwrap(),
NetRelation::new(
"NR003".to_string(),
"NE_MISSING".to_string(), "NE_A".to_string(),
1,
0,
true,
false,
)
.unwrap(),
];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 2);
assert!(invalid.contains(&"NR002".to_string()));
assert!(invalid.contains(&"NR003".to_string()));
}
#[test]
fn test_validate_netrelation_references_empty_collections() {
let netelements = vec![];
let netrelations = vec![];
let invalid = validate_netrelation_references(&netelements, &netrelations);
assert_eq!(invalid.len(), 0);
}
#[test]
fn test_intrinsic_coordinate_calculation() {
use chrono::Utc;
use tp_lib_core::models::{AssociatedNetElement, GnssPosition, TrainPath};
use tp_lib_core::{project_onto_path, PathConfig};
let netelements = vec![Netelement {
id: "NE_A".to_string(),
geometry: LineString::new(vec![
Coord {
x: 4.350,
y: 50.850,
},
Coord {
x: 4.360,
y: 50.860,
},
]),
crs: "EPSG:4326".to_string(),
}];
let segments =
vec![AssociatedNetElement::new("NE_A".to_string(), 0.85, 0.0, 1.0, 0, 2).unwrap()];
let path = TrainPath::new(segments, 0.85, Some(Utc::now()), None).unwrap();
let gnss_positions = vec![
GnssPosition::new(50.850, 4.350, Utc::now().into(), "EPSG:4326".to_string()).unwrap(), GnssPosition::new(50.855, 4.355, Utc::now().into(), "EPSG:4326".to_string()).unwrap(), GnssPosition::new(50.860, 4.360, Utc::now().into(), "EPSG:4326".to_string()).unwrap(), ];
let config = PathConfig::default();
let result = project_onto_path(&gnss_positions, &path, &netelements, &config);
assert!(result.is_ok(), "Projection should succeed");
let projected = result.unwrap();
assert_eq!(projected.len(), 3, "Should have 3 projected positions");
for proj in &projected {
assert!(
proj.intrinsic.is_some(),
"Intrinsic coordinate should be set"
);
let intrinsic = proj.intrinsic.unwrap();
assert!(
(0.0..=1.0).contains(&intrinsic),
"Intrinsic coordinate {} should be in range [0, 1]",
intrinsic
);
}
let start_intrinsic = projected[0].intrinsic.unwrap();
let end_intrinsic = projected[2].intrinsic.unwrap();
assert!(
start_intrinsic < end_intrinsic,
"Start intrinsic ({}) should be less than end intrinsic ({})",
start_intrinsic,
end_intrinsic
);
}
#[test]
fn test_fallback_detection_logic() {
use chrono::Utc;
use tp_lib_core::calculate_train_path;
use tp_lib_core::models::GnssPosition;
use tp_lib_core::path::{PathCalculationMode, PathConfig};
let gnss_positions = vec![
GnssPosition::new(50.950, 4.450, Utc::now().into(), "EPSG:4326".to_string()).unwrap(), GnssPosition::new(50.951, 4.451, Utc::now().into(), "EPSG:4326".to_string()).unwrap(),
];
let netelements = vec![Netelement {
id: "NE_A".to_string(),
geometry: LineString::new(vec![
Coord {
x: 4.350,
y: 50.850,
}, Coord {
x: 4.351,
y: 50.851,
},
]),
crs: "EPSG:4326".to_string(),
}];
let netrelations: Vec<NetRelation> = vec![];
let config = PathConfig {
cutoff_distance: 100.0, ..PathConfig::default()
};
let result = calculate_train_path(&gnss_positions, &netelements, &netrelations, &config);
assert!(result.is_ok(), "Fallback should succeed");
let path_result = result.unwrap();
assert_eq!(
path_result.mode,
PathCalculationMode::FallbackIndependent,
"Should use fallback mode when GNSS beyond cutoff distance"
);
assert!(
path_result.path.is_none(),
"Path should be None in fallback mode"
);
assert!(
path_result
.warnings
.iter()
.any(|w| w.contains("No continuous path found")),
"Should warn about path calculation failure, got: {:?}",
path_result.warnings
);
assert!(
path_result
.warnings
.iter()
.any(|w| w.to_lowercase().contains("fallback")
|| w.to_lowercase().contains("falling back")),
"Should warn about using fallback mode, got: {:?}",
path_result.warnings
);
}
#[test]
fn test_fallback_with_path_only_mode() {
use chrono::Utc;
use tp_lib_core::calculate_train_path;
use tp_lib_core::models::GnssPosition;
use tp_lib_core::path::{PathCalculationMode, PathConfig};
let gnss_positions =
vec![
GnssPosition::new(50.950, 4.450, Utc::now().into(), "EPSG:4326".to_string())
.unwrap(),
];
let netelements = vec![create_test_netelement("NE_A")];
let netrelations: Vec<NetRelation> = vec![];
let config = PathConfig {
path_only: true,
cutoff_distance: 100.0, ..PathConfig::default()
};
let result = calculate_train_path(&gnss_positions, &netelements, &netrelations, &config);
assert!(result.is_ok(), "Path-only fallback should succeed");
let path_result = result.unwrap();
assert_eq!(
path_result.mode,
PathCalculationMode::FallbackIndependent,
"Should use fallback mode when GNSS beyond cutoff"
);
assert!(
path_result.projected_positions.is_empty(),
"Path-only mode should skip fallback projection"
);
assert!(
path_result
.warnings
.iter()
.any(|w| w.contains("Path-only mode")),
"Should warn about path-only mode"
);
}
}