use super::*;
use crate::core::error::LoreError;
use wiremock::matchers::{body_json, header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

// ═══════════════════════════════════════════════════════════════════════
//  AC-1: GraphQL Client
// ═══════════════════════════════════════════════════════════════════════

#[tokio::test]
async fn test_graphql_query_success() {
    let server = MockServer::start().await;
    let response_body = serde_json::json!({
        "data": { "project": { "id": "1" } }
    });

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "test-token");
    let result = client
        .query("{ project { id } }", serde_json::json!({}))
        .await
        .unwrap();

    assert_eq!(result.data["project"]["id"], "1");
    assert!(!result.had_partial_errors);
    assert!(result.first_partial_error.is_none());
}

#[tokio::test]
async fn test_graphql_query_with_errors_no_data() {
    let server = MockServer::start().await;
    let response_body = serde_json::json!({
        "errors": [{ "message": "Field 'foo' not found" }]
    });

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "test-token");
    let err = client
        .query("{ foo }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::Other(msg) => {
            assert!(msg.contains("Field 'foo' not found"), "got: {msg}");
        }
        other => panic!("Expected LoreError::Other, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_graphql_auth_uses_bearer() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .and(header("Authorization", "Bearer my-secret-token"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": {"ok": true}})),
        )
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "my-secret-token");
    let result = client.query("{ ok }", serde_json::json!({})).await;
    assert!(result.is_ok());
}

#[tokio::test]
async fn test_graphql_401_maps_to_auth_failed() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(401))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "bad-token");
    let err = client
        .query("{ me }", serde_json::json!({}))
        .await
        .unwrap_err();

    assert!(matches!(err, LoreError::GitLabAuthFailed));
}

#[tokio::test]
async fn test_graphql_403_maps_to_auth_failed() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(403))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "forbidden-token");
    let err = client
        .query("{ admin }", serde_json::json!({}))
        .await
        .unwrap_err();

    assert!(matches!(err, LoreError::GitLabAuthFailed));
}

#[tokio::test]
async fn test_graphql_404_maps_to_not_found() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(404))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::GitLabNotFound { resource } => {
            assert_eq!(resource, "GraphQL endpoint");
        }
        other => panic!("Expected GitLabNotFound, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_graphql_partial_data_with_errors_returns_data() {
    let server = MockServer::start().await;
    let response_body = serde_json::json!({
        "data": { "project": { "name": "test" } },
        "errors": [{ "message": "Some field failed" }]
    });

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(&response_body))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let result = client
        .query("{ project { name } }", serde_json::json!({}))
        .await
        .unwrap();

    assert_eq!(result.data["project"]["name"], "test");
    assert!(result.had_partial_errors);
    assert_eq!(
        result.first_partial_error.as_deref(),
        Some("Some field failed")
    );
}

#[tokio::test]
async fn test_retry_after_delta_seconds() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "120"))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::GitLabRateLimited { retry_after } => {
            assert_eq!(retry_after, 120);
        }
        other => panic!("Expected GitLabRateLimited, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_retry_after_http_date_format() {
    let server = MockServer::start().await;

    let future = SystemTime::now() + Duration::from_secs(90);
    let date_str = httpdate::fmt_http_date(future);

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", date_str))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::GitLabRateLimited { retry_after } => {
            assert!(
                (85..=95).contains(&retry_after),
                "retry_after={retry_after}"
            );
        }
        other => panic!("Expected GitLabRateLimited, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_retry_after_invalid_falls_back_to_60() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "garbage"))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::GitLabRateLimited { retry_after } => {
            assert_eq!(retry_after, 60);
        }
        other => panic!("Expected GitLabRateLimited, got: {other:?}"),
    }
}

#[test]
fn test_retry_after_http_date_in_past_returns_one_second() {
    let now = SystemTime::now();
    let past = now - Duration::from_secs(120);
    let date_str = httpdate::fmt_http_date(past);

    assert_eq!(parse_retry_after_value(&date_str, now), 1);
}

#[test]
fn test_retry_after_delta_seconds_trims_whitespace() {
    let now = SystemTime::now();
    assert_eq!(parse_retry_after_value("  120  ", now), 120);
}

