//! Integration test for the runner-side `inject_error:` injection path.
//!
//! Builds a tool plan that carries a pre-resolved [`NamedError`],
//! hands it to the executor against an empty server pool, and asserts
//! that the test runs offline: no server lookup, no transport spawn,
//! no network call. The assertion pipeline sees the synthesized
//! `{"result": {"jsonrpc":"2.0","error":{...}}}` envelope and
//! evaluates dotted-path matchers against it as if the server had
//! returned it. The pool stays empty, so a regression that drops the
//! short-circuit and falls through to the live dispatch path would
//! surface as an "unknown server" failure rather than a passing test.

use std::collections::HashMap;
use std::sync::Arc;

use mcptest_core::connector::ServerPool;
use mcptest_core::executor::{McpExecutor, PlannedTest, Scoring, ToolPlan};
use mcptest_core::fixtures::{ErrorScope, NamedError};
use mcptest_core::report::TestStatus;
use mcptest_core::runner::{TestExecutor, TestSpec};
use mcptest_core::suite::{Action, Assertion, Matcher};
use serde_json::json;

#[tokio::test]
async fn injected_error_runs_offline_against_an_empty_pool() {
    let pool = Arc::new(ServerPool::new());
    let plan = PlannedTest::Tool(ToolPlan {
        // `nonexistent` would fail an "unknown server" guard if the
        // injection short-circuit was skipped, so the assertion below
        // doubles as a regression check on the dispatch path.
        server: "nonexistent".into(),
        action: Action::ToolCall {
            tool: "create_issue".into(),
            args: json!({"title": "hi"}),
        },
        expectations: vec![Assertion {
            target: "result.error.code".into(),
            matcher: Matcher::Exact(json!(-32000)),
            weight: 1.0,
            name: None,
            transform: None,
        }],
        budgets: Default::default(),
        transport: Default::default(),
        scoring: Scoring::default(),
        transform: None,
        inject_error: Some(Box::new(NamedError {
            name: "rate_limited".into(),
            code: -32000,
            message: "rate limit exceeded".into(),
            scope: ErrorScope::Tool("create_issue".into()),
        })),
        metamorphic: None,
        fuzz: None,
        negative_path: None,
        responder: None,
    });

    let mut plans: HashMap<String, Arc<PlannedTest>> = HashMap::new();
    plans.insert("rate_limited_path".into(), Arc::new(plan));

    let executor = McpExecutor::new(pool.clone(), plans);
    let outcome = executor
        .execute_one(&TestSpec::new("rate_limited_path"))
        .await;

    assert_eq!(
        outcome.status,
        TestStatus::Pass,
        "injected fixture should run offline and pass; got {outcome:?}"
    );
}

#[tokio::test]
async fn injected_error_envelope_carries_the_named_message_for_text_assertions() {
    let pool = Arc::new(ServerPool::new());
    let plan = PlannedTest::Tool(ToolPlan {
        server: "nonexistent".into(),
        action: Action::ToolCall {
            tool: "get_user".into(),
            args: json!({"id": "1"}),
        },
        expectations: vec![Assertion {
            target: "result.error.message".into(),
            matcher: Matcher::IContains("token expired".into()),
            weight: 1.0,
            name: None,
            transform: None,
        }],
        budgets: Default::default(),
        transport: Default::default(),
        scoring: Scoring::default(),
        transform: None,
        inject_error: Some(Box::new(NamedError {
            name: "auth_expired".into(),
            code: -32001,
            message: "oauth token expired".into(),
            scope: ErrorScope::Any,
        })),
        metamorphic: None,
        fuzz: None,
        negative_path: None,
        responder: None,
    });

    let mut plans: HashMap<String, Arc<PlannedTest>> = HashMap::new();
    plans.insert("auth_expired_path".into(), Arc::new(plan));

    let executor = McpExecutor::new(pool, plans);
    let outcome = executor
        .execute_one(&TestSpec::new("auth_expired_path"))
        .await;

    assert_eq!(
        outcome.status,
        TestStatus::Pass,
        "contains match against the injected message should pass; got {outcome:?}"
    );
}

#[tokio::test]
async fn injected_error_envelope_failing_assertion_returns_a_failure_outcome() {
    // A failing matcher against the synthesized envelope must surface as a
    // normal test failure, not a runner panic. This locks the
    // injection path to the same outcome semantics as a real call.
    let pool = Arc::new(ServerPool::new());
    let plan = PlannedTest::Tool(ToolPlan {
        server: "nonexistent".into(),
        action: Action::ToolCall {
            tool: "create_issue".into(),
            args: json!({}),
        },
        expectations: vec![Assertion {
            target: "result.error.code".into(),
            matcher: Matcher::Exact(json!(0)),
            weight: 1.0,
            name: None,
            transform: None,
        }],
        budgets: Default::default(),
        transport: Default::default(),
        scoring: Scoring::default(),
        transform: None,
        inject_error: Some(Box::new(NamedError {
            name: "rate_limited".into(),
            code: -32000,
            message: "rate limit exceeded".into(),
            scope: ErrorScope::Tool("create_issue".into()),
        })),
        metamorphic: None,
        fuzz: None,
        negative_path: None,
        responder: None,
    });

    let mut plans: HashMap<String, Arc<PlannedTest>> = HashMap::new();
    plans.insert("wrong_assertion".into(), Arc::new(plan));

    let executor = McpExecutor::new(pool, plans);
    let outcome = executor
        .execute_one(&TestSpec::new("wrong_assertion"))
        .await;

    assert_eq!(
        outcome.status,
        TestStatus::Fail,
        "exact(0) against -32000 should fail; got {outcome:?}"
    );
}
