use super::*;
use crate::io::{NodeGraphPanInertiaTuning, NodeGraphPanOnDragButtons};
use crate::runtime::viewport::{
    ViewportDragPanInput, ViewportGestureContext, ViewportGestureRejection,
    ViewportPanInertiaRequest, ViewportPointerButton, plan_viewport_pan_inertia,
};

fn viewport_drag_rejection_scenario(
    name: &'static str,
    pan_on_drag: NodeGraphPanOnDragButtons,
    context: ViewportGestureContext,
    input: ViewportDragPanInput,
    rejection: ViewportGestureRejection,
) -> ConformanceScenario {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let mut editor_config = crate::io::NodeGraphEditorConfig::default();
    editor_config.interaction.pan_on_drag = pan_on_drag;

    ConformanceScenario::new(name, graph)
        .with_editor_config(editor_config)
        .with_xyflow_callbacks()
        .with_actions([
            ConformanceAction::expect_viewport_drag_pan_gesture_rejected(context, input, rejection),
        ])
        .with_expected_trace([])
}

#[test]
fn conformance_runner_records_viewport_pan_zoom_fixture_and_callbacks() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();

    let pan_start = ViewportMoveStart {
        kind: ViewportMoveKind::PanDrag,
        pan: CanvasPoint::default(),
        zoom: 1.0,
    };
    let pan_start_event = NodeGraphGestureEvent::ViewportMoveStart(pan_start);
    let pan = CanvasPoint { x: 40.0, y: -10.0 };
    let pan_update = ViewportMove {
        kind: ViewportMoveKind::PanDrag,
        pan,
        zoom: 1.0,
    };
    let pan_update_event = NodeGraphGestureEvent::ViewportMove(pan_update);
    let pan_end = ViewportMoveEnd {
        kind: ViewportMoveKind::PanDrag,
        pan,
        zoom: 1.0,
        outcome: ViewportMoveEndOutcome::Ended,
    };
    let pan_end_event = NodeGraphGestureEvent::ViewportMoveEnd(pan_end);

    let zoom_start = ViewportMoveStart {
        kind: ViewportMoveKind::ZoomWheel,
        pan,
        zoom: 1.0,
    };
    let zoom_start_event = NodeGraphGestureEvent::ViewportMoveStart(zoom_start);
    let zoomed_pan = CanvasPoint { x: -10.0, y: -35.0 };
    let zoom_update = ViewportMove {
        kind: ViewportMoveKind::ZoomWheel,
        pan: zoomed_pan,
        zoom: 2.0,
    };
    let zoom_update_event = NodeGraphGestureEvent::ViewportMove(zoom_update);
    let zoom_end = ViewportMoveEnd {
        kind: ViewportMoveKind::ZoomWheel,
        pan: zoomed_pan,
        zoom: 2.0,
        outcome: ViewportMoveEndOutcome::Ended,
    };
    let zoom_end_event = NodeGraphGestureEvent::ViewportMoveEnd(zoom_end);

    let scenario = ConformanceScenario::new("viewport pan zoom fixture", graph)
        .with_xyflow_callbacks()
        .with_actions([
            ConformanceAction::emit_gesture(pan_start_event.clone()),
            ConformanceAction::apply_viewport_pan(ViewportPanRequest::new(CanvasPoint {
                x: 40.0,
                y: -10.0,
            })),
            ConformanceAction::emit_gesture(pan_update_event.clone()),
            ConformanceAction::emit_gesture(pan_end_event.clone()),
            ConformanceAction::emit_gesture(zoom_start_event.clone()),
            ConformanceAction::apply_viewport_zoom(ViewportZoomRequest::new(
                CanvasPoint { x: 100.0, y: 50.0 },
                2.0,
                0.5,
                4.0,
            )),
            ConformanceAction::emit_gesture(zoom_update_event.clone()),
            ConformanceAction::emit_gesture(zoom_end_event.clone()),
        ])
        .with_expected_trace([
            ConformanceTraceEvent::gesture(pan_start_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveStart(pan_start)),
            ConformanceTraceEvent::viewport(pan, 1.0),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport { pan, zoom: 1.0 }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan,
                zoom: 1.0,
            }),
            ConformanceTraceEvent::gesture(pan_update_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMove(pan_update)),
            ConformanceTraceEvent::gesture(pan_end_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveEnd(pan_end)),
            ConformanceTraceEvent::gesture(zoom_start_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveStart(
                zoom_start,
            )),
            ConformanceTraceEvent::viewport(zoomed_pan, 2.0),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport {
                    pan: zoomed_pan,
                    zoom: 2.0,
                }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan: zoomed_pan,
                zoom: 2.0,
            }),
            ConformanceTraceEvent::gesture(zoom_update_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMove(zoom_update)),
            ConformanceTraceEvent::gesture(zoom_end_event),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportMoveEnd(zoom_end)),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_applies_constrained_viewport_pan_fixture() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let mut editor_config = crate::io::NodeGraphEditorConfig::default();
    editor_config.interaction.translate_extent = Some(CanvasRect {
        origin: CanvasPoint { x: 0.0, y: 0.0 },
        size: CanvasSize {
            width: 100.0,
            height: 100.0,
        },
    });

    let scenario = ConformanceScenario::new("viewport constrained pan fixture", graph)
        .with_editor_config(editor_config)
        .with_actions([ConformanceAction::apply_viewport_pan_constrained(
            ViewportPanRequest::new(CanvasPoint {
                x: 400.0,
                y: -300.0,
            }),
            CanvasSize {
                width: 50.0,
                height: 50.0,
            },
        )])
        .with_expected_trace([ConformanceTraceEvent::viewport(
            CanvasPoint { x: 0.0, y: -50.0 },
            1.0,
        )]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_records_auto_pan_fixture_and_callbacks() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let mut editor_config = crate::io::NodeGraphEditorConfig::default();
    editor_config.interaction.auto_pan.speed = 100.0;
    editor_config.interaction.auto_pan.margin = 20.0;

    let pan = CanvasPoint { x: -50.0, y: 0.0 };
    let scenario = ConformanceScenario::new("auto-pan fixture", graph)
        .with_editor_config(editor_config)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_auto_pan(AutoPanRequest::new(
            AutoPanActivation::Always,
            CanvasPoint { x: 190.0, y: 50.0 },
            CanvasSize {
                width: 200.0,
                height: 100.0,
            },
            1.0,
        ))])
        .with_expected_trace([
            ConformanceTraceEvent::viewport(pan, 1.0),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport { pan, zoom: 1.0 }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan,
                zoom: 1.0,
            }),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_records_selection_auto_pan_fixture_and_callbacks() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let mut editor_config = crate::io::NodeGraphEditorConfig::default();
    editor_config.interaction.auto_pan.on_node_drag = false;
    editor_config.interaction.auto_pan.on_connect = false;
    editor_config.interaction.auto_pan.on_node_focus = false;
    editor_config.interaction.auto_pan.speed = 100.0;
    editor_config.interaction.auto_pan.margin = 20.0;

    let pan = CanvasPoint { x: -50.0, y: 0.0 };
    let scenario = ConformanceScenario::new("selection auto-pan fixture", graph)
        .with_editor_config(editor_config)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_selection_auto_pan(
            SelectionAutoPanRequest::new(
                CanvasPoint { x: 190.0, y: 50.0 },
                CanvasSize {
                    width: 200.0,
                    height: 100.0,
                },
                1.0,
            ),
        )])
        .with_expected_trace([
            ConformanceTraceEvent::viewport(pan, 1.0),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport { pan, zoom: 1.0 }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan,
                zoom: 1.0,
            }),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_rejects_viewport_drag_pan_when_selection_modifier_claims_pointer() {
    let scenario = viewport_drag_rejection_scenario(
        "viewport selection modifier suppresses drag pan",
        NodeGraphPanOnDragButtons {
            left: true,
            middle: false,
            right: false,
        },
        ViewportGestureContext {
            selection_key_pressed: true,
            ..ViewportGestureContext::idle()
        },
        ViewportDragPanInput::new(ViewportPointerButton::Left, CanvasPoint { x: 10.0, y: 4.0 }),
        ViewportGestureRejection::PanOnDragDisabled,
    );

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}

