//! Integration tests for the upload reporter.
//!
//! Each test spins up a wiremock server inside a tokio runtime and calls
//! `UploadReporter::send` from a blocking helper thread so the sync API
//! does not collide with the async mock server.

use std::path::PathBuf;
use std::sync::Arc;

use jsonschema::Validator;
use mcptest_core::report::upload::{
    PrivacyPolicy, UploadConfig, UploadOutcome, UploadReporter, WireVersion,
};
use mcptest_core::report::Report;
use serde_json::Value;
use tokio::runtime::Runtime;
use url::Url;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};

/// Spin up a wiremock server on a background tokio runtime. The runtime
/// is leaked so the mock keeps serving for the lifetime of the test.
struct TestServer {
    base_url: String,
    runtime: &'static Runtime,
    inner: Arc<MockServer>,
}

impl TestServer {
    fn start() -> Self {
        let rt: &'static Runtime = Box::leak(Box::new(Runtime::new().unwrap()));
        let server = rt.block_on(async { MockServer::start().await });
        let base_url = server.uri();
        TestServer {
            base_url,
            runtime: rt,
            inner: Arc::new(server),
        }
    }

    fn mount(&self, mock: Mock) {
        self.runtime.block_on(async {
            mock.mount(&self.inner).await;
        });
    }

    fn endpoint(&self, route: &str) -> Url {
        Url::parse(&format!("{}{route}", self.base_url)).unwrap()
    }

    fn received_requests(&self) -> Vec<Request> {
        self.runtime
            .block_on(async { self.inner.received_requests().await.unwrap_or_default() })
    }
}

fn run_send(reporter: UploadReporter, report: Report) -> UploadOutcome {
    std::thread::scope(|scope| {
        scope
            .spawn(move || reporter.send(&report))
            .join()
            .expect("send panicked")
    })
}

#[test]
fn returns_uploaded_on_200() {
    let server = TestServer::start();
    server.mount(
        Mock::given(method("POST"))
            .and(path("/collect"))
            .respond_with(ResponseTemplate::new(200)),
    );
    let cfg = UploadConfig::new(server.endpoint("/collect"));
    let reporter = UploadReporter::new(cfg).unwrap();
    let outcome = run_send(reporter, Report::sample());
    match outcome {
        UploadOutcome::Uploaded { status, run_id, .. } => {
            assert_eq!(status, 200);
            assert_eq!(run_id, Report::sample().run_id);
        }
        other => panic!("expected Uploaded, got {other:?}"),
    }
}

#[test]
fn returns_client_error_on_401_with_no_retry() {
    let server = TestServer::start();
    server.mount(
        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(401).set_body_string("unauthorized")),
    );
    let mut cfg = UploadConfig::new(server.endpoint("/collect"));
    cfg.max_retries = 5;
    let reporter = UploadReporter::new(cfg).unwrap();
    let outcome = run_send(reporter, Report::sample());
    match outcome {
        UploadOutcome::ClientError {
            status,
            body_excerpt,
        } => {
            assert_eq!(status, 401);
            assert!(body_excerpt.contains("unauthorized"));
        }
        other => panic!("expected ClientError, got {other:?}"),
    }
    assert_eq!(
        server.received_requests().len(),
        1,
        "4xx must not be retried"
    );
}

#[test]
fn retries_on_503_and_then_returns_client_error() {
    let server = TestServer::start();
    server.mount(
        Mock::given(method("POST"))
            .respond_with(ResponseTemplate::new(503).set_body_string("busy")),
    );
    let mut cfg = UploadConfig::new(server.endpoint("/collect"));
    cfg.max_retries = 2;
    cfg.initial_retry_delay_ms = 1;
    let reporter = UploadReporter::new(cfg).unwrap();
    let outcome = run_send(reporter, Report::sample());
    match outcome {
        UploadOutcome::ClientError { status, .. } => assert_eq!(status, 503),
        other => panic!("expected ClientError after retries, got {other:?}"),
    }
    assert_eq!(
        server.received_requests().len(),
        3,
        "should have made max_retries + 1 attempts"
    );
}

#[test]
fn returns_skipped_when_connection_refused() {
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let port = listener.local_addr().unwrap().port();
    drop(listener);
    let endpoint = Url::parse(&format!("http://127.0.0.1:{port}/collect")).unwrap();
    let mut cfg = UploadConfig::new(endpoint);
    cfg.max_retries = 1;
    cfg.initial_retry_delay_ms = 1;
    let reporter = UploadReporter::new(cfg).unwrap();
    let outcome = run_send(reporter, Report::sample());
    assert!(
        matches!(outcome, UploadOutcome::Skipped { .. }),
        "expected Skipped after transport errors, got {outcome:?}"
    );
}

