//! Wire-contract tests for the Leidos adapter: the exact REST request the
//! adapter constructs is asserted against the FFSP contract, without
//! touching the real service. The adapter is REST-only — no SOAP.
#![allow(clippy::expect_used, clippy::panic, missing_docs)]

use aerocontext_core::{
    Area, AreaBriefingRequest, FlightRules, GeoPoint, ProductKind, ProviderError,
    RouteBriefingRequest, RouteWaypoint, WeatherBriefingProvider,
};
use aerocontext_leidos::{ConfigError, LeidosClient, LeidosConfig, PRODUCTION_ENDPOINT};
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn client(server: &MockServer) -> LeidosClient {
    LeidosClient::new(LeidosConfig::new(server.uri(), "user", "pass"))
        .expect("client config is valid")
}

fn request(area: Area) -> AreaBriefingRequest {
    AreaBriefingRequest::new(area).with_departure_at("2026-06-05T14:30:00Z".parse().ok())
}

// REST responses carry the same schema body, without a SOAP envelope; the
// parser dispatches on local element names.
const SUCCESS_BODY: &str = r#"<AreaBriefingResponse xmlns="https://lmfsweb.afss.com/Website/schemas">
  <returnStatus>true</returnStatus>
  <SimpleWeatherBriefing>METAR KIAD 041952Z 28008KT 10SM FEW250 29/13 A3003</SimpleWeatherBriefing>
</AreaBriefingResponse>"#;

fn xml_response(body: &str) -> ResponseTemplate {
    ResponseTemplate::new(200).set_body_raw(body.to_owned(), "application/xml")
}

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

    Mock::given(method("POST"))
        .and(path("/FP/areaBriefing"))
        // Pre-emptive HTTP Basic with the vendor credentials:
        // base64("user:pass").
        .and(header("authorization", "Basic dXNlcjpwYXNz"))
        .and(header("content-type", "application/x-www-form-urlencoded"))
        .respond_with(xml_response(SUCCESS_BODY))
        .expect(1)
        .mount(&server)
        .await;

    let briefing = client(&server)
        .area_briefing(&request(Area::LocationRadius {
            ident: "KIAD".to_owned(),
            radius_nm: 25.0,
        }))
        .await
        .expect("briefing succeeds");

    assert_eq!(briefing.source, "leidos");
    assert_eq!(briefing.products.len(), 1);
    assert_eq!(briefing.products[0].kind, ProductKind::Metar);
    assert_eq!(briefing.products[0].location.as_deref(), Some("KIAD"));
    assert!(
        briefing
            .narrative
            .as_deref()
            .is_some_and(|text| text.starts_with("METAR KIAD")),
    );
    assert_eq!(briefing.departure_at, "2026-06-05T14:30:00Z".parse().ok());

    let requests = server.received_requests().await.expect("requests recorded");
    let body = String::from_utf8(requests[0].body.clone()).expect("utf-8 body");
    // The form carries the SIMPLE non-briefing markers and the area.
    for needle in [
        "briefingType=SIMPLE",
        "notABriefing=true",
        "includeCodedMessages=true",
        "fixName=KIAD",
        "briefingRadius=25",
        "departureInstant=2026-06-05T14", // ':' is percent-encoded after this
    ] {
        assert!(body.contains(needle), "missing {needle:?} in {body}");
    }
}

#[tokio::test]
async fn point_radius_centers_become_ddmm_locations() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .respond_with(xml_response(SUCCESS_BODY))
        .expect(1)
        .mount(&server)
        .await;

    client(&server)
        .area_briefing(&request(Area::PointRadius {
            center: GeoPoint {
                lat: 38.95,
                lon: -77.45,
            },
            radius_nm: 30.0,
        }))
        .await
        .expect("briefing succeeds");

    let requests = server.received_requests().await.expect("requests recorded");
    let body = String::from_utf8(requests[0].body.clone()).expect("utf-8 body");
    assert!(
        body.contains("fixName=3857N07727W"),
        "coordinate centers use the schema's DDMM(N|S)DDDMM(E|W) form: {body}"
    );
    assert!(body.contains("briefingRadius=30"));
}

#[tokio::test]
async fn route_briefing_posts_a_domestic_route_request() {
    let server = MockServer::start().await;
    let route_body = SUCCESS_BODY.replace("AreaBriefingResponse", "RouteBriefingResponse");
    Mock::given(method("POST"))
        .and(path("/FP/routeBriefing"))
        .and(header("authorization", "Basic dXNlcjpwYXNz"))
        .respond_with(xml_response(&route_body))
        .expect(1)
        .mount(&server)
        .await;

    let req = RouteBriefingRequest::new(
        vec![
            RouteWaypoint::new(GeoPoint {
                lat: 40.69,
                lon: -74.17,
            })
            .with_ident(Some("KEWR".to_owned())),
            RouteWaypoint::new(GeoPoint {
                lat: 40.92,
                lon: -77.99,
            })
            .with_ident(Some("PSB".to_owned())),
            RouteWaypoint::new(GeoPoint {
                lat: 37.75,
                lon: -97.22,
            })
            .with_ident(Some("KAAO".to_owned())),
        ],
        25.0,
    )
    .with_cruise_altitude_ft(Some(40000))
    .with_cruise_tas_kt(Some(450.0))
    .with_flight_rules(FlightRules::Ifr)
    .with_departure_at("2026-06-12T03:20:00Z".parse().ok());

    let briefing = client(&server)
        .route_briefing(&req)
        .await
        .expect("route briefing succeeds");
    assert_eq!(briefing.source, "leidos");
    assert!(briefing.narrative.is_some());

    let requests = server.received_requests().await.expect("requests recorded");
    let body = String::from_utf8(requests[0].body.clone()).expect("utf-8 body");
    for needle in [
        "type=DOMESTIC",
        "flightRules=IFR",
        "departure=KEWR",
        "destination=KAAO",
        "route=PSB",
        "routeCorridorWidth=50", // 25 NM half-width → 50 NM corridor
        "altitudeFL=400",        // FL400
        "speedKnots=450",
    ] {
        assert!(body.contains(needle), "missing {needle:?} in {body}");
    }
}

