//! In-process MCP smoke test — exercise the read-only tool handlers
//! against the local vault and verify their JSON shapes.
//!
//! Why in-process: every MCP tool is an `async fn execute(params)`
//! that returns a `serde_json::Value`. We can drive them directly
//! through the same code path the rmcp dispatcher does, without
//! spawning a subprocess or speaking JSON-RPC framing — and still
//! catch the regressions that matter (deserialize-validation,
//! ops-call shape, JSON output structure).
//!
//! What this proves:
//! - The `audit`, `list`, `status`, `doctor`, `health` tools all
//!   round-trip from a typed `*Params` through `envseal::ops::*` to
//!   a `serde_json::Value` with the contract documented in their
//!   `execute` functions.
//! - The unified Signal pipeline survives MCP-side serialization —
//!   `health` returns the new structured `entropy_warnings` shape
//!   (id/category/severity/label/detail/mitigation) and not the
//!   legacy `Vec<String>` from before the v0.3.0 migration.
//!
//! The mutation tools (store, revoke, inject, run, supervised,
//! pipe, import, request_key, inject_file, check, env) require a
//! mutable test vault and a stable passphrase or GUI bypass — out
//! of scope for read-only smoke. Each has its own input-validation
//! test under `tests/rejects_*.rs`.

use envseal_mcp::tools;

#[tokio::test]
async fn audit_tool_returns_count_and_entries_array() {
    let v = tools::audit::execute(tools::audit::AuditParams {
        limit: 5,
        query: None,
        event_type: None,
    })
    .await
    .expect("audit must succeed against any vault state");
    let obj = v.as_object().expect("audit returns a JSON object");
    assert!(obj.contains_key("count"), "missing `count`");
    assert!(obj.contains_key("entries"), "missing `entries`");
    assert!(obj["entries"].is_array(), "`entries` must be a JSON array");
}

#[tokio::test]
async fn list_tool_returns_count_and_secrets_array() {
    let v = tools::list::execute(tools::list::ListParams { filter: None })
        .await
        .expect("list must succeed against any vault state");
    let obj = v.as_object().expect("list returns a JSON object");
    assert!(obj["count"].is_number(), "`count` must be numeric");
    assert!(obj["secrets"].is_array(), "`secrets` must be a JSON array");
}

#[tokio::test]
async fn list_tool_filter_validates_bounded_string() {
    // Empty filter is acceptable — no-op.
    let _ = tools::list::execute(tools::list::ListParams {
        filter: Some(String::new()),
    })
    .await;

    // Oversized filter is rejected by validate_bounded_string before
    // ever touching the vault.
    let big = "x".repeat(64 * 1024);
    let err = tools::list::execute(tools::list::ListParams { filter: Some(big) })
        .await
        .expect_err("oversized filter must be rejected");
    assert!(
        err.contains("filter") || err.contains("too long") || err.contains("exceeds"),
        "expected filter-length error, got: {err}"
    );
}

#[tokio::test]
async fn status_tool_returns_documented_fields() {
    let v = tools::status::execute(tools::status::StatusParams {})
        .await
        .expect("status must succeed");
    for field in [
        "vault_root",
        "secret_count",
        "initialized",
        "envseal_found",
        "security_tier",
        "last_audit",
    ] {
        assert!(
            v.as_object().unwrap().contains_key(field),
            "status output missing field `{field}`"
        );
    }
}

#[tokio::test]
async fn doctor_tool_returns_security_posture_fields() {
    let v = tools::doctor::execute(tools::doctor::DoctorParams {})
        .await
        .expect("doctor must succeed");
    let obj = v.as_object().expect("doctor returns a JSON object");
    // Stable schema documented in the tool's `execute` body. If we
    // rename a field, every consumer breaks — so the test asserts
    // exact key names rather than a loose "is_object()".
    for field in [
        "session",
        "ptrace_scope_warning",
        "self_preload_clean",
        "x11_session",
        "threat_level",
    ] {
        assert!(
            obj.contains_key(field),
            "doctor output missing field `{field}`"
        );
    }
}

#[tokio::test]
async fn health_tool_emits_structured_signals_not_strings() {
    // Regression for the v0.3.0 migration: `entropy_warnings` used
    // to be `Vec<String>` and is now a structured array of Signal
    // objects with `id`, `category`, `severity`, `label`, `detail`,
    // `mitigation`. If anyone reverts to plain strings, this test
    // fires immediately rather than waiting for the desktop GUI to
    // crash on `.id` access.
    let result = tools::health::execute(tools::health::HealthParams {
        max_age_days: Some(365),
    })
    .await;

    // CI runs with `ENVSEAL_NO_GUI=1` and no pre-initialized vault,
    // so `Vault::open_default()` legitimately fails before the
    // health tool can produce any signals. The contract this test
    // enforces is the JSON SHAPE of the structured `entropy_warnings`
    // output — it can only verify that shape if the vault opens. If
    // there's no display + no vault, skip the assertion rather than
    // pretending the test failed.
    let v = match result {
        Ok(v) => v,
        Err(e)
            if e.contains("no display server available")
                || e.contains("user rejected the approval request") =>
        {
            eprintln!(
                "skipping health-tool shape check — headless runner with no \
                 pre-initialized vault: {e}"
            );
            return;
        }
        Err(e) => panic!("health must succeed: {e:?}"),
    };
    let secrets = v["secrets"]
        .as_array()
        .expect("`secrets` must be a JSON array");
    for entry in secrets {
        let warnings = entry["entropy_warnings"]
            .as_array()
            .expect("entropy_warnings must be an array");
        for w in warnings {
            // Each warning must be an OBJECT with the documented
            // fields — never a bare string.
            let obj = w
                .as_object()
                .expect("entropy_warnings entries must be JSON objects, not strings");
            for field in [
                "id",
                "category",
                "severity",
                "label",
                "detail",
                "mitigation",
            ] {
                assert!(
                    obj.contains_key(field),
                    "warning missing `{field}` — Signal-shape regression"
                );
            }
        }
    }
}
