//! Wire-contract tests for the AWC adapter: every request the adapter
//! constructs is asserted against the documented Data API contract,
//! without touching the real endpoint.
#![allow(clippy::expect_used, clippy::panic, missing_docs)]

use aerocontext_awc::{AwcClient, AwcConfig};
use aerocontext_core::{
    Area, AreaBriefingRequest, GeoPoint, NavDataCycle, NavDataSnapshot, NavPoint, NavPointKind,
    ProductKind, ProviderError, WeatherBriefingProvider,
};
use chrono::NaiveDate;
use wiremock::matchers::{header, method, path, query_param, query_param_is_missing};
use wiremock::{Mock, MockServer, ResponseTemplate};

const USER_AGENT: &str = "aerocontext-awc-tests/1.0";

fn client(server: &MockServer) -> AwcClient {
    AwcClient::new(AwcConfig {
        base_url: server.uri(),
        user_agent: USER_AGENT.to_owned(),
        ..AwcConfig::default()
    })
    .expect("client config is valid")
}

/// Center at 60N 10E with a 60 NM radius: 60 NM is exactly one degree of
/// latitude, and cos(60°) = 0.5 doubles the longitude half-width, so the
/// derived bbox is exactly `59,8,61,12` — deterministic for matching.
fn point_radius_request(
    products: Vec<ProductKind>,
    lookback_hours: Option<u32>,
) -> AreaBriefingRequest {
    AreaBriefingRequest::new(Area::PointRadius {
        center: GeoPoint {
            lat: 60.0,
            lon: 10.0,
        },
        radius_nm: 60.0,
    })
    .with_products(products)
    .with_lookback_hours(lookback_hours)
}

const BBOX: &str = "59,8,61,12";

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

