use std::io::Write;
use std::process::{Command, Stdio};
use std::sync::{Mutex, OnceLock};

/// Session password acquired once during bootstrap.
static SESSION_PASSWORD: OnceLock<String> = OnceLock::new();
/// Mutex to serialize run_rpc calls (tests share repo state).
static RPC_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn bootstrap_session() -> &'static str {
    SESSION_PASSWORD.get_or_init(|| {
        let agent_id = "unknown";

        // Ensure we are on a non-protected branch for tests
        let _ = Command::new("git")
            .args(["checkout", "-b", "feat/test-rpc-suite"])
            .output();

        // Acquire session once
        let session_out = Command::new(env!("CARGO_BIN_EXE_decapod"))
            .args(["session", "acquire"])
            .env("DECAPOD_AGENT_ID", agent_id)
            .env("DECAPOD_VALIDATE_SKIP_GIT_GATES", "1")
            .output()
            .expect("session acquire");
        let session_stdout = String::from_utf8_lossy(&session_out.stdout);
        let session_password = session_stdout
            .lines()
            .find_map(|line| {
                line.strip_prefix("Password: ")
                    .map(|v| v.trim().to_string())
            })
            .unwrap_or_else(|| "test".to_string());

        // Validate once
        let validate_out = Command::new(env!("CARGO_BIN_EXE_decapod"))
            .args(["validate"])
            .env("DECAPOD_AGENT_ID", agent_id)
            .env("DECAPOD_SESSION_PASSWORD", &session_password)
            .env("DECAPOD_VALIDATE_SKIP_GIT_GATES", "1")
            .env("DECAPOD_VALIDATE_SKIP_TOOLING_GATES", "1")
            .output()
            .expect("validate");
        assert!(
            validate_out.status.success(),
            "validate failed:\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&validate_out.stdout),
            String::from_utf8_lossy(&validate_out.stderr)
        );

        // Ingest once
        let ingest_out = Command::new(env!("CARGO_BIN_EXE_decapod"))
            .args(["docs", "ingest"])
            .env("DECAPOD_AGENT_ID", agent_id)
            .env("DECAPOD_SESSION_PASSWORD", &session_password)
            .output()
            .expect("docs ingest");
        assert!(
            ingest_out.status.success(),
            "docs ingest failed: {}",
            String::from_utf8_lossy(&ingest_out.stderr)
        );

        session_password
    })
}

fn run_rpc(request: serde_json::Value) -> serde_json::Value {
    let session_password = bootstrap_session();
    let agent_id = "unknown";

    // Serialize RPC calls — tests share repo state via external processes.
    let lock = RPC_LOCK.get_or_init(|| Mutex::new(()));
    let _guard = match lock.lock() {
        Ok(guard) => guard,
        Err(poisoned) => poisoned.into_inner(),
    };

    // Each test still needs its own todo claim for ordering-gate compliance.
    let todo_add_out = Command::new(env!("CARGO_BIN_EXE_decapod"))
        .args(["todo", "add", "ordering gate test task", "--format", "json"])
        .env("DECAPOD_AGENT_ID", agent_id)
        .env("DECAPOD_SESSION_PASSWORD", session_password)
        .output()
        .expect("todo add");
    assert!(
        todo_add_out.status.success(),
        "todo add failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&todo_add_out.stdout),
        String::from_utf8_lossy(&todo_add_out.stderr)
    );
    let todo_add_json: serde_json::Value =
        serde_json::from_slice(&todo_add_out.stdout).expect("parse todo add");
    let todo_id = todo_add_json["id"].as_str().expect("todo id").to_string();

    let todo_claim_out = Command::new(env!("CARGO_BIN_EXE_decapod"))
        .args([
            "todo", "claim", "--id", &todo_id, "--agent", agent_id, "--format", "json",
        ])
        .env("DECAPOD_AGENT_ID", agent_id)
        .env("DECAPOD_SESSION_PASSWORD", session_password)
        .output()
        .expect("todo claim");
    let todo_claim_json: serde_json::Value =
        serde_json::from_slice(&todo_claim_out.stdout).expect("parse todo claim");
    assert_eq!(todo_claim_json["status"], "ok");

    let run_rpc_once = |req: &serde_json::Value| -> serde_json::Value {
        let mut cmd = Command::new(env!("CARGO_BIN_EXE_decapod"));
        cmd.args(["rpc", "--stdin"])
            .env("DECAPOD_AGENT_ID", agent_id)
            .env("DECAPOD_SESSION_PASSWORD", session_password)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped());

        let mut child = cmd.spawn().expect("Failed to spawn decapod rpc");
        let mut stdin = child.stdin.take().expect("Failed to open stdin");
        stdin
            .write_all(serde_json::to_string(req).unwrap().as_bytes())
            .expect("Failed to write to stdin");
        drop(stdin);

        let output = child.wait_with_output().expect("Failed to read stdout");
        serde_json::from_slice(&output.stdout).expect("Failed to parse JSON response")
    };

    // Mandatory constitutional-awareness bootstrap sequence.
    let init_res = run_rpc_once(&serde_json::json!({ "op": "agent.init", "params": {} }));
    assert!(
        init_res["success"].as_bool().unwrap(),
        "agent.init failed: {}",
        init_res
    );

    let ctx_res = run_rpc_once(&serde_json::json!({ "op": "context.resolve", "params": {} }));
    assert!(
        ctx_res["success"].as_bool().unwrap(),
        "context.resolve failed: {}",
        ctx_res
    );

    let response = run_rpc_once(&request);
    if response["success"] == false {
        eprintln!(
            "RPC Error: {}",
            serde_json::to_string_pretty(&response).unwrap()
        );
    }
    response
}

