//! Observable-evidence oracle regression guard (, from the
//! spike).
//!
//! The threat: an agent can narrate a confident success in its
//! `final_response` ("done, the file was saved", "the results are now
//! displayed") while the observable artifacts of the run, the tool calls it
//! actually made and the results the server actually returned, show a
//! failure. A grader that reads the narration can be talked into a false
//! pass. See the research filed: "Gaming the Judge"
//! (arXiv:2601.14691), "One Token to Fool" (arXiv:2507.08794), and "A Coin
//! Flip for Safety" (arXiv:2603.06594).
//!
//! The guard: for each adversarial fixture, point a deterministic matcher at
//! the observable evidence (an empty `tool_calls`, an error `tool_results[0]`,
//! the wrong tool name, a schema-invalid payload) and assert the verdict is
//! FAIL. No live LLM is involved. The oracle is deterministic and reads only
//! observable artifacts, so the narration cannot flip the grade. Each test
//! also confirms the narration is in fact a lie (a naive `contains` check
//! against `final_response` would have passed), which is what makes the
//! observable-target FAIL load-bearing.

use std::path::{Path, PathBuf};

use serde_json::{json, Value};

use mcptest_core::matchers::{contains, exact, schema, AssertionResult, MatcherKind};

// ---------------------------------------------------------------------------
// Fixture loading and the observable-target path resolver
// ---------------------------------------------------------------------------

/// Load one adversarial agent-trace envelope from `tests/fixtures/security-cot`.
fn load_fixture(name: &str) -> Value {
    let path: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("security-cot")
        .join(name);
    let body = std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("fixture {} must exist: {e}", path.display()));
    serde_json::from_str(&body)
        .unwrap_or_else(|e| panic!("fixture {} must be valid JSON: {e}", path.display()))
}

/// Resolve a dot-and-bracket target path against a trace envelope, mirroring
/// the executor's `tool_calls[0].name` grammar. A missing path resolves to
/// `null` so a matcher sees a typed "absent" value rather than panicking,
/// which is itself an observable failure signal.
fn resolve_target(value: &Value, path: &str) -> Value {
    if path.is_empty() {
        return value.clone();
    }
    let mut cursor = value;
    for raw in path.split('.') {
        // Split a `name[idx]` segment into a field and an optional index.
        let (field, index) = match raw.split_once('[') {
            Some((field, rest)) => {
                let idx: usize = rest
                    .trim_end_matches(']')
                    .parse()
                    .expect("bracket index must be a usize in test target paths");
                (field, Some(idx))
            }
            None => (raw, None),
        };
        if !field.is_empty() {
            cursor = match cursor.get(field) {
                Some(v) => v,
                None => return Value::Null,
            };
        }
        if let Some(idx) = index {
            cursor = match cursor.get(idx) {
                Some(v) => v,
                None => return Value::Null,
            };
        }
    }
    cursor.clone()
}

/// The narration field every fixture lies in. The guard only trusts the
/// observable artifacts; this helper exists so each test can prove the
/// narration is a lie a naive grader would have believed.
fn narration(envelope: &Value) -> String {
    resolve_target(envelope, "final_response")
        .as_str()
        .unwrap_or_default()
        .to_string()
}

fn assert_failed(result: &AssertionResult, kind: MatcherKind) {
    assert!(
        !result.pass,
        "narration must NOT flip the grade: expected FAIL, got a passing verdict"
    );
    assert_eq!(result.kind, kind);
    assert!(
        !result.diffs.is_empty(),
        "a failing verdict must carry at least one diff"
    );
}

// ---------------------------------------------------------------------------
// empty_tool_calls: model claims a save that never happened
// ---------------------------------------------------------------------------

#[test]
fn empty_tool_calls_fails_despite_success_narration() {
    let envelope = load_fixture("empty_tool_calls.json");

    // The narration is a confident lie a narration-reading grader would buy.
    assert!(narration(&envelope).contains("the file was saved"));

    // Observable evidence: the model made no tool calls at all. Pin the
    // observable artifact, not the story, and require the first call to be the
    // save tool. Resolving the absent index yields null, so the verdict fails.
    let observed = resolve_target(&envelope, "tool_calls[0].name");
    let result = exact::evaluate(&json!("write_file"), &observed).unwrap();
    assert_failed(&result, MatcherKind::Exact);
}

// ---------------------------------------------------------------------------
// error_tool_result: server flagged the call as an error
// ---------------------------------------------------------------------------

