//! AZ-644 integration tests driven by `wiremock`.
//!
//! Coverage:
//! - AC-1: happy-path fetch returns `Ok(Mission)` + health reflects connection_state="ok"
//! - AC-2: schema-invalid response returns `Err(SchemaInvalid)` with a sample
//! - AC-3: transient 503 → 200 sequence retries within budget
//! - AC-4: 5 consecutive 503s → `Err(MaxRetriesExceeded)` and health red

use std::time::Duration;

use shared::health::HealthLevel;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

use mission_client::{FetchError, MissionClient, MissionClientOptions};

fn good_mission_body(mission_id: &str) -> String {
    serde_json::json!({
        "mission_id": mission_id,
        "schema_version": "1.0.0",
        "items": [
            { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "kind": "waypoint",
              "at": { "latitude": 49.1, "longitude": 31.2, "altitude_m": 100.0 } }
        ],
        "geofences": [],
        "return_point": { "latitude": 49.0, "longitude": 31.0, "altitude_m": 0.0 }
    })
    .to_string()
}

fn options_for(mock: &MockServer, attempts: u32) -> MissionClientOptions {
    let mut o = MissionClientOptions::new(mock.uri());
    o.max_attempts = attempts;
    o.backoff_base = Duration::from_millis(10);
    o.backoff_cap = Duration::from_millis(50);
    o.request_timeout = Duration::from_secs(2);
    o.connect_timeout = Duration::from_secs(1);
    o
}

#[tokio::test]
async fn ac1_happy_path_fetch() {
    // Arrange
    let mock = MockServer::start().await;
    let mission_id = "11111111-2222-3333-4444-555555555555";
    Mock::given(method("GET"))
        .and(path(format!("/missions/{mission_id}")))
        .respond_with(ResponseTemplate::new(200).set_body_string(good_mission_body(mission_id)))
        .mount(&mock)
        .await;
    let client = MissionClient::new(options_for(&mock, 5)).expect("client builds");
    let h = client.handle();

    // Act
    let mission = h.pull_mission(mission_id).await.expect("happy fetch");

    // Assert
    assert_eq!(mission.mission_id.to_string(), mission_id);
    assert_eq!(mission.schema_version, "1.0.0");
    let health = h.health();
    assert_eq!(health.level, HealthLevel::Green);
}

#[tokio::test]
async fn ac2_schema_invalid_is_rejected() {
    // Arrange: HTTP 200 but the body is missing the required `mission_id`.
    let mock = MockServer::start().await;
    let bad_body = serde_json::json!({
        "schema_version": "1.0.0",
        "items": [
            { "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", "kind": "waypoint",
              "at": { "latitude": 49.1, "longitude": 31.2, "altitude_m": 100.0 } }
        ],
        "geofences": [],
        "return_point": { "latitude": 49.0, "longitude": 31.0, "altitude_m": 0.0 }
    })
    .to_string();
    Mock::given(method("GET"))
        .and(path("/missions/M1"))
        .respond_with(ResponseTemplate::new(200).set_body_string(bad_body.clone()))
        .mount(&mock)
        .await;
    let client = MissionClient::new(options_for(&mock, 5)).expect("client builds");
    let h = client.handle();

    // Act
    let err = h.pull_mission("M1").await.unwrap_err();

    // Assert
    match err {
        FetchError::SchemaInvalid { messages, sample } => {
            assert!(messages.iter().any(|m| m.contains("mission_id")));
            assert!(!sample.is_empty());
        }
        other => panic!("expected SchemaInvalid, got {other:?}"),
    }
    let health = h.health();
    assert_eq!(health.level, HealthLevel::Red);
}

#[tokio::test]
async fn ac3_transient_failure_retries_within_budget() {
    // Arrange: first two requests return 503, third returns 200.
    let mock = MockServer::start().await;
    let mission_id = "22222222-3333-4444-5555-666666666666";
    Mock::given(method("GET"))
        .and(path(format!("/missions/{mission_id}")))
        .respond_with(ResponseTemplate::new(503))
        .up_to_n_times(2)
        .mount(&mock)
        .await;
    Mock::given(method("GET"))
        .and(path(format!("/missions/{mission_id}")))
        .respond_with(ResponseTemplate::new(200).set_body_string(good_mission_body(mission_id)))
        .mount(&mock)
        .await;
    let client = MissionClient::new(options_for(&mock, 5)).expect("client builds");
    let h = client.handle();

    // Act
    let mission = h.pull_mission(mission_id).await.expect("retry succeeds");

    // Assert
    assert_eq!(mission.mission_id.to_string(), mission_id);
}

#[tokio::test]
async fn ac4_cap_exhaustion_returns_max_retries() {
    // Arrange: every request returns 503; we configure 3 attempts to keep the test fast.
    let mock = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/missions/M-cap"))
        .respond_with(ResponseTemplate::new(503))
        .mount(&mock)
        .await;
    let client = MissionClient::new(options_for(&mock, 3)).expect("client builds");
    let h = client.handle();

    // Act
    let err = h.pull_mission("M-cap").await.unwrap_err();

    // Assert
    match err {
        FetchError::MaxRetriesExceeded { attempts } => assert_eq!(attempts, 3),
        other => panic!("expected MaxRetriesExceeded, got {other:?}"),
    }
    let health = h.health();
    assert_eq!(health.level, HealthLevel::Red);
}

#[tokio::test]
async fn permanent_client_error_does_not_retry() {
    // Arrange: 404 should be permanent (no retry).
    let mock = MockServer::start().await;
    let scoped_mock = Mock::given(method("GET"))
        .and(path("/missions/M-perm"))
        .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
        .expect(1)
        .mount_as_scoped(&mock)
        .await;
    let client = MissionClient::new(options_for(&mock, 5)).expect("client builds");
    let h = client.handle();

    // Act
    let err = h.pull_mission("M-perm").await.unwrap_err();

    // Assert
    assert!(matches!(err, FetchError::Permanent(_)));
    drop(scoped_mock); // sanity-asserts the `.expect(1)` count was honored
}
