use geographdb_core::{
    articulation_points_4d, astar_find_path_4d, bridges_4d, earliest_arrival_path_4d,
    fastest_temporal_path_4d, reachable_4d, strongly_connected_components_4d,
    time_dependent_dijkstra_4d, GraphNode4D, SpatialRegion, TemporalEdge, TemporalWindow,
    TraversalContext4D,
};
use glam::Vec3;

fn test_graph() -> Vec<GraphNode4D> {
    vec![
        GraphNode4D {
            id: 1,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 2,
                    weight: 1.0,
                    begin_ts: 0,
                    end_ts: 100,
                },
                TemporalEdge {
                    dst: 4,
                    weight: 10.0,
                    begin_ts: 0,
                    end_ts: 100,
                },
            ],
        },
        GraphNode4D {
            id: 2,
            x: 1.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 3,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 40,
            }],
        },
        GraphNode4D {
            id: 3,
            x: 2.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 1,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
        GraphNode4D {
            id: 4,
            x: 50.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
    ]
}

#[test]
fn reachable_4d_filters_by_time_and_space() {
    let ctx = TraversalContext4D {
        time_window: Some(TemporalWindow { start: 50, end: 60 }),
        spatial_region: Some(SpatialRegion::Sphere {
            center: Vec3::ZERO,
            radius: 5.0,
        }),
        graph_weight: 1.0,
        spatial_weight: 0.0,
        temporal_weight: 0.0,
    };

    let reachable = reachable_4d(&test_graph(), 1, &ctx);

    assert_eq!(reachable, vec![1, 2]);
}

#[test]
fn astar_4d_uses_edge_time_validity_and_spatial_cost() {
    let ctx = TraversalContext4D {
        time_window: Some(TemporalWindow { start: 10, end: 20 }),
        spatial_region: None,
        graph_weight: 1.0,
        spatial_weight: 0.5,
        temporal_weight: 0.0,
    };

    let path = astar_find_path_4d(&test_graph(), 1, 3, &ctx).expect("path should exist");

    assert_eq!(path.node_ids, vec![1, 2, 3]);
    assert!(path.total_cost > 0.0);
}

#[test]
fn astar_4d_rejects_paths_when_edge_is_outside_time_window() {
    let ctx = TraversalContext4D {
        time_window: Some(TemporalWindow { start: 50, end: 60 }),
        spatial_region: None,
        graph_weight: 1.0,
        spatial_weight: 0.0,
        temporal_weight: 0.0,
    };

    assert!(astar_find_path_4d(&test_graph(), 1, 3, &ctx).is_none());
}

