//! Cross-source reconciliation on synthetic data: drive the real AWC and
//! Leidos adapters against mock servers, then prove the comparison
//! attributes every value — and every disagreement — to the right source.
//! No live endpoint is touched.
#![allow(clippy::expect_used, clippy::panic, missing_docs)]

use std::sync::Arc;

use aerocontext::{
    AwcClient, AwcConfig, GeoPoint, LeidosClient, LeidosConfig, WeatherBriefingProvider, compare,
    successful_briefings,
};
use aerocontext_core::{Agreement, Area, AreaBriefingRequest, ProductComparison, ProductKind};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

// AWC reports KIAD (matches Leidos), KDCA (AWC only); one TAF for KIAD
// that DISAGREES with Leidos.
const AWC_METAR_JSON: &str = r#"[
  {"icaoId":"KIAD","rawOb":"KIAD 041952Z 28008KT 10SM FEW250 29/13 A3003","obsTime":1717530720},
  {"icaoId":"KDCA","rawOb":"KDCA 041952Z 30010KT 10SM CLR 30/12 A3001","obsTime":1717530720}
]"#;
const AWC_TAF_JSON: &str = r#"[
  {"icaoId":"KIAD","rawTAF":"KIAD 041730Z 0418/0524 27010KT P6SM SCT250","issueTime":"2026-06-04 17:30:00"}
]"#;

// Synthetic Leidos SIMPLE briefing (WSDL-style SimpleWeatherBriefing
// string): KIAD METAR agrees with AWC, KIAD TAF differs, KJYO is
// Leidos-only.
const LEIDOS_SOAP: &str = "<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>AREA BRIEFING FOR 3857N07727W WITHIN 25 NM\n\
METAR KIAD 041952Z 28008KT 10SM FEW250 29/13 A3003=\n\
TAF KIAD 041730Z 0418/0524 30015KT P6SM BKN040=\n\
METAR KJYO 041952Z 27005KT 10SM CLR 28/14 A3003=\n\
END OF BRIEFING</ns2:SimpleWeatherBriefing>\
    </ns2:AreaBriefingResponse>\
  </SOAP-ENV:Body>\
</SOAP-ENV:Envelope>";

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

async fn awc_server() -> MockServer {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/metar"))
        .respond_with(json(AWC_METAR_JSON))
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/taf"))
        .respond_with(json(AWC_TAF_JSON))
        .mount(&server)
        .await;
    // Default fan-out also queries these; empty responses keep the
    // comparison focused on METAR/TAF.
    Mock::given(method("GET"))
        .and(path("/pirep"))
        .respond_with(ResponseTemplate::new(204))
        .mount(&server)
        .await;
    Mock::given(method("GET"))
        .and(path("/airsigmet"))
        .respond_with(json("[]"))
        .mount(&server)
        .await;
    server
}

async fn leidos_server() -> MockServer {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .respond_with(ResponseTemplate::new(200).set_body_raw(LEIDOS_SOAP.to_owned(), "text/xml"))
        .mount(&server)
        .await;
    server
}

fn find<'a>(
    report: &'a [ProductComparison],
    loc: &str,
    kind: &ProductKind,
) -> &'a ProductComparison {
    report
        .iter()
        .find(|c| c.key.location.as_deref() == Some(loc) && &c.key.kind == kind)
        .unwrap_or_else(|| panic!("no comparison for {loc} {kind:?}"))
}

#[tokio::test]
async fn agreeing_and_disagreeing_products_are_attributed_to_their_sources() {
    let awc = awc_server().await;
    let leidos = leidos_server().await;

    let awc_client = AwcClient::new(AwcConfig {
        base_url: awc.uri(),
        user_agent: "aerocontext-cross-source-test/1.0".to_owned(),
    })
    .expect("awc config");
    let leidos_client = LeidosClient::new(LeidosConfig {
        endpoint: leidos.uri(),
        vendor_id: "vendor".to_owned(),
        vendor_password: "secret".to_owned(),
    })
    .expect("leidos config");

    let sources: Vec<Arc<dyn WeatherBriefingProvider>> =
        vec![Arc::new(awc_client), Arc::new(leidos_client)];

    // Empty product list: AWC fans out its defaults, Leidos serves its
    // SIMPLE briefing. PointRadius is the one area shape BOTH accept.
    let request = AreaBriefingRequest {
        area: Area::PointRadius {
            center: GeoPoint {
                lat: 38.95,
                lon: -77.45,
            },
            radius_nm: 25.0,
        },
        products: vec![],
        lookback_hours: None,
        departure_at: "2026-06-05T14:30:00Z".parse().ok(),
    };

    let outcomes = gather_outcomes(&sources, &request).await;
    let briefings = successful_briefings(&outcomes);
    assert_eq!(briefings.len(), 2, "both sources answered");

    let report = compare(&briefings);
    // Surface the attributed report so a human can read it.
    println!("{report}");

    // KIAD METAR: same weather, different surface form -> AGREE, both cited.
    let kiad_metar = find(&report.comparisons, "KIAD", &ProductKind::Metar);
    assert_eq!(kiad_metar.agreement, Agreement::Agree);
    let cited: Vec<&str> = kiad_metar
        .values
        .iter()
        .map(|v| v.source.as_str())
        .collect();
    assert!(cited.contains(&"awc") && cited.contains(&"leidos"));

    // KIAD TAF: the sources DISAGREE, and the user can tell which says which.
    let kiad_taf = find(&report.comparisons, "KIAD", &ProductKind::Taf);
    assert_eq!(kiad_taf.agreement, Agreement::Disagree);
    let awc_taf = kiad_taf
        .values
        .iter()
        .find(|v| v.source == "awc")
        .expect("awc taf");
    let leidos_taf = kiad_taf
        .values
        .iter()
        .find(|v| v.source == "leidos")
        .expect("leidos taf");
    assert!(
        awc_taf.raw_text.contains("SCT250"),
        "awc forecast preserved verbatim"
    );
    assert!(
        leidos_taf.raw_text.contains("BKN040"),
        "leidos forecast preserved verbatim"
    );

    // KDCA only from AWC, KJYO only from Leidos.
    assert_eq!(
        find(&report.comparisons, "KDCA", &ProductKind::Metar).agreement,
        Agreement::Single
    );
    assert_eq!(
        find(&report.comparisons, "KDCA", &ProductKind::Metar).values[0].source,
        "awc"
    );
    assert_eq!(
        find(&report.comparisons, "KJYO", &ProductKind::Metar).agreement,
        Agreement::Single
    );
    assert_eq!(
        find(&report.comparisons, "KJYO", &ProductKind::Metar).values[0].source,
        "leidos"
    );

    // The rendered text is unambiguous about attribution on the conflict.
    let rendered = report.to_string();
    assert!(rendered.contains("KIAD Taf [DISAGREE]"));
    assert!(rendered.contains("awc: KIAD 041730Z"));
    assert!(rendered.contains("leidos: TAF KIAD 041730Z"));

    // Exactly one conflict surfaced by the helper.
    assert_eq!(report.disagreements().count(), 1);
}

async fn gather_outcomes(
    sources: &[Arc<dyn WeatherBriefingProvider>],
    request: &AreaBriefingRequest,
) -> Vec<aerocontext::GatherOutcome> {
    aerocontext::gather(sources, request).await
}
