//! Migration doctor: connect, initialize, run the pair-corpus check.
//!
//! v1 ships the static deprecated-capability checks. Categories that need
//! the stateless transport, the schema validator, or the auth conformance
//! pack land in follow-up tickets and surface as `skipped` findings here
//! with rationale pointing at the relevant ticket.
//!
//! Wired in through `mcptest doctor --target-version 2026-07-28`. The
//! probe is a one-shot connect plus initialize; the rest of the doctor
//! pipeline (DNS, TCP, TLS, auth) runs first and is intentionally kept
//! separate so this code stays a pure transport-touching shim around the
//! corpus matcher in `mcptest_core::migration`.

use std::time::Duration;

use anyhow::{Context, Result};
use mcptest_core::migration::{FindingStatus, MigrationCorpus, MigrationFinding};
use mcptest_core::protocol::{Client, ProtocolVersion};
use mcptest_core::transport::{StreamableHttp, StreamableHttpConfig};
use serde::Serialize;

use super::DoctorAuth;

/// Output format for the migration report.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum MigrationCheckFormat {
    /// Human-readable table grouped by verdict.
    Pretty,
    /// One JSON object with `target_version` and a `findings` array.
    Json,
}

/// Run the migration doctor against `base_url`. The function connects,
/// runs `initialize`, hands the response to the embedded migration
/// pair-corpus, and prints the report in `format`. Returns the desired
/// process exit code (`0` when every detected case passes or is skipped,
/// `1` when any case fails).
pub fn run_migration_check(
    base_url: &str,
    auth: DoctorAuth,
    target_version: &str,
    format: MigrationCheckFormat,
) -> Result<i32> {
    let corpus = MigrationCorpus::builtin().context("builtin migration corpus is invalid")?;
    let findings = probe_and_check(base_url, auth, &corpus, target_version)?;
    match format {
        MigrationCheckFormat::Pretty => print_pretty(target_version, &findings),
        MigrationCheckFormat::Json => print_json(target_version, &findings)?,
    }
    let exit_code = if findings
        .iter()
        .any(|f| matches!(f.status, FindingStatus::Fail))
    {
        1
    } else {
        0
    };
    Ok(exit_code)
}

fn probe_and_check(
    base_url: &str,
    auth: DoctorAuth,
    corpus: &MigrationCorpus,
    target_version: &str,
) -> Result<Vec<MigrationFinding>> {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .context("could not start probe runtime")?;
    runtime.block_on(async move {
        let cfg = StreamableHttpConfig {
            url: base_url.to_owned(),
            bearer_token: auth.bearer_token,
            request_timeout: Duration::from_secs(15),
            connect_timeout: Duration::from_secs(10),
            ..StreamableHttpConfig::default()
        };
        let (transport, handle) = StreamableHttp::connect(cfg)
            .map_err(|e| anyhow::anyhow!("streamable HTTP setup failed: {e}"))?;
        let client = Client::builder(transport)
            .with_request_timeout(Duration::from_secs(15))
            .build();
        let outcome = client
            .initialize(ProtocolVersion::current())
            .await
            .context("MCP initialize handshake failed")?;
        client.close().await.ok();
        handle.shutdown().await;
        // The migration corpus check inspects the initialize response
        // shape. We reconstruct the raw shape from the typed outcome the
        // protocol client surfaces so the matcher stays transport-free.
        let init = serde_json::json!({
            "protocolVersion": outcome.negotiated_version.as_wire(),
            "serverInfo": {
                "name": outcome.server.name,
                "version": outcome.server.version,
            },
            "capabilities": outcome.server.capabilities,
        });
        let mut findings = corpus.check_initialize_response(&init);
        // The corpus rows are static; the negotiated protocol version is
        // information the user wants in the report, so we attach it as a
        // detail on the first stateless-transport skip row.
        if let Some(row) = findings.iter_mut().find(|f| f.id == "session-id-reliance") {
            if matches!(row.status, FindingStatus::Skipped) {
                row.detail = format!(
                    "{} (server negotiated `{}`; target `{}`)",
                    row.detail,
                    outcome.negotiated_version.as_wire(),
                    target_version
                );
            }
        }
        Ok::<_, anyhow::Error>(findings)
    })
}

#[derive(Debug, Serialize)]
struct MigrationReport<'a> {
    target_version: &'a str,
    findings: &'a [MigrationFinding],
}

fn print_pretty(target_version: &str, findings: &[MigrationFinding]) {
    let pass = findings
        .iter()
        .filter(|f| matches!(f.status, FindingStatus::Pass))
        .count();
    let fail = findings
        .iter()
        .filter(|f| matches!(f.status, FindingStatus::Fail))
        .count();
    let skip = findings
        .iter()
        .filter(|f| matches!(f.status, FindingStatus::Skipped))
        .count();
    println!();
    println!(
        "migration doctor (target {target_version}): {fail} fail, {pass} pass, {skip} skipped"
    );
    println!();
    for f in findings {
        let badge = match f.status {
            FindingStatus::Pass => "[PASS]",
            FindingStatus::Fail => "[FAIL]",
            FindingStatus::Skipped => "[SKIP]",
        };
        println!(
            "  {badge} [{kind}] {id} ({severity})",
            kind = f.kind,
            id = f.id,
            severity = f.severity
        );
        println!("        {}", f.summary);
        if !f.detail.is_empty() {
            println!("        detail: {}", f.detail);
        }
        if matches!(f.status, FindingStatus::Fail) {
            println!("        replacement: {}", f.replacement);
            println!("        doc:         {}", f.doc_link);
        }
    }
}

fn print_json(target_version: &str, findings: &[MigrationFinding]) -> Result<()> {
    let report = MigrationReport {
        target_version,
        findings,
    };
    println!("{}", serde_json::to_string(&report)?);
    Ok(())
}
