use crate::runtime::connection::{
    CONNECT_EDGE_TRANSACTION_LABEL, ClosestConnectionHandleInput, ConnectEdgeRequest,
    ConnectionDragActivationInput, ConnectionEndIntent, ConnectionHandleCandidate,
    ConnectionHandleConnection, ConnectionHandleIndicatorInput, ConnectionHandleRef,
    ConnectionHandleValidity, ConnectionLifecycleState, ConnectionTargetCandidate,
    ConnectionTargetFromHandlesInput, ConnectionTargetHandle, ConnectionTargetInput,
    closest_connection_handle, connection_drag_threshold_met, connection_handle_validity,
    new_connection_start, resolve_connection_handle_indicator, resolve_connection_lifecycle,
    resolve_connection_target, resolve_connection_target_from_handles,
};
use crate::runtime::geometry::{HandleBounds, HandlePosition};
use crate::runtime::tests::fixtures::{GraphFixtureUpdateExt, fixture_insert_port};
use crate::schema::{MenuDescriptor, MenuSurface};
use jellyflow_core::core::{
    CanvasPoint, CanvasRect, CanvasSize, EdgeId, NodeId, Port, PortCapacity, PortDirection, PortId,
    PortKey, PortKind,
};
use jellyflow_core::interaction::NodeGraphConnectionMode;
use jellyflow_core::ops::GraphOp;
use jellyflow_core::types::TypeDesc;

#[test]
fn connection_drag_threshold_uses_xyflow_squared_screen_distance_semantics() {
    assert!(
        connection_drag_threshold_met(ConnectionDragActivationInput::new(
            CanvasPoint::default(),
            0.0,
        )),
        "threshold zero starts immediately"
    );
    assert!(
        !connection_drag_threshold_met(ConnectionDragActivationInput::new(
            CanvasPoint { x: 3.0, y: 4.0 },
            5.0,
        )),
        "distance equal to threshold does not start connection"
    );
    assert!(
        connection_drag_threshold_met(ConnectionDragActivationInput::new(
            CanvasPoint { x: 3.0, y: 4.0 },
            4.99,
        )),
        "distance greater than threshold starts connection"
    );
    assert!(
        !connection_drag_threshold_met(ConnectionDragActivationInput::new(
            CanvasPoint { x: 3.0, y: 4.0 },
            -5.0,
        )),
        "negative thresholds follow XyFlow's squared-threshold shape"
    );
}

#[test]
fn connection_drag_threshold_rejects_non_finite_inputs() {
    assert!(!connection_drag_threshold_met(
        ConnectionDragActivationInput::new(
            CanvasPoint {
                x: f32::INFINITY,
                y: 0.0,
            },
            1.0,
        ),
    ));
    assert!(!connection_drag_threshold_met(
        ConnectionDragActivationInput::new(CanvasPoint { x: 1.0, y: 0.0 }, f32::NAN),
    ));
}

#[test]
fn closest_connection_handle_prefers_opposite_direction_on_equal_distance() {
    let from = handle_ref(PortDirection::Out);
    let same_distance_out = candidate(PortDirection::Out, CanvasPoint { x: 10.0, y: 10.0 });
    let same_distance_in = candidate(PortDirection::In, CanvasPoint { x: 10.0, y: 10.0 });
    let far_in = candidate(PortDirection::In, CanvasPoint { x: 100.0, y: 100.0 });
    let candidates = [same_distance_out, same_distance_in, far_in];

    let result = closest_connection_handle(ClosestConnectionHandleInput::new(
        CanvasPoint { x: 15.0, y: 15.0 },
        10.0,
        from,
        &candidates,
    ))
    .expect("closest handle");

    assert_eq!(result.handle.direction, PortDirection::In);
    assert_eq!(result.center, CanvasPoint { x: 15.0, y: 15.0 });
    assert_eq!(result.distance, 0.0);
}

