//! Scripted MCP client integration test.
//!
//! Drives `plsql-mcp` exactly as a real MCP client would — one
//! newline-delimited JSON-RPC request at a time through
//! [`handle_request_line`] — over the full registered foundation
//! tool surface, and golden-asserts the structural invariants of
//! every response.
//!
//! "Golden snapshot" here follows the workspace idiom:
//! deterministic structural assertions on the canonical JSON rather
//! than an `insta` dependency. Every
//! invariant that must never silently regress is pinned:
//! JSON-RPC framing, id echo, the MCP-007 trust block on every
//! success, and the exact registered foundation tool set.

use plsql_mcp::{
    PlsqlMcpServer, ToolRegistry, default_tool_registry, register_analyze_project_tool,
    register_foundation_tools, register_graph_tools,
};
use serde_json::{Value, json};

const AGENT_UX_SURFACE_GOLDEN: &str = include_str!("golden/agent_ux_surface.json");

/// The foundation tool surface a scripted client should see.
const EXPECTED_TOOLS: &[&str] = &[
    "analyze_project",
    "find_callers",
    "find_callees",
    "get_dependencies",
    "dynamic_sql_evidence",
    "completeness_report",
    "doc_lookup",
];

fn foundation_registry() -> ToolRegistry {
    let mut r = ToolRegistry::new();
    register_analyze_project_tool(&mut r);
    register_graph_tools(&mut r);
    register_foundation_tools(&mut r);
    r
}

fn server(registry: ToolRegistry) -> PlsqlMcpServer {
    PlsqlMcpServer::new(registry).expect("server runtime builds")
}

fn call(server: &mut PlsqlMcpServer, line: &str) -> serde_json::Value {
    let resp = server
        .handle_request_line(line)
        .expect("server produced a response");
    serde_json::to_value(&resp).expect("response serializes")
}

fn call_json(server: &mut PlsqlMcpServer, request: serde_json::Value) -> serde_json::Value {
    call(
        server,
        &serde_json::to_string(&request).expect("request serializes"),
    )
}

#[test]
fn scripted_session_golden_invariants() {
    let mut server = server(foundation_registry());

    // --- frame 1: initialize ---
    let init = call(
        &mut server,
        r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#,
    );
    assert_eq!(init["jsonrpc"], "2.0");
    assert_eq!(init["id"], 1, "id is echoed");
    assert!(init["error"].is_null(), "initialize must not error");
    assert!(init["result"]["serverInfo"]["name"].is_string());
    // MCP-007: trust block on every success.
    assert_eq!(
        init["result"]["meta"]["trust_block"]["schema_id"],
        "plsql.mcp.trust_block"
    );

    // --- frame 2: tools/list — exact foundation surface ---
    let list = call(
        &mut server,
        r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#,
    );
    assert_eq!(list["id"], 2);
    let tools = list["result"]["tools"]
        .as_array()
        .expect("tools array present");
    let mut names: Vec<String> = tools
        .iter()
        .map(|t| t["name"].as_str().unwrap().to_string())
        .collect();
    names.sort();
    let mut expected: Vec<String> = EXPECTED_TOOLS.iter().map(|s| (*s).to_string()).collect();
    expected.sort();
    assert_eq!(names, expected, "registered foundation surface is pinned");
    assert_eq!(
        list["result"]["meta"]["trust_block"]["live_database_used"],
        false
    );

    // --- frame 3: ping — empty payload + trust block ---
    let ping = call(
        &mut server,
        r#"{"jsonrpc":"2.0","id":3,"method":"ping","params":{}}"#,
    );
    assert_eq!(ping["id"], 3);
    assert!(ping["result"]["meta"]["trust_block"].is_object());

    // --- frame 4: unknown method — typed JSON-RPC error ---
    let bad = call(
        &mut server,
        r#"{"jsonrpc":"2.0","id":4,"method":"no/such","params":{}}"#,
    );
    assert_eq!(bad["id"], 4);
    assert_eq!(bad["error"]["code"], -32601, "method not found");
    assert!(
        bad["result"].is_null(),
        "an error response carries no result (and so no trust block)"
    );

    // --- frame 5: malformed line — parse error, never a panic ---
    let parse_err = call(&mut server, "{not valid json");
    assert_eq!(parse_err["error"]["code"], -32700);
}