#[tokio::test]
async fn test_graphql_network_error() {
    let client = GraphqlClient::new("http://127.0.0.1:1", "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    assert!(
        matches!(err, LoreError::GitLabNetworkError { .. }),
        "Expected GitLabNetworkError, got: {err:?}"
    );
}

#[tokio::test]
async fn test_graphql_request_body_format() {
    let server = MockServer::start().await;

    let expected_body = serde_json::json!({
        "query": "{ project(fullPath: $path) { id } }",
        "variables": { "path": "group/repo" }
    });

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .and(body_json(&expected_body))
        .respond_with(
            ResponseTemplate::new(200)
                .set_body_json(serde_json::json!({"data": {"project": {"id": "1"}}})),
        )
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let result = client
        .query(
            "{ project(fullPath: $path) { id } }",
            serde_json::json!({"path": "group/repo"}),
        )
        .await;

    assert!(result.is_ok(), "Body format mismatch: {result:?}");
}

#[tokio::test]
async fn test_graphql_base_url_trailing_slash() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": {"ok": true}})),
        )
        .mount(&server)
        .await;

    let url_with_slash = format!("{}/", server.uri());
    let client = GraphqlClient::new(&url_with_slash, "token");
    let result = client.query("{ ok }", serde_json::json!({})).await;

    assert!(result.is_ok());
}

#[tokio::test]
async fn test_graphql_data_null_no_errors() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"data": null})))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "token");
    let err = client
        .query("{ x }", serde_json::json!({}))
        .await
        .unwrap_err();

    match err {
        LoreError::Other(msg) => {
            assert!(msg.contains("missing 'data' field"), "got: {msg}");
        }
        other => panic!("Expected LoreError::Other, got: {other:?}"),
    }
}

// ═══════════════════════════════════════════════════════════════════════
//  AC-3: Status Fetcher
// ═══════════════════════════════════════════════════════════════════════

/// Helper: build a GraphQL work-items response page with given issues.
fn make_work_items_page(
    items: &[(i64, Option<&str>)],
    has_next_page: bool,
    end_cursor: Option<&str>,
) -> serde_json::Value {
    let nodes: Vec<serde_json::Value> = items
        .iter()
        .map(|(iid, status_name)| {
            let mut widgets = vec![serde_json::json!({"__typename": "WorkItemWidgetDescription"})];
            if let Some(name) = status_name {
                widgets.push(serde_json::json!({
                    "__typename": "WorkItemWidgetStatus",
                    "status": {
                        "name": name,
                        "category": "IN_PROGRESS",
                        "color": "#1f75cb",
                        "iconName": "status-in-progress"
                    }
                }));
            }
            serde_json::json!({
                "iid": iid.to_string(),
                "widgets": widgets,
            })
        })
        .collect();

    serde_json::json!({
        "data": {
            "project": {
                "workItems": {
                    "nodes": nodes,
                    "pageInfo": {
                        "endCursor": end_cursor,
                        "hasNextPage": has_next_page,
                    }
                }
            }
        }
    })
}

/// Helper: build a page where issue has status widget but status is null.
fn make_null_status_widget_page(iid: i64) -> serde_json::Value {
    serde_json::json!({
        "data": {
            "project": {
                "workItems": {
                    "nodes": [{
                        "iid": iid.to_string(),
                        "widgets": [
                            {"__typename": "WorkItemWidgetStatus", "status": null}
                        ]
                    }],
                    "pageInfo": {
                        "endCursor": null,
                        "hasNextPage": false,
                    }
                }
            }
        }
    })
}

#[tokio::test]
async fn test_fetch_statuses_pagination() {
    let server = MockServer::start().await;

    // Page 1: returns cursor "cursor_page2"
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with({
            ResponseTemplate::new(200).set_body_json(make_work_items_page(
                &[(1, Some("In progress")), (2, Some("To do"))],
                true,
                Some("cursor_page2"),
            ))
        })
        .up_to_n_times(1)
        .expect(1)
        .mount(&server)
        .await;

    // Page 2: no more pages
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(make_work_items_page(
                &[(3, Some("Done"))],
                false,
                None,
            )),
        )
        .expect(1)
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 3);
    assert!(result.statuses.contains_key(&1));
    assert!(result.statuses.contains_key(&2));
    assert!(result.statuses.contains_key(&3));
    assert_eq!(result.all_fetched_iids.len(), 3);
    assert!(result.unsupported_reason.is_none());
}