#[test]
fn conformance_runner_rejects_viewport_drag_pan_when_connection_claims_pointer() {
    let scenario = viewport_drag_rejection_scenario(
        "viewport connection suppresses drag pan",
        NodeGraphPanOnDragButtons {
            left: false,
            middle: false,
            right: true,
        },
        ViewportGestureContext {
            connection_in_progress: true,
            ..ViewportGestureContext::idle()
        },
        ViewportDragPanInput::new(
            ViewportPointerButton::Right,
            CanvasPoint { x: 10.0, y: 4.0 },
        ),
        ViewportGestureRejection::ConnectionInProgress,
    );

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}

#[test]
fn conformance_runner_asserts_viewport_animation_frame_without_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let from = ViewportTransform::new(CanvasPoint { x: 0.0, y: 0.0 }, 1.0).unwrap();
    let to = ViewportTransform::new(CanvasPoint { x: 100.0, y: -50.0 }, 3.0).unwrap();
    let expected = ViewportAnimationFrame {
        elapsed_seconds: 0.5,
        progress: 0.25,
        eased_progress: 0.0625,
        transform: ViewportTransform::new(CanvasPoint { x: 6.25, y: -3.125 }, 1.125).unwrap(),
        done: false,
    };

    let scenario = ConformanceScenario::new("viewport animation frame", graph)
        .with_actions([ConformanceAction::assert_viewport_animation_frame(
            ViewportAnimationRequest::new(from, to, ViewportAnimationOptions::new(2.0)),
            0.5,
            expected,
        )])
        .with_expected_trace([]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}