#[test]
fn closest_connection_handle_skips_starting_handle_and_rejects_invalid_inputs() {
    let from = handle_ref(PortDirection::Out);
    let same_handle = ConnectionHandleCandidate::new(
        from,
        node_rect(CanvasPoint { x: 0.0, y: 0.0 }),
        handle_bounds(CanvasPoint { x: 10.0, y: 10.0 }),
    );
    let outside_radius = candidate(PortDirection::In, CanvasPoint { x: 100.0, y: 100.0 });
    let candidates = [same_handle, outside_radius];

    assert!(
        closest_connection_handle(ClosestConnectionHandleInput::new(
            CanvasPoint { x: 15.0, y: 15.0 },
            10.0,
            from,
            &candidates,
        ))
        .is_none()
    );
    assert!(
        closest_connection_handle(ClosestConnectionHandleInput::new(
            CanvasPoint {
                x: f32::NAN,
                y: 15.0
            },
            10.0,
            from,
            &candidates,
        ))
        .is_none()
    );
    assert!(
        closest_connection_handle(ClosestConnectionHandleInput::new(
            CanvasPoint { x: 15.0, y: 15.0 },
            -1.0,
            from,
            &candidates,
        ))
        .is_none()
    );
}

#[test]
fn connection_handle_validity_matches_xyflow_true_false_null_shape() {
    assert_eq!(
        connection_handle_validity(false, true),
        ConnectionHandleValidity::Valid
    );
    assert_eq!(
        connection_handle_validity(true, false),
        ConnectionHandleValidity::Invalid
    );
    assert_eq!(
        connection_handle_validity(false, false),
        ConnectionHandleValidity::NoHandle
    );
}

#[test]
fn resolve_connection_target_orders_endpoints_like_xyflow() {
    let source = handle_ref(PortDirection::Out);
    let target = target_handle(PortDirection::In);

    let result = resolve_connection_target(ConnectionTargetInput::new(
        source,
        Some(target),
        NodeGraphConnectionMode::Strict,
        true,
    ));

    assert!(result.is_handle_valid);
    assert_eq!(result.feedback, ConnectionHandleValidity::Valid);
    assert_eq!(
        result.connection,
        Some(ConnectionHandleConnection {
            source,
            target: target.handle,
        })
    );

    let target_start = handle_ref(PortDirection::In);
    let source_target = target_handle(PortDirection::Out);
    let reversed = resolve_connection_target(ConnectionTargetInput::new(
        target_start,
        Some(source_target),
        NodeGraphConnectionMode::Strict,
        true,
    ));

    assert_eq!(
        reversed.connection,
        Some(ConnectionHandleConnection {
            source: source_target.handle,
            target: target_start,
        })
    );
}

#[test]
fn resolve_connection_target_matches_strict_and_loose_mode_rules() {
    let from = handle_ref(PortDirection::Out);
    let same_direction_target = target_handle(PortDirection::Out);

    let strict = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(same_direction_target),
        NodeGraphConnectionMode::Strict,
        true,
    ));
    assert!(!strict.is_handle_valid);
    assert_eq!(strict.feedback, ConnectionHandleValidity::Invalid);
    assert!(strict.connection.is_some());

    let loose = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(same_direction_target),
        NodeGraphConnectionMode::Loose,
        true,
    ));
    assert!(loose.is_handle_valid);
    assert_eq!(loose.feedback, ConnectionHandleValidity::Valid);

    let same_handle = ConnectionTargetHandle::new(from, true, true);
    let same_handle_loose = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(same_handle),
        NodeGraphConnectionMode::Loose,
        true,
    ));
    assert!(!same_handle_loose.is_handle_valid);
    assert_eq!(
        same_handle_loose.feedback,
        ConnectionHandleValidity::Invalid
    );
}