#[test]
fn astar_4d_can_prefer_spatially_local_path_over_lower_graph_weight() {
    let nodes = vec![
        GraphNode4D {
            id: 1,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 2,
                    weight: 5.0,
                    begin_ts: 0,
                    end_ts: 100,
                },
                TemporalEdge {
                    dst: 4,
                    weight: 1.0,
                    begin_ts: 0,
                    end_ts: 100,
                },
            ],
        },
        GraphNode4D {
            id: 2,
            x: 1.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 3,
                weight: 5.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
        GraphNode4D {
            id: 3,
            x: 2.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
        GraphNode4D {
            id: 4,
            x: 100.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 3,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
    ];

    let graph_only = TraversalContext4D {
        graph_weight: 1.0,
        spatial_weight: 0.0,
        ..TraversalContext4D::default()
    };
    let spatial_aware = TraversalContext4D {
        graph_weight: 1.0,
        spatial_weight: 1.0,
        ..TraversalContext4D::default()
    };

    let graph_path = astar_find_path_4d(&nodes, 1, 3, &graph_only).unwrap();
    let spatial_path = astar_find_path_4d(&nodes, 1, 3, &spatial_aware).unwrap();

    assert_eq!(graph_path.node_ids, vec![1, 4, 3]);
    assert_eq!(spatial_path.node_ids, vec![1, 2, 3]);
}

#[test]
fn scc_4d_respects_temporal_edge_filtering() {
    let early = TraversalContext4D {
        time_window: Some(TemporalWindow { start: 10, end: 20 }),
        spatial_region: None,
        graph_weight: 1.0,
        spatial_weight: 0.0,
        temporal_weight: 0.0,
    };
    let late = TraversalContext4D {
        time_window: Some(TemporalWindow { start: 50, end: 60 }),
        spatial_region: None,
        graph_weight: 1.0,
        spatial_weight: 0.0,
        temporal_weight: 0.0,
    };

    let early_components = strongly_connected_components_4d(&test_graph(), &early);
    let late_components = strongly_connected_components_4d(&test_graph(), &late);

    assert!(early_components.iter().any(|c| c == &vec![1, 2, 3]));
    assert!(!late_components.iter().any(|c| c == &vec![1, 2, 3]));
}

fn temporal_delivery_graph() -> Vec<GraphNode4D> {
    vec![
        GraphNode4D {
            id: 1,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 2,
                    weight: 4.0,
                    begin_ts: 10,
                    end_ts: 30,
                },
                TemporalEdge {
                    dst: 3,
                    weight: 4.0,
                    begin_ts: 10,
                    end_ts: 18,
                },
            ],
        },
        GraphNode4D {
            id: 2,
            x: 1.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 6,
                    weight: 4.0,
                    begin_ts: 10,
                    end_ts: 14,
                },
                TemporalEdge {
                    dst: 5,
                    weight: 4.0,
                    begin_ts: 22,
                    end_ts: 26,
                },
            ],
        },
        GraphNode4D {
            id: 3,
            x: 0.0,
            y: 2.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 4,
                weight: 5.0,
                begin_ts: 14,
                end_ts: 25,
            }],
        },
        GraphNode4D {
            id: 4,
            x: 0.0,
            y: 4.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 6,
                weight: 4.0,
                begin_ts: 23,
                end_ts: 40,
            }],
        },
        GraphNode4D {
            id: 5,
            x: 2.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 6,
                weight: 4.0,
                begin_ts: 26,
                end_ts: 40,
            }],
        },
        GraphNode4D {
            id: 6,
            x: 3.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
    ]
}

#[test]
fn earliest_arrival_path_4d_respects_causal_edge_windows() {
    let journey = earliest_arrival_path_4d(&temporal_delivery_graph(), 1, 6, 10, None)
        .expect("temporal journey should exist");

    assert_eq!(journey.node_ids, vec![1, 3, 4, 6]);
    assert_eq!(journey.departure_time, 10);
    assert_eq!(journey.arrival_time, 27);
    assert_eq!(journey.duration, 17);
}

#[test]
fn earliest_arrival_path_4d_rejects_edge_that_expires_before_arrival() {
    let journey = earliest_arrival_path_4d(&temporal_delivery_graph(), 1, 6, 10, None).unwrap();

    assert_ne!(journey.node_ids, vec![1, 2, 6]);
}

#[test]
fn fastest_temporal_path_4d_can_depart_later_than_earliest_arrival() {
    let journey = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 10, 20, None)
        .expect("fastest journey should exist");

    assert_eq!(journey.node_ids, vec![1, 2, 5, 6]);
    assert_eq!(journey.departure_time, 18);
    assert_eq!(journey.arrival_time, 30);
    assert_eq!(journey.duration, 12);
}

#[test]
fn fastest_temporal_path_4d_rejects_inverted_window() {
    let result = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 50, 10, None);
    assert!(result.is_none());
}

#[test]
fn fastest_temporal_path_4d_rejects_oversized_window() {
    let result = fastest_temporal_path_4d(&temporal_delivery_graph(), 1, 6, 0, u64::MAX, None);
    assert!(result.is_none());
}