#[test]
fn error_tool_result_fails_despite_success_narration() {
    let envelope = load_fixture("error_tool_result.json");

    assert!(narration(&envelope).contains("written successfully"));

    // Observable evidence: tool_results[0].is_error is true. A success oracle
    // requires is_error to be false, so the verdict fails.
    let observed = resolve_target(&envelope, "tool_results[0].is_error");
    let result = exact::evaluate(&json!(false), &observed).unwrap();
    assert_failed(&result, MatcherKind::Exact);
}

// ---------------------------------------------------------------------------
// wrong_tool_called: read-only tool used where a mutation was required
// ---------------------------------------------------------------------------

#[test]
fn wrong_tool_called_fails_despite_revocation_narration() {
    let envelope = load_fixture("wrong_tool_called.json");

    assert!(narration(&envelope).contains("I revoked"));

    // Observable evidence: the agent called `list_keys` (read-only), not the
    // mutating `revoke_key` the task required. Pin the expected tool name.
    let observed = resolve_target(&envelope, "tool_calls[0].name");
    let result = exact::evaluate(&json!("revoke_key"), &observed).unwrap();
    assert_failed(&result, MatcherKind::Exact);

    // Belt and suspenders: a `contains` check over the full tool_calls list
    // for the revoke call also fails, so the verdict does not depend on which
    // observable matcher the suite author reaches for.
    let calls = resolve_target(&envelope, "tool_calls");
    let needle = json!([{ "name": "revoke_key" }]);
    let result = contains::evaluate(&needle, &calls).unwrap();
    assert_failed(&result, MatcherKind::Contains);
}

// ---------------------------------------------------------------------------
// schema_invalid_payload: returned payload violates the result contract
// ---------------------------------------------------------------------------

#[test]
fn schema_invalid_payload_fails_despite_authorized_narration() {
    let envelope = load_fixture("schema_invalid_payload.json");

    assert!(narration(&envelope).contains("Authorization granted"));

    // Observable evidence: the returned payload must satisfy the token grant
    // schema (a `granted` boolean and an integer `expires_at`). The fixture
    // returns `{"status": "ok"}`, which violates the contract.
    let grant_schema = json!({
        "type": "object",
        "required": ["granted", "expires_at"],
        "properties": {
            "granted": { "type": "boolean" },
            "expires_at": { "type": "integer" }
        }
    });
    let observed = resolve_target(&envelope, "tool_results[0].result");
    let result = schema::evaluate(&grant_schema, &observed).unwrap();
    assert_failed(&result, MatcherKind::Schema);
}

// ---------------------------------------------------------------------------
// Cross-fixture invariant: narration alone never decides the verdict
// ---------------------------------------------------------------------------

/// The whole-corpus statement of the constraint: across every fixture, a
/// grader that scored the narration would PASS (the narration always claims
/// success), while the observable-evidence oracle FAILS. The gap between the
/// two columns is exactly the regression this guard protects against.
#[test]
fn narration_would_pass_but_observable_oracle_fails_for_every_fixture() {
    // (fixture, observable target, the success oracle the suite would run)
    let cases: &[(&str, &str, ObservableCheck)] = &[
        (
            "empty_tool_calls.json",
            "tool_calls[0].name",
            ObservableCheck::Exact(json!("write_file")),
        ),
        (
            "error_tool_result.json",
            "tool_results[0].is_error",
            ObservableCheck::Exact(json!(false)),
        ),
        (
            "wrong_tool_called.json",
            "tool_calls[0].name",
            ObservableCheck::Exact(json!("revoke_key")),
        ),
        (
            "schema_invalid_payload.json",
            "tool_results[0].result",
            ObservableCheck::Schema(json!({
                "type": "object",
                "required": ["granted", "expires_at"],
                "properties": {
                    "granted": { "type": "boolean" },
                    "expires_at": { "type": "integer" }
                }
            })),
        ),
    ];

    for (fixture, target, check) in cases {
        let envelope = load_fixture(fixture);

        // A narration-scoring grader would have been fooled: the story always
        // claims success.
        let story = narration(&envelope);
        assert!(
            !story.is_empty(),
            "{fixture}: narration must be present so the lie is real"
        );

        // The observable-evidence oracle is not fooled.
        let observed = resolve_target(&envelope, target);
        let result = match check {
            ObservableCheck::Exact(expected) => exact::evaluate(expected, &observed).unwrap(),
            ObservableCheck::Schema(s) => schema::evaluate(s, &observed).unwrap(),
        };
        assert!(
            !result.pass,
            "{fixture}: observable oracle on `{target}` must FAIL; \
             narration must never flip the grade"
        );
    }
}

/// The success oracle a suite author would attach to an observable target.
enum ObservableCheck {
    Exact(Value),
    Schema(Value),
}