#[test]
fn resolve_connection_target_applies_target_connectability_and_custom_validity() {
    let from = handle_ref(PortDirection::Out);
    let blocked_target = ConnectionTargetHandle::new(handle_ref(PortDirection::In), true, false);
    let blocked = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(blocked_target),
        NodeGraphConnectionMode::Strict,
        true,
    ));
    assert!(!blocked.is_handle_valid);
    assert_eq!(blocked.feedback, ConnectionHandleValidity::Invalid);

    let custom_rejected = resolve_connection_target(
        ConnectionTargetInput::new(
            from,
            Some(target_handle(PortDirection::In)),
            NodeGraphConnectionMode::Strict,
            true,
        )
        .with_connection_validity(false),
    );
    assert!(!custom_rejected.is_handle_valid);
    assert_eq!(custom_rejected.feedback, ConnectionHandleValidity::Invalid);
}

#[test]
fn connection_target_input_json_defaults_custom_validity_to_true() {
    let from = handle_ref(PortDirection::Out);
    let target = target_handle(PortDirection::In);
    let input =
        ConnectionTargetInput::new(from, Some(target), NodeGraphConnectionMode::Strict, true);
    let mut encoded = serde_json::to_value(input).expect("serialize connection target input");
    encoded
        .as_object_mut()
        .expect("connection target input object")
        .remove("is_valid_connection");

    let decoded: ConnectionTargetInput =
        serde_json::from_value(encoded).expect("deserialize connection target input");

    assert!(decoded.is_valid_connection);
    assert!(resolve_connection_target(decoded).is_handle_valid);
}

#[test]
fn resolve_connection_target_preserves_xyflow_feedback_null_when_no_handle_is_close() {
    let result = resolve_connection_target(ConnectionTargetInput::new(
        handle_ref(PortDirection::Out),
        None,
        NodeGraphConnectionMode::Strict,
        false,
    ));

    assert_eq!(result.target, None);
    assert_eq!(result.connection, None);
    assert!(!result.is_handle_valid);
    assert_eq!(result.feedback, ConnectionHandleValidity::NoHandle);
}

#[test]
fn resolve_connection_target_from_handles_uses_closest_handle_then_policy() {
    let from = handle_ref(PortDirection::Out);
    let valid_far = target_candidate(
        PortDirection::In,
        CanvasPoint { x: 80.0, y: 80.0 },
        true,
        true,
    );
    let blocked_near = target_candidate(
        PortDirection::In,
        CanvasPoint { x: 10.0, y: 10.0 },
        true,
        false,
    );
    let candidates = [valid_far, blocked_near];

    let result = resolve_connection_target_from_handles(ConnectionTargetFromHandlesInput::new(
        CanvasPoint { x: 15.0, y: 15.0 },
        120.0,
        from,
        &candidates,
        NodeGraphConnectionMode::Strict,
    ));

    assert_eq!(result.target, Some(blocked_near.target));
    assert_eq!(
        result.connection,
        Some(ConnectionHandleConnection {
            source: from,
            target: blocked_near.target.handle,
        })
    );
    assert!(!result.is_handle_valid);
    assert_eq!(result.feedback, ConnectionHandleValidity::Invalid);
}

#[test]
fn resolve_connection_target_from_handles_preserves_no_handle_feedback_outside_radius() {
    let from = handle_ref(PortDirection::Out);
    let candidate = target_candidate(
        PortDirection::In,
        CanvasPoint { x: 80.0, y: 80.0 },
        true,
        true,
    );
    let candidates = [candidate];

    let result = resolve_connection_target_from_handles(ConnectionTargetFromHandlesInput::new(
        CanvasPoint { x: 0.0, y: 0.0 },
        10.0,
        from,
        &candidates,
        NodeGraphConnectionMode::Strict,
    ));

    assert_eq!(result.target, None);
    assert_eq!(result.connection, None);
    assert!(!result.is_handle_valid);
    assert_eq!(result.feedback, ConnectionHandleValidity::NoHandle);
}