#[test]
fn responses_are_deterministic_across_runs() {
    let mut server = server(foundation_registry());
    let line = r#"{"jsonrpc":"2.0","id":9,"method":"tools/list","params":{}}"#;
    let a = call(&mut server, line);
    let b = call(&mut server, line);
    assert_eq!(a, b, "identical request -> byte-identical response");
}

#[test]
fn tools_call_parse_file_executes_a_real_tool_over_the_wire() {
    // oracle-l65d: a `tools/call` for a self-contained static tool
    // must reach the real `run_parse_file` and return a structured
    // parse result over the wire — not a placeholder. This is the
    // headline "fully wired" assertion, exercised through the exact
    // `handle_request_line` path a real MCP client uses.
    let mut server = server(default_tool_registry());
    let resp = call(
        &mut server,
        concat!(
            r#"{"jsonrpc":"2.0","id":50,"method":"tools/call","params":"#,
            r#"{"name":"parse_file","arguments":"#,
            r#"{"source":"CREATE OR REPLACE PACKAGE p AS PROCEDURE q; END;\n/\n"}}}"#,
        ),
    );
    assert_eq!(resp["id"], 50);
    assert!(resp["error"].is_null(), "parse_file must not error");
    let result = &resp["result"];
    assert_eq!(result["isError"], false);
    // The real ParseFileResponse rode back in structuredContent.
    assert!(
        result["structuredContent"]["declaration_count"]
            .as_u64()
            .unwrap()
            >= 1,
        "real parser counted declarations: {result}"
    );
    // MCP-007 trust block still attached to the wired result.
    assert_eq!(
        result["meta"]["trust_block"]["schema_id"],
        "plsql.mcp.trust_block"
    );
}

#[test]
fn tools_call_live_db_tool_returns_an_honest_gated_result_over_the_wire() {
    // oracle-l65d: a live-DB tool (`query`) is wired — it dispatches
    // — but with no connection it returns a typed, honest error
    // result over the wire. Never a panic, never a fake success.
    let mut server = server(default_tool_registry());
    let resp = call(
        &mut server,
        concat!(
            r#"{"jsonrpc":"2.0","id":51,"method":"tools/call","params":"#,
            r#"{"name":"query","arguments":{"sql":"SELECT 1 FROM dual"}}}"#,
        ),
    );
    assert_eq!(resp["id"], 51);
    assert!(resp["error"].is_null(), "transport call itself succeeds");
    assert_eq!(
        resp["result"]["isError"], true,
        "no connection => honest error result"
    );
    let text = resp["result"]["content"][0]["text"]
        .as_str()
        .unwrap()
        .to_lowercase();
    assert!(
        text.contains("connection") || text.contains("live-db"),
        "gated message names the missing runtime: {text}"
    );
}

#[test]
#[ignore = "requires local plsql-intelligence-xe at localhost:1521/FREEPDB1"]
fn live_xe_connect_then_query_hits_oracle_over_the_wire() {
    let mut server = server(default_tool_registry());

    let connected = call_json(
        &mut server,
        serde_json::json!({
            "jsonrpc": "2.0",
            "id": 70,
            "method": "tools/call",
            "params": {
                "name": "connect",
                "arguments": {
                    "name": "xe-system",
                    "connect_string": "//localhost:1521/FREEPDB1",
                    "username": "SYSTEM",
                    "password": "DemoPlsqlIntel#2026",
                    "description": "local XE smoke test",
                    "permanently_read_only": true
                }
            }
        }),
    );
    assert!(
        connected["error"].is_null(),
        "connect must open a real oraclemcp-db session: {connected}"
    );
    assert_eq!(connected["result"]["structuredContent"]["connected"], true);
    assert_eq!(
        connected["result"]["structuredContent"]["active"],
        "xe-system"
    );

    let queried = call_json(
        &mut server,
        serde_json::json!({
            "jsonrpc": "2.0",
            "id": 71,
            "method": "tools/call",
            "params": {
                "name": "query",
                "arguments": {
                    "sql": "SELECT 1 AS val FROM dual"
                }
            }
        }),
    );
    assert!(
        queried["error"].is_null(),
        "query must run through the active live session: {queried}"
    );
    assert_eq!(
        queried["result"]["structuredContent"]["columns"][0]["name"],
        "VAL"
    );
    assert_eq!(
        queried["result"]["structuredContent"]["rows"][0]["cells"][0]["value"],
        "1"
    );
}