#[tokio::test]
async fn in_band_failure_is_a_provider_error() {
    let server = MockServer::start().await;
    let failure = r#"<AreaBriefingResponse>
        <returnStatus>false</returnStatus>
        <returnCodedMessage><code>AUTH001</code><message>Vendor not authorized</message></returnCodedMessage>
    </AreaBriefingResponse>"#;
    Mock::given(method("POST"))
        .respond_with(xml_response(failure))
        .expect(1)
        .mount(&server)
        .await;

    let error = client(&server)
        .area_briefing(&request(Area::LocationRadius {
            ident: "KIAD".to_owned(),
            radius_nm: 25.0,
        }))
        .await
        .expect_err("returnStatus=false is an error");
    match error {
        ProviderError::Provider {
            message, status, ..
        } => {
            assert!(
                message.contains("AUTH001"),
                "coded message surfaced: {message}"
            );
            assert_eq!(status, None, "in-band failure, not an HTTP failure");
        }
        other => panic!("expected Provider error, got {other:?}"),
    }
}

#[tokio::test]
async fn fault_on_http_500_is_surfaced() {
    let server = MockServer::start().await;
    let fault =
        r#"<Fault><faultstring>Unmarshalling Error: unexpected element</faultstring></Fault>"#;
    Mock::given(method("POST"))
        .respond_with(ResponseTemplate::new(500).set_body_raw(fault.to_owned(), "application/xml"))
        .expect(1)
        .mount(&server)
        .await;

    let error = client(&server)
        .area_briefing(&request(Area::LocationRadius {
            ident: "KIAD".to_owned(),
            radius_nm: 25.0,
        }))
        .await
        .expect_err("HTTP 500 is an error");
    match error {
        ProviderError::Provider {
            message, status, ..
        } => {
            assert_eq!(status, Some(500));
            assert!(
                message.contains("Unmarshalling Error"),
                "faultstring surfaced: {message}"
            );
        }
        other => panic!("expected Provider error, got {other:?}"),
    }
}

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

    let bbox = request(Area::BoundingBox {
        south_west: GeoPoint {
            lat: 30.0,
            lon: -100.0,
        },
        north_east: GeoPoint {
            lat: 40.0,
            lon: -90.0,
        },
    });
    assert!(matches!(
        client.area_briefing(&bbox).await,
        Err(ProviderError::Unsupported { .. })
    ));

    let oversized = request(Area::LocationRadius {
        ident: "KIAD".to_owned(),
        radius_nm: 150.0,
    });
    assert!(matches!(
        client.area_briefing(&oversized).await,
        Err(ProviderError::Unsupported { .. })
    ));

    // METAR/TAF requests are servable (narrative filter); anything not
    // yet mapped onto briefingPreferences still refuses pre-network.
    let mut with_products = request(Area::LocationRadius {
        ident: "KIAD".to_owned(),
        radius_nm: 25.0,
    });
    with_products.products = vec![ProductKind::Pirep];
    assert!(matches!(
        client.area_briefing(&with_products).await,
        Err(ProviderError::Unsupported { .. })
    ));

    assert!(
        server
            .received_requests()
            .await
            .unwrap_or_default()
            .is_empty()
    );
}

#[test]
fn missing_vendor_credentials_fail_at_construction() {
    let config = |id: &str, password: &str| LeidosConfig::new(PRODUCTION_ENDPOINT, id, password);
    assert!(matches!(
        LeidosClient::new(config("", "pass")),
        Err(ConfigError::MissingCredentials)
    ));
    assert!(matches!(
        LeidosClient::new(config("user", "   ")),
        Err(ConfigError::MissingCredentials)
    ));
    assert!(LeidosClient::new(config("user", "pass")).is_ok());
}

#[tokio::test]
async fn metar_request_is_served_by_filtering_the_narrative_extraction() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .respond_with(xml_response(SUCCESS_BODY))
        .expect(1)
        .mount(&server)
        .await;

    let mut metar_only = request(Area::LocationRadius {
        ident: "KIAD".to_owned(),
        radius_nm: 25.0,
    });
    metar_only.products = vec![ProductKind::Metar];
    let briefing = client(&server)
        .area_briefing(&metar_only)
        .await
        .expect("METAR request is servable");
    assert!(
        briefing
            .products
            .iter()
            .all(|product| product.kind == ProductKind::Metar)
    );
    assert!(briefing.narrative.is_some(), "narrative is never dropped");
}