#[tokio::test]
async fn test_fetch_statuses_no_status_widget() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": {
                "project": {
                    "workItems": {
                        "nodes": [{
                            "iid": "42",
                            "widgets": [
                                {"__typename": "WorkItemWidgetDescription"},
                                {"__typename": "WorkItemWidgetLabels"}
                            ]
                        }],
                        "pageInfo": {"endCursor": null, "hasNextPage": false}
                    }
                }
            }
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(
        result.statuses.is_empty(),
        "No status widget -> no statuses"
    );
    assert!(
        result.all_fetched_iids.contains(&42),
        "IID 42 should still be in all_fetched_iids"
    );
}

#[tokio::test]
async fn test_fetch_statuses_404_graceful() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(404))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(result.statuses.is_empty());
    assert!(result.all_fetched_iids.is_empty());
    assert!(matches!(
        result.unsupported_reason,
        Some(UnsupportedReason::GraphqlEndpointMissing)
    ));
}

#[tokio::test]
async fn test_fetch_statuses_403_graceful() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(403))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(result.statuses.is_empty());
    assert!(result.all_fetched_iids.is_empty());
    assert!(matches!(
        result.unsupported_reason,
        Some(UnsupportedReason::AuthForbidden)
    ));
}

#[tokio::test]
async fn test_fetch_statuses_unsupported_reason_none_on_success() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(make_work_items_page(
                &[(1, Some("To do"))],
                false,
                None,
            )),
        )
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(result.unsupported_reason.is_none());
}

#[tokio::test]
async fn test_typename_matching_ignores_non_status_widgets() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": {
                "project": {
                    "workItems": {
                        "nodes": [{
                            "iid": "10",
                            "widgets": [
                                {"__typename": "WorkItemWidgetDescription"},
                                {"__typename": "WorkItemWidgetLabels"},
                                {"__typename": "WorkItemWidgetAssignees"},
                                {
                                    "__typename": "WorkItemWidgetStatus",
                                    "status": {
                                        "name": "In progress",
                                        "category": "IN_PROGRESS"
                                    }
                                }
                            ]
                        }],
                        "pageInfo": {"endCursor": null, "hasNextPage": false}
                    }
                }
            }
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 1);
    assert_eq!(result.statuses[&10].name, "In progress");
}

#[tokio::test]
async fn test_fetch_statuses_cursor_stall_aborts() {
    let server = MockServer::start().await;

    let stall_response = serde_json::json!({
        "data": {
            "project": {
                "workItems": {
                    "nodes": [{"iid": "1", "widgets": []}],
                    "pageInfo": {"endCursor": "same_cursor", "hasNextPage": true}
                }
            }
        }
    });

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(stall_response))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(
        result.all_fetched_iids.contains(&1),
        "Should contain the one IID fetched before stall"
    );
}

#[tokio::test]
async fn test_fetch_statuses_complexity_error_reduces_page_size() {
    let server = MockServer::start().await;
    let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let call_count_clone = call_count.clone();

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(move |_req: &wiremock::Request| {
            let n =
                call_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
            if n == 0 {
                ResponseTemplate::new(200).set_body_json(serde_json::json!({
                    "errors": [{"message": "Query has complexity of 300, which exceeds max complexity of 250"}]
                }))
            } else {
                ResponseTemplate::new(200).set_body_json(make_work_items_page(
                    &[(1, Some("In progress"))],
                    false,
                    None,
                ))
            }
        })
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 1);
    assert_eq!(result.statuses[&1].name, "In progress");
    assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 2);
}

#[tokio::test]
async fn test_fetch_statuses_timeout_error_reduces_page_size() {
    let server = MockServer::start().await;
    let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let call_count_clone = call_count.clone();

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(move |_req: &wiremock::Request| {
            let n = call_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
            if n == 0 {
                ResponseTemplate::new(200).set_body_json(serde_json::json!({
                    "errors": [{"message": "Query timeout after 30000ms"}]
                }))
            } else {
                ResponseTemplate::new(200).set_body_json(make_work_items_page(
                    &[(5, Some("Done"))],
                    false,
                    None,
                ))
            }
        })
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 1);
    assert!(call_count.load(std::sync::atomic::Ordering::SeqCst) >= 2);
}