#[test]
fn conformance_runner_applies_viewport_animation_frame_with_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let from = ViewportTransform::new(CanvasPoint { x: 0.0, y: 0.0 }, 1.0).unwrap();
    let to = ViewportTransform::new(CanvasPoint { x: 80.0, y: -40.0 }, 2.0).unwrap();
    let pan = CanvasPoint { x: 40.0, y: -20.0 };
    let zoom = 1.5;

    let scenario = ConformanceScenario::new("viewport animation frame apply", graph)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_viewport_animation_frame(
            ViewportAnimationRequest::new(from, to, ViewportAnimationOptions::new(1.0)),
            0.5,
        )])
        .with_expected_trace([
            ConformanceTraceEvent::viewport(pan, zoom),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport { pan, zoom }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange { pan, zoom }),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_applies_viewport_animation_frames_with_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let from = ViewportTransform::new(CanvasPoint { x: 0.0, y: 0.0 }, 1.0).unwrap();
    let to = ViewportTransform::new(CanvasPoint { x: 80.0, y: -40.0 }, 2.0).unwrap();
    let midpoint_pan = CanvasPoint { x: 40.0, y: -20.0 };
    let endpoint_pan = CanvasPoint { x: 80.0, y: -40.0 };

    let scenario = ConformanceScenario::new("viewport animation frame sequence", graph)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_viewport_animation_frames(
            ViewportAnimationRequest::new(from, to, ViewportAnimationOptions::new(1.0)),
            [0.5, 1.0],
        )])
        .with_expected_trace([
            ConformanceTraceEvent::viewport(midpoint_pan, 1.5),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport {
                    pan: midpoint_pan,
                    zoom: 1.5,
                }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan: midpoint_pan,
                zoom: 1.5,
            }),
            ConformanceTraceEvent::viewport(endpoint_pan, 2.0),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport {
                    pan: endpoint_pan,
                    zoom: 2.0,
                }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan: endpoint_pan,
                zoom: 2.0,
            }),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_rejects_empty_viewport_animation_frame_sequence() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let from = ViewportTransform::new(CanvasPoint { x: 0.0, y: 0.0 }, 1.0).unwrap();
    let to = ViewportTransform::new(CanvasPoint { x: 80.0, y: -40.0 }, 2.0).unwrap();

    let scenario = ConformanceScenario::new("empty viewport animation frame sequence", graph)
        .with_actions([ConformanceAction::apply_viewport_animation_frames(
            ViewportAnimationRequest::new(from, to, ViewportAnimationOptions::new(1.0)),
            Vec::<f32>::new(),
        )]);

    let err = run_conformance_scenario(&scenario).expect_err("empty frame sequence should error");

    assert_eq!(err.scenario, "empty viewport animation frame sequence");
    assert_eq!(err.action_index, 0);
    assert_eq!(err.action_kind, "apply_viewport_animation_frames");
    assert!(err.message.contains("frame list was empty"));
}

#[test]
fn conformance_runner_applies_viewport_pan_inertia_frames_with_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let request = ViewportPanInertiaRequest::new(
        ViewportTransform::new(CanvasPoint::default(), 2.0).unwrap(),
        CanvasPoint { x: 1000.0, y: 0.0 },
        NodeGraphPanInertiaTuning {
            enabled: true,
            decay_per_s: 2.0,
            min_speed: 100.0,
            max_speed: 1000.0,
        },
    );
    let plan = plan_viewport_pan_inertia(request.clone()).expect("inertia plan");
    let mid = plan.frame_at(0.5).expect("mid inertia frame");
    let terminal = plan.terminal_frame().expect("terminal inertia frame");

    let scenario = ConformanceScenario::new("viewport pan inertia frame sequence", graph)
        .with_xyflow_callbacks()
        .with_actions([ConformanceAction::apply_viewport_pan_inertia_frames(
            request,
            [0.5, plan.duration_seconds],
        )])
        .with_expected_trace([
            ConformanceTraceEvent::viewport(mid.transform.pan, mid.transform.zoom),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport {
                    pan: mid.transform.pan,
                    zoom: mid.transform.zoom,
                }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan: mid.transform.pan,
                zoom: mid.transform.zoom,
            }),
            ConformanceTraceEvent::viewport(terminal.transform.pan, terminal.transform.zoom),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewChange {
                changes: vec![ConformanceViewChange::Viewport {
                    pan: terminal.transform.pan,
                    zoom: terminal.transform.zoom,
                }],
            }),
            ConformanceTraceEvent::callback(ConformanceCallbackEvent::ViewportChange {
                pan: terminal.transform.pan,
                zoom: terminal.transform.zoom,
            }),
        ]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
}