#[test]
fn connection_handle_indicator_shows_start_and_click_end_states_like_xyflow() {
    let handle = handle_ref(PortDirection::Out);
    let idle = resolve_connection_handle_indicator(
        ConnectionHandleIndicatorInput::new(handle, NodeGraphConnectionMode::Strict)
            .with_connectability(true, true, false),
    );
    assert!(idle.show_connection_indicator);
    assert!(!idle.connection_in_progress);

    let click_connecting = resolve_connection_handle_indicator(
        ConnectionHandleIndicatorInput::new(handle, NodeGraphConnectionMode::Strict)
            .with_click_start(Some(handle))
            .with_connectability(true, false, true),
    );
    assert!(click_connecting.click_connecting);
    assert!(click_connecting.click_connection_in_progress);
    assert!(click_connecting.show_connection_indicator);
}

#[test]
fn connection_handle_indicator_filters_strict_and_loose_end_handles() {
    let from = handle_ref(PortDirection::Out);
    let strict_target = target_handle(PortDirection::In).handle;
    let strict_same_direction = target_handle(PortDirection::Out).handle;

    let target_indicator = resolve_connection_handle_indicator(
        ConnectionHandleIndicatorInput::new(strict_target, NodeGraphConnectionMode::Strict)
            .with_connection(
                Some(from),
                Some(strict_target),
                ConnectionHandleValidity::Valid,
            ),
    );
    assert!(target_indicator.connecting_to);
    assert!(target_indicator.possible_end_handle);
    assert!(target_indicator.valid);
    assert!(target_indicator.show_connection_indicator);

    let same_direction_indicator = resolve_connection_handle_indicator(
        ConnectionHandleIndicatorInput::new(strict_same_direction, NodeGraphConnectionMode::Strict)
            .with_connection(
                Some(from),
                Some(strict_same_direction),
                ConnectionHandleValidity::Invalid,
            ),
    );
    assert!(!same_direction_indicator.possible_end_handle);
    assert!(!same_direction_indicator.valid);
    assert!(!same_direction_indicator.show_connection_indicator);

    let same_handle_loose =
        resolve_connection_handle_indicator(
            ConnectionHandleIndicatorInput::new(from, NodeGraphConnectionMode::Loose)
                .with_connection(Some(from), Some(from), ConnectionHandleValidity::Invalid),
        );
    assert!(same_handle_loose.connecting_from);
    assert!(!same_handle_loose.possible_end_handle);
    assert!(!same_handle_loose.show_connection_indicator);
}

#[test]
fn store_apply_connect_edge_commits_labeled_add_edge_transaction() {
    let (mut graph, _a, b, out_port, _in_port, _edge_id) = super::fixtures::make_graph();
    let next_in = PortId::new();
    graph
        .update_node(&b, |node| node.ports.push(next_in))
        .expect("target node");
    fixture_insert_port(
        &mut graph,
        next_in,
        Port {
            node: b,
            key: PortKey::new("in2"),
            dir: PortDirection::In,
            kind: PortKind::Data,
            capacity: PortCapacity::Multi,
            connectable: None,
            connectable_start: None,
            connectable_end: None,
            ty: None,
            data: serde_json::Value::Null,
        },
    );
    let mut store = crate::runtime::store::NodeGraphStore::new(
        graph,
        crate::io::NodeGraphViewState::default(),
        crate::runtime::tests::fixtures::default_editor_config(),
    );
    let edge_id = EdgeId::from_u128(900);

    let outcome = store
        .apply_connect_edge(
            ConnectEdgeRequest::new(out_port, next_in, NodeGraphConnectionMode::Strict)
                .with_edge_id(edge_id),
        )
        .expect("connect edge should dispatch")
        .expect("connect edge should commit");

    assert_eq!(
        outcome.committed().label(),
        Some(CONNECT_EDGE_TRANSACTION_LABEL)
    );
    match outcome.committed().ops() {
        [GraphOp::AddEdge { id, edge }] => {
            assert_eq!(*id, edge_id);
            assert_eq!(edge.from, out_port);
            assert_eq!(edge.to, next_in);
        }
        other => panic!("expected single add-edge op, got {other:#?}"),
    };
    assert!(store.graph().edges().contains_key(&edge_id));
}