#[test]
fn request_body_validates_against_wire_v0_schema() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(202)));
    let cfg = UploadConfig::new(server.endpoint("/collect"));
    let reporter = UploadReporter::new(cfg).unwrap();
    let outcome = run_send(reporter, Report::sample());
    assert!(matches!(outcome, UploadOutcome::Uploaded { .. }));

    let requests = server.received_requests();
    assert_eq!(requests.len(), 1);
    let body: Value = serde_json::from_slice(&requests[0].body).unwrap();

    let schema_path = wire_schema_path();
    let schema_str = std::fs::read_to_string(&schema_path)
        .unwrap_or_else(|e| panic!("read {schema_path:?} failed: {e}"));
    let schema_json: Value = serde_json::from_str(&schema_str).unwrap();
    let validator = Validator::new(&schema_json).expect("compile schema");
    let errors: Vec<_> = validator.iter_errors(&body).collect();
    assert!(
        errors.is_empty(),
        "body did not validate against wire v0 schema: {errors:?}\nbody={body}"
    );
}

#[test]
fn authorization_header_present_when_token_set() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let mut cfg = UploadConfig::new(server.endpoint("/collect"));
    cfg.bearer_token = Some("secret-token".into());
    let reporter = UploadReporter::new(cfg).unwrap();
    let _ = run_send(reporter, Report::sample());

    let req = server.received_requests();
    let auth = req[0]
        .headers
        .get("authorization")
        .expect("authorization header present");
    assert_eq!(auth, "Bearer secret-token");
}

#[test]
fn authorization_header_absent_when_token_unset() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let cfg = UploadConfig::new(server.endpoint("/collect"));
    let reporter = UploadReporter::new(cfg).unwrap();
    let _ = run_send(reporter, Report::sample());

    let req = server.received_requests();
    assert!(req[0].headers.get("authorization").is_none());
}

#[test]
fn run_id_and_wire_version_headers_are_set() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let cfg = UploadConfig::new(server.endpoint("/collect"));
    let reporter = UploadReporter::new(cfg).unwrap();
    let report = Report::sample();
    let expected_id = report.run_id.clone();
    let _ = run_send(reporter, report);

    let req = server.received_requests();
    assert_eq!(req[0].headers.get("x-mcptest-wire-version").unwrap(), "v0");
    assert_eq!(
        req[0].headers.get("x-mcptest-run-id").unwrap(),
        &expected_id
    );
}

#[test]
fn user_agent_is_mcptest_versioned() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let cfg = UploadConfig::new(server.endpoint("/collect"));
    let reporter = UploadReporter::new(cfg).unwrap();
    let _ = run_send(reporter, Report::sample());

    let req = server.received_requests();
    let ua = req[0].headers.get("user-agent").unwrap().to_str().unwrap();
    assert!(
        ua.starts_with("mcptest/"),
        "expected user agent to start with mcptest/, got {ua}"
    );
}

#[test]
fn privacy_policy_strips_listed_metadata_field() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let mut cfg = UploadConfig::new(server.endpoint("/collect"));
    cfg.privacy = PrivacyPolicy {
        redact_request_payloads: false,
        redact_response_payloads: false,
        redact_run_metadata_fields: vec!["/run/mcptest_version".into()],
    };
    let reporter = UploadReporter::new(cfg).unwrap();
    let _ = run_send(reporter, Report::sample());

    let req = server.received_requests();
    let body: Value = serde_json::from_slice(&req[0].body).unwrap();
    assert!(
        body["run"].get("mcptest_version").is_none(),
        "expected mcptest_version to be stripped, got {body}"
    );
}

#[test]
fn organization_included_when_set() {
    let server = TestServer::start();
    server.mount(Mock::given(method("POST")).respond_with(ResponseTemplate::new(200)));
    let mut cfg = UploadConfig::new(server.endpoint("/collect"));
    cfg.organization = Some("acme-corp".into());
    let reporter = UploadReporter::new(cfg).unwrap();
    let _ = run_send(reporter, Report::sample());

    let req = server.received_requests();
    let body: Value = serde_json::from_slice(&req[0].body).unwrap();
    assert_eq!(body["organization"], "acme-corp");
    assert_eq!(body["wire_version"], "v0");
}

#[test]
fn wire_version_v0_round_trips_through_config() {
    let cfg = UploadConfig::new(Url::parse("https://example.test/v0").unwrap());
    assert_eq!(cfg.wire_version, WireVersion::V0);
}

/// Locate `schemas/wire/v0.json` relative to the workspace root, regardless
/// of which crate cargo runs the test from.
fn wire_schema_path() -> PathBuf {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    manifest
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .join("schemas")
        .join("wire")
        .join("v0.json")
}
