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

use aerocontext_core::{
    Area, AreaBriefingRequest, GeoPoint, ProductKind, ProviderError, 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 {
        endpoint: server.uri(),
        vendor_id: "user".to_owned(),
        vendor_password: "pass".to_owned(),
    })
    .expect("client config is valid")
}

fn request(area: Area) -> AreaBriefingRequest {
    AreaBriefingRequest {
        area,
        products: vec![],
        lookback_hours: None,
        departure_at: "2026-06-05T14:30:00Z".parse().ok(),
    }
}

const SUCCESS_BODY: &str = r#"<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Body>
    <ns2:AreaBriefingResponse xmlns:ns2="https://lmfsweb.afss.com/Website/schemas">
      <ns2:returnStatus>true</ns2:returnStatus>
      <ns2:SimpleWeatherBriefing>METAR KIAD 041952Z 28008KT 10SM FEW250 29/13 A3003</ns2:SimpleWeatherBriefing>
    </ns2:AreaBriefingResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>"#;

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

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

    Mock::given(method("POST"))
        .and(path("/"))
        // Pre-emptive HTTP Basic with the vendor credentials:
        // base64("user:pass").
        .and(header("authorization", "Basic dXNlcjpwYXNz"))
        // SOAP 1.1 with the WSDL's empty soapAction, sent quoted.
        .and(header("soapaction", "\"\""))
        .and(header("content-type", "text/xml; charset=utf-8"))
        .respond_with(soap_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");
    // The SIMPLE narrative is kept, and its METAR is also surfaced as a
    // discrete product so the briefing is comparable with other sources.
    assert_eq!(briefing.products.len(), 1);
    assert_eq!(
        briefing.products[0].kind,
        aerocontext_core::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")),
    );

    let requests = server.received_requests().await.expect("requests recorded");
    let body = String::from_utf8(requests[0].body.clone()).expect("utf-8 body");

    // The AreaBriefingRequest complex type is an xs:sequence: assert each
    // qualified child is present AND in schema order.
    let expected_in_order = [
        "<lfs:AreaBriefingRequest>",
        "<lfs:includeCodedMessages>true</lfs:includeCodedMessages>",
        "<lfs:notABriefing>true</lfs:notABriefing>",
        "<lfs:versionRequested>20160225</lfs:versionRequested>",
        "<lfs:briefingType>SIMPLE</lfs:briefingType>",
        "<lfs:fixName><lfs:locationIdentifier>KIAD</lfs:locationIdentifier></lfs:fixName>",
        "<lfs:departureInstant>2026-06-05T14:30:00Z</lfs:departureInstant>",
        "<lfs:briefingRadius>25</lfs:briefingRadius>",
        "</lfs:AreaBriefingRequest>",
    ];
    let mut cursor = 0;
    for needle in expected_in_order {
        let found = body[cursor..]
            .find(needle)
            .unwrap_or_else(|| panic!("missing or out of order: {needle}\nbody: {body}"));
        cursor += found + needle.len();
    }
    assert!(body.contains(r#"xmlns:lfs="https://lmfsweb.afss.com/Website/schemas""#));
}

#[tokio::test]
async fn point_radius_centers_become_ddmm_locations() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .respond_with(soap_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("<lfs:locationIdentifier>3857N07727W</lfs:locationIdentifier>"),
        "coordinate centers use the schema's DDMM(N|S)DDDMM(E|W) form: {body}"
    );
    assert!(body.contains("<lfs:briefingRadius>30</lfs:briefingRadius>"));
}

#[tokio::test]
async fn in_band_failure_is_a_provider_error() {
    let server = MockServer::start().await;
    let failure = r#"<Envelope><Body><AreaBriefingResponse>
        <returnStatus>false</returnStatus>
        <returnCodedMessage><code>AUTH001</code><message>Vendor not authorized</message></returnCodedMessage>
    </AreaBriefingResponse></Body></Envelope>"#;
    Mock::given(method("POST"))
        .respond_with(soap_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 soap_fault_on_http_500_is_surfaced() {
    let server = MockServer::start().await;
    let fault = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
      <soap:Body><soap:Fault>
        <faultcode>soap:Client</faultcode>
        <faultstring>Unmarshalling Error: unexpected element</faultstring>
      </soap:Fault></soap:Body>
    </soap:Envelope>"#;
    Mock::given(method("POST"))
        .respond_with(ResponseTemplate::new(500).set_body_raw(fault.to_owned(), "text/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 { .. })
    ));

    let mut with_products = request(Area::LocationRadius {
        ident: "KIAD".to_owned(),
        radius_nm: 25.0,
    });
    with_products.products = vec![ProductKind::Metar];
    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 {
        endpoint: PRODUCTION_ENDPOINT.to_owned(),
        vendor_id: id.to_owned(),
        vendor_password: password.to_owned(),
    };
    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());
}