fn test_nav_data() -> NavDataSnapshot {
    NavDataSnapshot::new(
        NavDataCycle::faa_nasr(NaiveDate::from_ymd_opt(2026, 5, 14).expect("test date is valid"))
            .expect("cycle builds"),
        vec![NavPoint::new(
            "TEST",
            NavPointKind::Airport,
            GeoPoint {
                lat: 60.0,
                lon: 10.0,
            },
        )],
    )
}

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

    Mock::given(method("GET"))
        .and(path("/metar"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .and(header("user-agent", USER_AGENT))
        .respond_with(json_response(
            r#"[{"icaoId":"ENGM","rawOb":"ENGM 041350Z 18005KT CAVOK 22/10 Q1018","obsTime":1717509000}]"#,
        ))
        .expect(1)
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path("/taf"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .respond_with(json_response(
            r#"[{"icaoId":"ENGM","rawTAF":"ENGM 041100Z 0412/0512 20008KT 9999 SCT030"}]"#,
        ))
        .expect(1)
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path("/pirep"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .respond_with(json_response(
            r#"[{"rawOb":"OSL UA /OV ENGM/TM 1340/FL050/TP C172/TB LGT","obsTime":1717508400}]"#,
        ))
        .expect(1)
        .mount(&server)
        .await;

    // `airsigmet` has no bbox parameter — the adapter must not send one,
    // and must clip the whole-region result client-side instead.
    Mock::given(method("GET"))
        .and(path("/airsigmet"))
        .and(query_param("format", "json"))
        .and(query_param_is_missing("bbox"))
        .respond_with(json_response(
            r#"[
              {"rawAirSigmet":"INSIDE SIGMET","coords":[{"lat":59.5,"lon":9.0},{"lat":60.5,"lon":11.0}]},
              {"rawAirSigmet":"OUTSIDE SIGMET","coords":[{"lat":30.0,"lon":-100.0},{"lat":32.0,"lon":-98.0}]},
              {"rawAirSigmet":"LAT-ONLY SIGMET","coords":[{"lat":60.0,"lon":100.0},{"lat":60.5,"lon":102.0}]},
              {"rawAirSigmet":"LON-ONLY SIGMET","coords":[{"lat":30.0,"lon":10.0},{"lat":32.0,"lon":11.0}]}
            ]"#,
        ))
        .expect(1)
        .mount(&server)
        .await;

    let briefing = client(&server)
        .area_briefing(&point_radius_request(vec![], None))
        .await
        .expect("briefing succeeds");

    assert_eq!(briefing.source, "awc");
    assert!(briefing.narrative.is_none());
    let texts: Vec<&str> = briefing
        .products
        .iter()
        .map(|p| p.raw_text.as_str())
        .collect();
    assert_eq!(
        texts.len(),
        4,
        "one metar, one taf, one pirep, one clipped sigmet"
    );
    assert!(texts.iter().any(|t| t.starts_with("ENGM 041350Z")));
    assert!(texts.iter().any(|t| t.starts_with("ENGM 041100Z")));
    assert!(texts.iter().any(|t| t.starts_with("OSL UA")));
    assert!(texts.contains(&"INSIDE SIGMET"));
    assert!(
        !texts.contains(&"OUTSIDE SIGMET"),
        "outside-area sigmet must be clipped"
    );
    // Single-axis overlap is not overlap — these two pin the longitude
    // and latitude checks independently.
    assert!(
        !texts.contains(&"LAT-ONLY SIGMET"),
        "latitude-only overlap must be clipped"
    );
    assert!(
        !texts.contains(&"LON-ONLY SIGMET"),
        "longitude-only overlap must be clipped"
    );
}

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

    Mock::given(method("GET"))
        .and(path("/metar"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .and(query_param("hours", "2"))
        .respond_with(json_response("[]"))
        .expect(1)
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path("/pirep"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .and(query_param("age", "2"))
        .and(query_param_is_missing("hours"))
        .respond_with(json_response("[]"))
        .expect(1)
        .mount(&server)
        .await;

    client(&server)
        .area_briefing(&point_radius_request(
            vec![ProductKind::Metar, ProductKind::Pirep],
            Some(2),
        ))
        .await
        .expect("briefing succeeds");
}

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

    // AWC signals "valid query, no data" with 204 No Content and an empty
    // body (not an empty JSON array). Feeding that body to serde would
    // fail, so the adapter must branch on the status first.
    Mock::given(method("GET"))
        .and(path("/metar"))
        .respond_with(ResponseTemplate::new(204))
        .expect(1)
        .mount(&server)
        .await;

    let briefing = client(&server)
        .area_briefing(&point_radius_request(vec![ProductKind::Metar], None))
        .await
        .expect("204 is success");
    assert!(briefing.products.is_empty());
}

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

    Mock::given(method("GET"))
        .and(path("/metar"))
        .respond_with(ResponseTemplate::new(400).set_body_string("Invalid bbox"))
        .expect(1)
        .mount(&server)
        .await;

    let error = client(&server)
        .area_briefing(&point_radius_request(vec![ProductKind::Metar], None))
        .await
        .expect_err("400 is an error");
    match error {
        ProviderError::Provider {
            status, message, ..
        } => {
            assert_eq!(status, Some(400));
            assert!(message.contains("Invalid bbox"));
        }
        other => panic!("expected Provider error, got {other:?}"),
    }
}

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

    Mock::given(method("GET"))
        .and(path("/taf"))
        .respond_with(json_response("[]"))
        .expect(1)
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/metar"))
        .respond_with(json_response("[]"))
        .expect(0)
        .mount(&server)
        .await;

    client(&server)
        .area_briefing(&point_radius_request(vec![ProductKind::Taf], None))
        .await
        .expect("briefing succeeds");
}

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

    // The bbox parameter is latitude-first (lat0,lon0,lat1,lon1) per the
    // Data API spec — the reverse of GeoJSON coordinate order.
    Mock::given(method("GET"))
        .and(path("/metar"))
        .and(query_param("bbox", "30,-100,40,-90"))
        .respond_with(json_response("[]"))
        .expect(1)
        .mount(&server)
        .await;

    let request = AreaBriefingRequest::new(Area::BoundingBox {
        south_west: GeoPoint {
            lat: 30.0,
            lon: -100.0,
        },
        north_east: GeoPoint {
            lat: 40.0,
            lon: -90.0,
        },
    })
    .with_products(vec![ProductKind::Metar]);
    client(&server)
        .area_briefing(&request)
        .await
        .expect("briefing succeeds");
}

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

    Mock::given(method("GET"))
        .and(path("/metar"))
        .and(query_param("bbox", BBOX))
        .and(query_param("format", "json"))
        .respond_with(json_response("[]"))
        .expect(1)
        .mount(&server)
        .await;

    let request = AreaBriefingRequest::new(Area::LocationRadius {
        ident: "test".to_owned(),
        radius_nm: 60.0,
    })
    .with_products(vec![ProductKind::Metar]);
    client(&server)
        .with_nav_data(test_nav_data())
        .area_briefing(&request)
        .await
        .expect("briefing succeeds");
}

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

    let location_area = AreaBriefingRequest::new(Area::LocationRadius {
        ident: "KSFO".to_owned(),
        radius_nm: 50.0,
    });
    assert!(matches!(
        client.area_briefing(&location_area).await,
        Err(ProviderError::Unsupported { .. })
    ));

    let notam = point_radius_request(vec![ProductKind::Notam], None);
    assert!(matches!(
        client.area_briefing(&notam).await,
        Err(ProviderError::Unsupported { .. })
    ));

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