#[tokio::test]
async fn test_fetch_statuses_smallest_page_still_fails() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "errors": [{"message": "Query has complexity of 9999"}]
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let err = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap_err();

    assert!(
        matches!(err, LoreError::Other(_)),
        "Expected error after exhausting all page sizes, got: {err:?}"
    );
}

#[tokio::test]
async fn test_fetch_statuses_page_size_resets_after_success() {
    let server = MockServer::start().await;
    let call_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let call_count_clone = call_count.clone();

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(move |_req: &wiremock::Request| {
            let n = call_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
            match n {
                0 => {
                    // Page 1 at size 100: success, has next page
                    ResponseTemplate::new(200).set_body_json(make_work_items_page(
                        &[(1, Some("To do"))],
                        true,
                        Some("cursor_p2"),
                    ))
                }
                1 => {
                    // Page 2 at size 100 (reset): complexity error
                    ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "errors": [{"message": "Query has complexity of 300"}]
                    }))
                }
                2 => {
                    // Page 2 retry at size 50: success
                    ResponseTemplate::new(200).set_body_json(make_work_items_page(
                        &[(2, Some("Done"))],
                        false,
                        None,
                    ))
                }
                _ => ResponseTemplate::new(500),
            }
        })
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 2);
    assert!(result.statuses.contains_key(&1));
    assert!(result.statuses.contains_key(&2));
    assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 3);
}

#[tokio::test]
async fn test_fetch_statuses_partial_errors_tracked() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": {
                "project": {
                    "workItems": {
                        "nodes": [{"iid": "1", "widgets": [
                            {"__typename": "WorkItemWidgetStatus", "status": {"name": "To do"}}
                        ]}],
                        "pageInfo": {"endCursor": null, "hasNextPage": false}
                    }
                }
            },
            "errors": [{"message": "Rate limit warning: approaching limit"}]
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.partial_error_count, 1);
    assert_eq!(
        result.first_partial_error.as_deref(),
        Some("Rate limit warning: approaching limit")
    );
    assert_eq!(result.statuses.len(), 1);
}

#[tokio::test]
async fn test_fetch_statuses_empty_project() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": {
                "project": {
                    "workItems": {
                        "nodes": [],
                        "pageInfo": {"endCursor": null, "hasNextPage": false}
                    }
                }
            }
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(result.statuses.is_empty());
    assert!(result.all_fetched_iids.is_empty());
    assert!(result.unsupported_reason.is_none());
    assert_eq!(result.partial_error_count, 0);
}

#[tokio::test]
async fn test_fetch_statuses_null_status_in_widget() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(make_null_status_widget_page(42)))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert!(
        result.statuses.is_empty(),
        "Null status should not be in map"
    );
    assert!(
        result.all_fetched_iids.contains(&42),
        "IID should still be tracked in all_fetched_iids"
    );
}

#[tokio::test]
async fn test_fetch_statuses_non_numeric_iid_skipped() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "project": {
                        "workItems": {
                            "nodes": [
                                {
                                    "iid": "not_a_number",
                                    "widgets": [{"__typename": "WorkItemWidgetStatus", "status": {"name": "To do"}}]
                                },
                                {
                                    "iid": "7",
                                    "widgets": [{"__typename": "WorkItemWidgetStatus", "status": {"name": "Done"}}]
                                }
                            ],
                            "pageInfo": {"endCursor": null, "hasNextPage": false}
                        }
                    }
                }
            })),
        )
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.statuses.len(), 1);
    assert!(result.statuses.contains_key(&7));
    assert_eq!(result.all_fetched_iids.len(), 1);
}

#[tokio::test]
async fn test_fetch_statuses_null_cursor_with_has_next_aborts() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/api/graphql"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "data": {
                "project": {
                    "workItems": {
                        "nodes": [{"iid": "1", "widgets": []}],
                        "pageInfo": {"endCursor": null, "hasNextPage": true}
                    }
                }
            }
        })))
        .mount(&server)
        .await;

    let client = GraphqlClient::new(&server.uri(), "tok123");
    let result = fetch_issue_statuses(&client, "group/project")
        .await
        .unwrap();

    assert_eq!(result.all_fetched_iids.len(), 1);
}