#[test]
fn test_rpc_context_resolve_determinism() {
    let request = serde_json::json!({
        "op": "context.resolve",
        "params": {
            "op": "workspace.ensure",
            "touched_paths": ["src/core/rpc.rs"],
            "intent_tags": ["security"],
            "limit": 5
        }
    });

    let res1 = run_rpc(request.clone());
    let res2 = run_rpc(request.clone());

    assert_eq!(res1["result"], res2["result"]);
    assert!(res1["success"].as_bool().unwrap());

    let fragments = res1["result"]["fragments"].as_array().unwrap();
    assert!(!fragments.is_empty());
}

#[test]
fn test_rpc_schema_get() {
    let request = serde_json::json!({
        "op": "schema.get",
        "params": {
            "entity": "todo"
        }
    });

    let res = run_rpc(request);
    assert!(res["success"].as_bool().unwrap());
    assert_eq!(res["result"]["schema_version"], "v1");
}

#[test]
fn test_rpc_store_upsert_knowledge() {
    let id = format!("K_TEST_{}", ulid::Ulid::new());
    let request = serde_json::json!({
        "op": "store.upsert",
        "params": {
            "entity": "knowledge",
            "payload": {
                "id": id,
                "title": "RPC Test Knowledge",
                "text": "This is a test entry from RPC",
                "provenance": "cmd:cargo-test"
            }
        }
    });

    let res = run_rpc(request);
    assert!(res["success"].as_bool().unwrap());
    assert_eq!(res["result"]["stored"], true);
    assert_eq!(res["result"]["id"], id);
}

#[test]
fn test_rpc_context_bindings() {
    let request = serde_json::json!({
        "op": "context.bindings",
        "params": {}
    });

    let res = run_rpc(request);
    assert!(res["success"].as_bool().unwrap());
    assert!(res["result"]["ops"].get("workspace.ensure").is_some());
}

#[test]
fn test_rpc_trace_and_redaction() {
    let secret_id = format!("SECRET_{}", ulid::Ulid::new());
    let request = serde_json::json!({
        "op": "schema.get",
        "params": {
            "entity": "todo",
            "my_password": "supersecretpassword",
            "id": secret_id
        }
    });

    let _res = run_rpc(request);

    // Export traces to verify
    let child = Command::new("cargo")
        .args(["run", "--", "trace", "export", "--last", "50"])
        .env("DECAPOD_SESSION_PASSWORD", "test") // Dummy
        .env("DECAPOD_AGENT_ID", "test") // Dummy
        .stdout(Stdio::piped())
        .spawn()
        .expect("Failed to spawn cargo run for trace export");

    let output = child.wait_with_output().expect("Failed to read stdout");
    let trace_line = String::from_utf8_lossy(&output.stdout);

    assert!(trace_line.contains(&secret_id));
    assert!(trace_line.contains("[REDACTED]"));
    assert!(!trace_line.contains("supersecretpassword"));
}