fn temporal_bottleneck_graph() -> Vec<GraphNode4D> {
    vec![
        GraphNode4D {
            id: 1,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 2,
                    weight: 1.0,
                    begin_ts: 0,
                    end_ts: 100,
                },
                TemporalEdge {
                    dst: 4,
                    weight: 1.0,
                    begin_ts: 40,
                    end_ts: 100,
                },
            ],
        },
        GraphNode4D {
            id: 2,
            x: 1.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 3,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
        GraphNode4D {
            id: 3,
            x: 2.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 6,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
        GraphNode4D {
            id: 4,
            x: 0.0,
            y: 2.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 5,
                weight: 1.0,
                begin_ts: 0,
                end_ts: 100,
            }],
        },
        GraphNode4D {
            id: 5,
            x: 1.0,
            y: 2.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 3,
                    weight: 1.0,
                    begin_ts: 40,
                    end_ts: 100,
                },
                TemporalEdge {
                    dst: 6,
                    weight: 1.0,
                    begin_ts: 40,
                    end_ts: 100,
                },
            ],
        },
        GraphNode4D {
            id: 6,
            x: 3.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
    ]
}

fn bottleneck_context(start: u64, end: u64) -> TraversalContext4D {
    TraversalContext4D {
        time_window: Some(TemporalWindow { start, end }),
        spatial_region: Some(SpatialRegion::Sphere {
            center: Vec3::ZERO,
            radius: 10.0,
        }),
        ..TraversalContext4D::default()
    }
}

#[test]
fn articulation_points_4d_change_across_temporal_windows() {
    let early = articulation_points_4d(&temporal_bottleneck_graph(), &bottleneck_context(10, 20));
    let late = articulation_points_4d(&temporal_bottleneck_graph(), &bottleneck_context(50, 60));

    assert_eq!(early, vec![2, 3]);
    assert!(late.is_empty());
}

#[test]
fn bridges_4d_change_across_temporal_windows() {
    let early = bridges_4d(&temporal_bottleneck_graph(), &bottleneck_context(10, 20));
    let late = bridges_4d(&temporal_bottleneck_graph(), &bottleneck_context(50, 60));

    assert_eq!(early, vec![(1, 2), (2, 3), (3, 6), (4, 5)]);
    assert!(late.is_empty());
}

fn signal_propagation_graph() -> Vec<GraphNode4D> {
    vec![
        GraphNode4D {
            id: 1,
            x: 0.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![
                TemporalEdge {
                    dst: 2,
                    weight: 4.0,
                    begin_ts: 10,
                    end_ts: 30,
                },
                TemporalEdge {
                    dst: 4,
                    weight: 6.0,
                    begin_ts: 10,
                    end_ts: 30,
                },
            ],
        },
        GraphNode4D {
            id: 2,
            x: 1.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 3,
                weight: 5.0,
                begin_ts: 14,
                end_ts: 30,
            }],
        },
        GraphNode4D {
            id: 3,
            x: 2.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 6,
                weight: 4.0,
                begin_ts: 10,
                end_ts: 18,
            }],
        },
        GraphNode4D {
            id: 4,
            x: 0.0,
            y: 2.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![TemporalEdge {
                dst: 5,
                weight: 8.0,
                begin_ts: 16,
                end_ts: 40,
            }],
        },
        GraphNode4D {
            id: 5,
            x: 1.0,
            y: 2.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
        GraphNode4D {
            id: 6,
            x: 3.0,
            y: 0.0,
            z: 0.0,
            begin_ts: 0,
            end_ts: 100,
            properties: Default::default(),
            successors: vec![],
        },
    ]
}

#[test]
fn time_dependent_dijkstra_4d_computes_arrival_wave() {
    let result = time_dependent_dijkstra_4d(&signal_propagation_graph(), 1, 10, None)
        .expect("start node should be usable");

    let arrivals: Vec<(u64, u64, Vec<u64>)> = result
        .reachable
        .iter()
        .map(|arrival| (arrival.node_id, arrival.arrival_time, arrival.path.clone()))
        .collect();

    assert_eq!(
        arrivals,
        vec![
            (1, 10, vec![1]),
            (2, 14, vec![1, 2]),
            (4, 16, vec![1, 4]),
            (3, 19, vec![1, 2, 3]),
            (5, 24, vec![1, 4, 5]),
        ]
    );
    assert_eq!(result.unreachable, vec![6]);
}