#[test]
fn store_apply_connect_edge_rejects_incompatible_typed_data_ports() {
    let (mut graph, _a, b, out_port, _in_port, _edge_id) = super::fixtures::make_graph();
    graph
        .update_port(&out_port, |port| port.ty = Some(TypeDesc::Int))
        .expect("source port exists");
    let string_in = PortId::new();
    graph
        .update_node(&b, |node| node.ports.push(string_in))
        .expect("target node exists");
    fixture_insert_port(
        &mut graph,
        string_in,
        Port {
            node: b,
            key: PortKey::new("string"),
            dir: PortDirection::In,
            kind: PortKind::Data,
            capacity: PortCapacity::Multi,
            connectable: None,
            connectable_start: None,
            connectable_end: None,
            ty: Some(TypeDesc::String),
            data: serde_json::Value::Null,
        },
    );
    let mut store = crate::runtime::store::NodeGraphStore::new(
        graph,
        crate::io::NodeGraphViewState::default(),
        crate::runtime::tests::fixtures::default_editor_config(),
    );

    let err = store
        .apply_connect_edge(ConnectEdgeRequest::new(
            out_port,
            string_in,
            NodeGraphConnectionMode::Strict,
        ))
        .expect_err("typed mismatch should be rejected by the default store connect path");

    let diagnostics = err.diagnostics().expect("typed rejection diagnostics");
    assert!(diagnostics.iter().any(|diagnostic| {
        diagnostic.key == "connect.type_mismatch"
            && diagnostic.message.contains("types are incompatible")
    }));
}

#[test]
fn connection_lifecycle_commits_valid_hover_target() {
    let from = handle_ref(PortDirection::Out);
    let target = target_handle(PortDirection::In);
    let hover = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(target),
        NodeGraphConnectionMode::Strict,
        true,
    ));
    let start = new_connection_start(from.port, [from.port], NodeGraphConnectionMode::Strict);

    let result =
        resolve_connection_lifecycle(start.clone(), Some(hover), ConnectionEndIntent::Complete);

    assert_eq!(result.start, start);
    assert_eq!(result.state, ConnectionLifecycleState::Committed);
    assert!(result.did_commit());
    assert_eq!(result.target, Some(target.handle.port));
    assert_eq!(result.end.target, Some(target.handle.port));
    assert_eq!(
        result.end.outcome,
        crate::runtime::events::ConnectEndOutcome::Committed
    );
    assert_eq!(
        result.connection,
        Some(ConnectionHandleConnection {
            source: from,
            target: target.handle,
        })
    );
}

#[test]
fn connection_lifecycle_rejects_invalid_hover_target_without_committing() {
    let from = handle_ref(PortDirection::Out);
    let target = ConnectionTargetHandle::new(handle_ref(PortDirection::In), true, false);
    let hover = resolve_connection_target(ConnectionTargetInput::new(
        from,
        Some(target),
        NodeGraphConnectionMode::Strict,
        true,
    ));
    let start = new_connection_start(from.port, [from.port], NodeGraphConnectionMode::Strict);

    let result = resolve_connection_lifecycle(start, Some(hover), ConnectionEndIntent::Complete);

    assert_eq!(result.state, ConnectionLifecycleState::Rejected);
    assert!(!result.did_commit());
    assert_eq!(result.target, Some(target.handle.port));
    assert_eq!(result.end.target, Some(target.handle.port));
    assert_eq!(
        result.end.outcome,
        crate::runtime::events::ConnectEndOutcome::Rejected
    );
    assert_eq!(
        result.hover.unwrap().feedback,
        ConnectionHandleValidity::Invalid
    );
}