#[test]
fn conformance_runner_asserts_and_rejects_viewport_pan_inertia_without_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let tuning = NodeGraphPanInertiaTuning {
        enabled: true,
        decay_per_s: 2.0,
        min_speed: 100.0,
        max_speed: 1000.0,
    };
    let request = ViewportPanInertiaRequest::new(
        ViewportTransform::new(CanvasPoint::default(), 1.0).unwrap(),
        CanvasPoint { x: 500.0, y: 0.0 },
        tuning.clone(),
    );
    let expected = plan_viewport_pan_inertia(request.clone())
        .expect("inertia plan")
        .frame_at(0.25)
        .expect("inertia frame");

    let scenario = ConformanceScenario::new("viewport pan inertia assertion", graph)
        .with_actions([
            ConformanceAction::assert_viewport_pan_inertia_frame(request, 0.25, expected),
            ConformanceAction::expect_viewport_pan_inertia_rejected(
                ViewportPanInertiaRequest::new(
                    ViewportTransform::new(CanvasPoint::default(), 1.0).unwrap(),
                    CanvasPoint { x: 50.0, y: 0.0 },
                    tuning,
                ),
            ),
        ])
        .with_expected_trace([]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}

#[test]
fn conformance_runner_rejects_empty_viewport_pan_inertia_frame_sequence() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let request = ViewportPanInertiaRequest::new(
        ViewportTransform::new(CanvasPoint::default(), 1.0).unwrap(),
        CanvasPoint { x: 500.0, y: 0.0 },
        NodeGraphPanInertiaTuning {
            enabled: true,
            decay_per_s: 2.0,
            min_speed: 100.0,
            max_speed: 1000.0,
        },
    );

    let scenario = ConformanceScenario::new("empty viewport pan inertia frame sequence", graph)
        .with_actions([ConformanceAction::apply_viewport_pan_inertia_frames(
            request,
            Vec::<f32>::new(),
        )]);

    let err = run_conformance_scenario(&scenario).expect_err("empty frame sequence should error");

    assert_eq!(err.scenario, "empty viewport pan inertia frame sequence");
    assert_eq!(err.action_index, 0);
    assert_eq!(err.action_kind, "apply_viewport_pan_inertia_frames");
    assert!(err.message.contains("frame list was empty"));
}

#[test]
fn conformance_runner_asserts_double_click_zoom_plan_without_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let current = ViewportTransform::new(CanvasPoint { x: 10.0, y: 20.0 }, 2.0).unwrap();
    let anchor = CanvasPoint { x: 120.0, y: 60.0 };
    let input = ViewportDoubleClickZoomInput::new(
        current,
        anchor,
        2.0,
        0.5,
        3.0,
        ViewportAnimationOptions::new(0.2),
    );
    let target = ViewportTransform::new(CanvasPoint { x: -10.0, y: 10.0 }, 3.0).unwrap();
    let expected = ViewportAnimationPlan {
        from: current,
        to: target,
        duration_seconds: 0.2,
        easing: ViewportAnimationEasing::CubicInOut,
    };

    let scenario = ConformanceScenario::new("viewport double-click zoom", graph)
        .with_actions([ConformanceAction::assert_viewport_double_click_zoom(
            input, expected,
        )])
        .with_expected_trace([]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}

#[test]
fn conformance_runner_asserts_double_click_zoom_rejection_without_trace() {
    let (graph, _node_id, _b, _out_port, _in_port, _edge_id) = make_graph();
    let mut editor_config = crate::io::NodeGraphEditorConfig::default();
    editor_config.interaction.zoom_on_double_click = false;
    let current = ViewportTransform::new(CanvasPoint::default(), 1.0).unwrap();

    let scenario = ConformanceScenario::new("viewport double-click zoom rejection", graph)
        .with_editor_config(editor_config)
        .with_actions([
            ConformanceAction::expect_viewport_double_click_zoom_rejected(
                ViewportDoubleClickZoomInput::new(
                    current,
                    CanvasPoint { x: 10.0, y: 10.0 },
                    2.0,
                    0.5,
                    4.0,
                    ViewportAnimationOptions::new(0.2),
                ),
                ViewportGestureRejection::DoubleClickZoomDisabled,
            ),
        ])
        .with_expected_trace([]);

    let report = run_conformance_scenario(&scenario).expect("fixture should run");

    assert!(report.is_match(), "{report}");
    assert!(report.actual_trace().is_empty());
}