#[test]
fn tools_call_bad_arguments_returns_invalid_params_over_the_wire() {
    // oracle-l65d: un-deserializable arguments are a proper -32602
    // JSON-RPC error, never a panic.
    let mut server = server(default_tool_registry());
    let resp = call(
        &mut server,
        concat!(
            r#"{"jsonrpc":"2.0","id":52,"method":"tools/call","params":"#,
            r#"{"name":"parse_file","arguments":{"source":99}}}"#,
        ),
    );
    assert_eq!(resp["id"], 52);
    assert_eq!(resp["error"]["code"], -32602);
}

#[test]
fn agent_ux_surface_wire_golden_matches_snapshot() {
    let mut server = server(default_tool_registry());

    let tools_list = call_json(
        &mut server,
        json!({
            "jsonrpc": "2.0",
            "id": 80,
            "method": "tools/list",
            "params": {}
        }),
    );
    let capabilities = call_json(
        &mut server,
        json!({
            "jsonrpc": "2.0",
            "id": 81,
            "method": "tools/call",
            "params": {
                "name": "oracle_capabilities",
                "arguments": {}
            }
        }),
    );
    let forced_error = call_json(
        &mut server,
        json!({
            "jsonrpc": "2.0",
            "id": 82,
            "method": "tools/call",
            "params": {
                "name": "parse_fil",
                "arguments": {}
            }
        }),
    );
    let resources_list = call_json(
        &mut server,
        json!({
            "jsonrpc": "2.0",
            "id": 83,
            "method": "resources/list",
            "params": {}
        }),
    );

    let actual = json!({
        "schema_id": "plsql.mcp.agent_ux_surface_wire_golden",
        "schema_version": "1.0.0",
        "frames": [
            summarize_tools_list(&tools_list),
            summarize_capabilities(&capabilities),
            summarize_forced_error(&forced_error),
            summarize_resources_list(&resources_list),
        ],
    });
    assert_json_snapshot(&actual, AGENT_UX_SURFACE_GOLDEN);
}

#[test]
fn every_registered_tool_is_discoverable_and_described() {
    let mut server = server(foundation_registry());
    let list = call(
        &mut server,
        r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#,
    );
    for t in list["result"]["tools"].as_array().unwrap() {
        assert!(t["name"].as_str().is_some_and(|n| !n.is_empty()));
        // Each tool advertises a non-empty human description so an
        // agent can choose without trial calls.
        let desc = t
            .get("description")
            .and_then(|d| d.as_str())
            .or_else(|| t.get("summary").and_then(|d| d.as_str()))
            .unwrap_or("");
        assert!(!desc.is_empty(), "tool {} has no description", t["name"]);
    }
}

fn assert_json_snapshot(actual: &Value, expected: &str) {
    let actual = format!(
        "{}\n",
        serde_json::to_string_pretty(actual).expect("snapshot serializes")
    );
    assert_eq!(actual, expected);
}

fn summarize_tools_list(response: &Value) -> Value {
    let result = field(response, "result");
    let tools = array_field(result, "tools");
    let selected_tools = [
        "oracle_capabilities",
        "parse_file",
        "query",
        "set_safety_profile",
        "create_or_replace",
        "deploy_ddl",
    ]
    .into_iter()
    .map(|name| summarize_tool(tool_by_name(tools, name)))
    .collect::<Vec<_>>();

    json!({
        "request": "tools/list",
        "response": ok_response_summary(response),
        "tool_count": tools.len(),
        "selected_tools": selected_tools,
    })
}

fn summarize_tool(tool: &Value) -> Value {
    let input_schema = field(tool, "inputSchema");
    let annotations = field(tool, "annotations");
    json!({
        "name": str_field(tool, "name"),
        "properties": property_keys(input_schema),
        "required": optional_string_array_field(input_schema, "required"),
        "annotations": {
            "readOnlyHint": bool_field(annotations, "readOnlyHint"),
            "destructiveHint": bool_field(annotations, "destructiveHint"),
            "available": bool_field(annotations, "available"),
            "activeSafetyProfile": str_field(annotations, "activeSafetyProfile"),
            "requiredOperatingLevel": optional_value_field(annotations, "requiredOperatingLevel"),
            "unavailableReason": optional_value_field(annotations, "unavailableReason"),
        },
    })
}