#[test]
fn connection_lifecycle_distinguishes_cancel_empty_drop_and_noop() {
    let from = handle_ref(PortDirection::Out);
    let start = new_connection_start(from.port, [from.port], NodeGraphConnectionMode::Strict);
    let canceled = resolve_connection_lifecycle(start.clone(), None, ConnectionEndIntent::Cancel);
    assert_eq!(canceled.state, ConnectionLifecycleState::Canceled);
    assert_eq!(
        canceled.end.outcome,
        crate::runtime::events::ConnectEndOutcome::Canceled
    );

    let dropped = resolve_connection_lifecycle(
        start.clone(),
        None,
        ConnectionEndIntent::DropOnPane {
            pointer: CanvasPoint { x: 40.0, y: 50.0 },
            menu: Some(
                MenuDescriptor::new("menu.dropped_wire", MenuSurface::DroppedWire)
                    .with_label("Insert node")
                    .with_action_key("action.insert.compatible_node"),
            ),
        },
    );
    assert_eq!(dropped.state, ConnectionLifecycleState::DroppedOnPane);
    assert!(dropped.opens_dropped_wire_menu());
    assert_eq!(dropped.dropped_at, Some(CanvasPoint { x: 40.0, y: 50.0 }));
    assert_eq!(
        dropped
            .dropped_wire_menu
            .as_ref()
            .and_then(|menu| menu.action_keys.first())
            .map(String::as_str),
        Some("action.insert.compatible_node")
    );
    assert_eq!(
        dropped.end.outcome,
        crate::runtime::events::ConnectEndOutcome::OpenInsertNodePicker
    );

    let empty_hover = resolve_connection_target(ConnectionTargetInput::new(
        from,
        None,
        NodeGraphConnectionMode::Strict,
        false,
    ));
    let noop =
        resolve_connection_lifecycle(start, Some(empty_hover), ConnectionEndIntent::Complete);
    assert_eq!(noop.state, ConnectionLifecycleState::NoOp);
    assert_eq!(
        noop.end.outcome,
        crate::runtime::events::ConnectEndOutcome::NoOp
    );
    assert_eq!(
        noop.hover.unwrap().feedback,
        ConnectionHandleValidity::NoHandle
    );
}

fn handle_ref(direction: PortDirection) -> ConnectionHandleRef {
    ConnectionHandleRef::new(NodeId::new(), PortId::new(), direction)
}

fn target_handle(direction: PortDirection) -> ConnectionTargetHandle {
    ConnectionTargetHandle::new(handle_ref(direction), true, true)
}

fn target_candidate(
    direction: PortDirection,
    handle_origin: CanvasPoint,
    connectable: bool,
    connectable_end: bool,
) -> ConnectionTargetCandidate {
    ConnectionTargetCandidate::new(
        ConnectionTargetHandle::new(handle_ref(direction), connectable, connectable_end),
        node_rect(CanvasPoint { x: 0.0, y: 0.0 }),
        handle_bounds(handle_origin),
    )
}

fn candidate(direction: PortDirection, handle_origin: CanvasPoint) -> ConnectionHandleCandidate {
    ConnectionHandleCandidate::new(
        handle_ref(direction),
        node_rect(CanvasPoint { x: 0.0, y: 0.0 }),
        handle_bounds(handle_origin),
    )
}

fn node_rect(origin: CanvasPoint) -> CanvasRect {
    CanvasRect {
        origin,
        size: CanvasSize {
            width: 200.0,
            height: 120.0,
        },
    }
}

fn handle_bounds(origin: CanvasPoint) -> HandleBounds {
    HandleBounds {
        rect: CanvasRect {
            origin,
            size: CanvasSize {
                width: 10.0,
                height: 10.0,
            },
        },
        position: HandlePosition::Right,
    }
}