fn summarize_capabilities(response: &Value) -> Value {
    let result = field(response, "result");
    let structured = field(result, "structuredContent");
    let features = field(structured, "features");
    json!({
        "request": "tools/call oracle_capabilities",
        "response": ok_response_summary(response),
        "isError": bool_field(result, "isError"),
        "server_name": str_field(structured, "server_name"),
        "protocol_version": str_field(structured, "protocol_version"),
        "tool_count": u64_field(structured, "tool_count"),
        "features": {
            "engine": bool_field(features, "engine"),
            "http_transport": bool_field(features, "http_transport"),
            "live_db": bool_field(features, "live_db"),
        },
        "resources": string_array_field(structured, "resources"),
        "next_actions": string_array_field(structured, "next_actions"),
    })
}

fn summarize_forced_error(response: &Value) -> Value {
    let error = field(response, "error");
    let data = field(error, "data");
    json!({
        "request": "tools/call parse_fil",
        "response": error_response_summary(response),
        "message": str_field(error, "message"),
        "error_class": str_field(data, "error_class"),
        "fuzzy_matches": string_array_field(data, "fuzzy_matches"),
        "next_steps": string_array_field(data, "next_steps"),
    })
}

fn summarize_resources_list(response: &Value) -> Value {
    let result = field(response, "result");
    let resources = array_field(result, "resources")
        .iter()
        .map(|resource| {
            json!({
                "uri": str_field(resource, "uri"),
                "name": str_field(resource, "name"),
                "mimeType": str_field(resource, "mimeType"),
            })
        })
        .collect::<Vec<_>>();
    json!({
        "request": "resources/list",
        "response": ok_response_summary(response),
        "resources": resources,
    })
}

fn ok_response_summary(response: &Value) -> Value {
    json!({
        "jsonrpc": str_field(response, "jsonrpc"),
        "id": field(response, "id").clone(),
        "has_trust_block": has_trust_block(response),
    })
}

fn error_response_summary(response: &Value) -> Value {
    let error = field(response, "error");
    json!({
        "jsonrpc": str_field(response, "jsonrpc"),
        "id": field(response, "id").clone(),
        "code": i64_field(error, "code"),
        "has_trust_block": has_trust_block(response),
    })
}

fn has_trust_block(response: &Value) -> bool {
    response
        .get("result")
        .and_then(|result| result.get("meta"))
        .and_then(|meta| meta.get("trust_block"))
        .and_then(|trust| trust.get("schema_id"))
        .and_then(Value::as_str)
        .is_some_and(|schema_id| schema_id == "plsql.mcp.trust_block")
}

fn tool_by_name<'a>(tools: &'a [Value], name: &str) -> &'a Value {
    tools
        .iter()
        .find(|tool| tool.get("name").and_then(Value::as_str) == Some(name))
        .expect("selected tool must be advertised")
}

fn field<'a>(value: &'a Value, key: &str) -> &'a Value {
    value.get(key).expect("expected JSON field")
}

fn array_field<'a>(value: &'a Value, key: &str) -> &'a [Value] {
    field(value, key)
        .as_array()
        .map(Vec::as_slice)
        .expect("expected JSON array field")
}

fn str_field(value: &Value, key: &str) -> String {
    field(value, key)
        .as_str()
        .expect("expected JSON string field")
        .to_owned()
}

fn bool_field(value: &Value, key: &str) -> bool {
    field(value, key)
        .as_bool()
        .expect("expected JSON bool field")
}

fn u64_field(value: &Value, key: &str) -> u64 {
    field(value, key).as_u64().expect("expected JSON u64 field")
}

fn i64_field(value: &Value, key: &str) -> i64 {
    field(value, key).as_i64().expect("expected JSON i64 field")
}

fn optional_value_field(value: &Value, key: &str) -> Value {
    value.get(key).cloned().unwrap_or(Value::Null)
}

fn string_array_field(value: &Value, key: &str) -> Vec<String> {
    array_field(value, key)
        .iter()
        .map(|entry| {
            entry
                .as_str()
                .expect("expected JSON string array entry")
                .to_owned()
        })
        .collect()
}

fn optional_string_array_field(value: &Value, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(Value::as_array)
        .map(|entries| {
            entries
                .iter()
                .map(|entry| {
                    entry
                        .as_str()
                        .expect("expected JSON string array entry")
                        .to_owned()
                })
                .collect()
        })
        .unwrap_or_default()
}

fn property_keys(input_schema: &Value) -> Vec<String> {
    let mut keys = input_schema
        .get("properties")
        .and_then(Value::as_object)
        .map(|properties| properties.keys().cloned().collect::<Vec<_>>())
        .unwrap_or_default();
    keys.sort();
    keys
}
