/// Cross-interface determinism: CLI, MCP, Python, Node.js MUST produce
/// identical output for the same seed + fields + options.
///
/// This is the core product guarantee — any divergence is a ship-blocker.
mod common;
use common::run_ok;

// ---------------------------------------------------------------------------
// MCP helpers
// ---------------------------------------------------------------------------

fn mcp_call(requests: &[&str]) -> Vec<serde_json::Value> {
    let bin = env!("CARGO_BIN_EXE_seedfaker");
    let mut child = std::process::Command::new(bin)
        .arg("mcp")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("spawn mcp");

    use std::io::Write;
    let stdin = child.stdin.as_mut().expect("stdin");
    for req in requests {
        stdin.write_all(req.as_bytes()).expect("write");
        stdin.write_all(b"\n").expect("nl");
    }
    drop(child.stdin.take());
    let output = child.wait_with_output().expect("wait");
    String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter(|l| !l.is_empty())
        .map(|l| serde_json::from_str(l).expect("json"))
        .collect()
}

fn mcp_generate(args_json: &str) -> Vec<serde_json::Value> {
    let init = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}"#;
    let call = format!(
        r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"generate","arguments":{args_json}}}}}"#
    );
    let responses = mcp_call(&[init, &call]);
    assert!(responses.len() >= 2, "expected at least 2 MCP responses");
    let text = responses[1]["result"]["content"][0]["text"].as_str().expect("text");
    serde_json::from_str(text).expect("parse records")
}

fn mcp_records_to_tsv(records: &[serde_json::Value], fields: &[&str]) -> Vec<String> {
    records
        .iter()
        .map(|r| fields.iter().map(|f| r[f].as_str().unwrap_or("")).collect::<Vec<_>>().join("\t"))
        .collect()
}

// ---------------------------------------------------------------------------
// CLI vs MCP: basic fields
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_basic() {
    let cli_out = run_ok(&["name", "email", "--seed", "xface", "-n", "5"]);
    let records = mcp_generate(r#"{"fields":["name","email"],"n":5,"seed":"xface"}"#);
    let mcp_lines = mcp_records_to_tsv(&records, &["name", "email"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP output differs");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: ctx strict
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_ctx_strict() {
    let cli_out = run_ok(&[
        "name", "email", "--seed", "xctx", "--locale", "en", "--ctx", "strict", "-n", "5",
    ]);
    let records = mcp_generate(
        r#"{"fields":["name","email"],"n":5,"seed":"xctx","locale":"en","ctx":"strict"}"#,
    );
    let mcp_lines = mcp_records_to_tsv(&records, &["name", "email"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP ctx=strict differs");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: date fields with year range
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_date_fields() {
    let cli_out = run_ok(&[
        "date",
        "timestamp",
        "--seed",
        "xdt",
        "-n",
        "5",
        "--since",
        "2020",
        "--until",
        "2025",
    ]);
    let records = mcp_generate(
        r#"{"fields":["date","timestamp"],"n":5,"seed":"xdt","since":2020,"until":2025}"#,
    );
    let mcp_lines = mcp_records_to_tsv(&records, &["date", "timestamp"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP date output differs");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: modifiers
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_modifiers() {
    let cli_out = run_ok(&["phone:e164", "date:us", "--seed", "xmod", "-n", "5"]);
    let records = mcp_generate(r#"{"fields":["phone:e164","date:us"],"n":5,"seed":"xmod"}"#);
    let mcp_lines = mcp_records_to_tsv(&records, &["phone_e164", "date_us"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP modifier output differs");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: corruption
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_corrupt() {
    let cli_out = run_ok(&["name", "email", "--seed", "xcor", "--corrupt", "high", "-n", "10"]);
    let records =
        mcp_generate(r#"{"fields":["name","email"],"n":10,"seed":"xcor","corrupt":"high"}"#);
    let mcp_lines = mcp_records_to_tsv(&records, &["name", "email"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP corrupt output differs");
}

// ---------------------------------------------------------------------------
// Python native bindings
// ---------------------------------------------------------------------------

fn project_root() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .to_path_buf()
}

fn python_available() -> bool {
    project_root().join("packages/pip/_seedfaker.so").exists()
}

fn run_python(code: &str) -> String {
    let pip_path = project_root().join("packages/pip");
    let out = std::process::Command::new("python3")
        .arg("-c")
        .arg(code)
        .env("PYTHONPATH", &pip_path)
        .output()
        .expect("python3");
    assert!(out.status.success(), "python failed: {}", String::from_utf8_lossy(&out.stderr));
    String::from_utf8(out.stdout).expect("utf8")
}

#[test]
fn python_native_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&["name", "email", "phone", "--seed", "bind-py", "-n", "5"]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-py")
for r in f.generate_records(["name", "email", "phone"], n=5):
    print(f"{r['name']}\t{r['email']}\t{r['phone']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python native differs from CLI");
}

#[test]
fn python_ctx_strict_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&[
        "name", "email", "--seed", "bind-ctx", "--locale", "en", "--ctx", "strict", "-n", "5",
    ]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-ctx", locale="en")
for r in f.generate_records(["name", "email"], n=5, ctx="strict"):
    print(f"{r['name']}\t{r['email']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python ctx=strict differs from CLI");
}

#[test]
fn python_date_with_year_range_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&[
        "date",
        "birthdate",
        "--seed",
        "bind-yr",
        "--locale",
        "en",
        "--since",
        "2020",
        "--until",
        "2025",
        "-n",
        "5",
    ]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-yr", locale="en", since=2020, until=2025)
for r in f.generate_records(["date", "birthdate"], n=5):
    print(f"{r['date']}\t{r['birthdate']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python since/until differs from CLI");
}

// ---------------------------------------------------------------------------
// Python: inline range in field spec
// ---------------------------------------------------------------------------

#[test]
fn python_inline_range_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&[
        "integer:100..200",
        "date:2020..2022",
        "--seed",
        "bind-range",
        "--locale",
        "en",
        "-n",
        "5",
    ]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-range", locale="en")
for r in f.generate_records(["integer:100..200", "date:2020..2022"], n=5):
    print(f"{r['integer:100..200']}\t{r['date:2020..2022']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python inline range differs from CLI");
}

// ---------------------------------------------------------------------------
// Python: ctx=loose matches CLI
// ---------------------------------------------------------------------------

#[test]
fn python_ctx_loose_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&[
        "name",
        "email",
        "--seed",
        "bind-loose",
        "--locale",
        "en",
        "--ctx",
        "loose",
        "-n",
        "10",
    ]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-loose", locale="en")
for r in f.generate_records(["name", "email"], n=10, ctx="loose"):
    print(f"{r['name']}\t{r['email']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python ctx=loose differs from CLI");
}

// ---------------------------------------------------------------------------
// Python: corruption matches CLI
// ---------------------------------------------------------------------------

#[test]
fn python_corrupt_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: _seedfaker.so not built");
        return;
    }
    let cli_out = run_ok(&[
        "name",
        "email",
        "--seed",
        "bind-cor",
        "--locale",
        "en",
        "--corrupt",
        "high",
        "-n",
        "10",
    ]);
    let py_out = run_python(
        r#"
from seedfaker import SeedFaker
f = SeedFaker(seed="bind-cor", locale="en")
for r in f.generate_records(["name", "email"], n=10, corrupt="high"):
    print(f"{r['name']}\t{r['email']}")
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python corrupt differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js native bindings
// ---------------------------------------------------------------------------

fn node_available() -> bool {
    project_root().join("packages/npm/seedfaker_napi.node").exists()
}

fn run_node(code: &str) -> String {
    let npm_path = project_root().join("packages/npm");
    let full_code = format!("const {{ SeedFaker }} = require('{}');\n{}", npm_path.display(), code);
    let out = std::process::Command::new("node").arg("-e").arg(&full_code).output().expect("node");
    assert!(out.status.success(), "node failed: {}", String::from_utf8_lossy(&out.stderr));
    String::from_utf8(out.stdout).expect("utf8")
}

#[test]
fn node_native_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&["name", "email", "phone", "--seed", "bind-js", "-n", "5"]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-js' });
const rows = f.generate_records(['name', 'email', 'phone'], { n: 5 });
rows.forEach(r => console.log(r.name + '\t' + r.email + '\t' + r.phone));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js native differs from CLI");
}

#[test]
fn node_date_with_year_range_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&[
        "date",
        "birthdate",
        "--seed",
        "bind-yr",
        "--locale",
        "en",
        "--since",
        "2020",
        "--until",
        "2025",
        "-n",
        "5",
    ]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-yr', locale: 'en', since: 2020, until: 2025 });
const rows = f.generate_records(['date', 'birthdate'], { n: 5 });
rows.forEach(r => console.log(r.date + '\t' + r.birthdate));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js year range differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js: inline range in field spec
// ---------------------------------------------------------------------------

#[test]
fn node_inline_range_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&[
        "integer:100..200",
        "date:2020..2022",
        "--seed",
        "bind-range",
        "--locale",
        "en",
        "-n",
        "5",
    ]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-range', locale: 'en' });
const rows = f.generate_records(['integer:100..200', 'date:2020..2022'], { n: 5 });
rows.forEach(r => console.log(r['integer:100..200'] + '\t' + r['date:2020..2022']));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js inline range differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js: ctx=loose matches CLI
// ---------------------------------------------------------------------------

#[test]
fn node_ctx_loose_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&[
        "name",
        "email",
        "--seed",
        "bind-loose",
        "--locale",
        "en",
        "--ctx",
        "loose",
        "-n",
        "10",
    ]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-loose', locale: 'en' });
const rows = f.generate_records(['name', 'email'], { n: 10, ctx: 'loose' });
rows.forEach(r => console.log(r.name + '\t' + r.email));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js ctx=loose differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js: corruption matches CLI
// ---------------------------------------------------------------------------

#[test]
fn node_corrupt_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&[
        "name",
        "email",
        "--seed",
        "bind-cor",
        "--locale",
        "en",
        "--corrupt",
        "high",
        "-n",
        "10",
    ]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-cor', locale: 'en' });
const rows = f.generate_records(['name', 'email'], { n: 10, corrupt: 'high' });
rows.forEach(r => console.log(r.name + '\t' + r.email));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js corrupt differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js: capitalize modifier matches Rust behavior
// ---------------------------------------------------------------------------

#[test]
fn node_capitalize_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&["name:capitalize", "--seed", "bind-cap", "--locale", "en", "-n", "10"]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-cap', locale: 'en' });
const vals = f.generate('name', { n: 10, modifier: 'capitalize' });
(Array.isArray(vals) ? vals : [vals]).forEach(v => console.log(v));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js capitalize differs from CLI");
}

// ---------------------------------------------------------------------------
// Python: capitalize modifier via generate()
// ---------------------------------------------------------------------------

#[test]
fn python_capitalize_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: seedfaker python not built");
        return;
    }
    let cli_out = run_ok(&["name:capitalize", "--seed", "bind-cap", "--locale", "en", "-n", "10"]);
    let py_out = run_python(
        r#"
f = SeedFaker(seed='bind-cap', locale='en')
for v in f.generate('name', n=10, modifier='capitalize'):
    print(v)
"#,
    );
    assert_eq!(cli_out.trim(), py_out.trim(), "Python capitalize differs from CLI");
}

// ---------------------------------------------------------------------------
// Node.js: transform in generate_records()
// ---------------------------------------------------------------------------

#[test]
fn node_transform_in_records_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out =
        run_ok(&["name:upper", "email:lower", "--seed", "bind-xform", "--locale", "en", "-n", "5"]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-xform', locale: 'en' });
const rows = f.generate_records(['name:upper', 'email:lower'], { n: 5 });
rows.forEach(r => console.log(r['name:upper'] + '\t' + r['email:lower']));
"#,
    );
    assert_eq!(
        cli_out.trim(),
        js_out.trim(),
        "Node.js transform in generate_records differs from CLI"
    );
}

// ---------------------------------------------------------------------------
// Python: transform in generate_records()
// ---------------------------------------------------------------------------

#[test]
fn python_transform_in_records_matches_cli() {
    if !python_available() {
        eprintln!("SKIP: seedfaker python not built");
        return;
    }
    let cli_out =
        run_ok(&["name:upper", "email:lower", "--seed", "bind-xform", "--locale", "en", "-n", "5"]);
    let py_out = run_python(
        r#"
f = SeedFaker(seed='bind-xform', locale='en')
for r in f.generate_records(['name:upper', 'email:lower'], n=5):
    print(r['name:upper'] + '\t' + r['email:lower'])
"#,
    );
    assert_eq!(
        cli_out.trim(),
        py_out.trim(),
        "Python transform in generate_records differs from CLI"
    );
}

// ---------------------------------------------------------------------------
// Node.js: generate() with n > 1 (batch single field)
// ---------------------------------------------------------------------------

#[test]
fn node_generate_batch_matches_cli() {
    if !node_available() {
        eprintln!("SKIP: seedfaker_napi.node not built");
        return;
    }
    let cli_out = run_ok(&["email", "--seed", "bind-batch", "--locale", "en", "-n", "5"]);
    let js_out = run_node(
        r#"
const f = new SeedFaker({ seed: 'bind-batch', locale: 'en' });
const vals = f.generate('email', { n: 5 });
(Array.isArray(vals) ? vals : [vals]).forEach(v => console.log(v));
"#,
    );
    assert_eq!(cli_out.trim(), js_out.trim(), "Node.js generate batch differs from CLI");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: ctx=loose
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_ctx_loose() {
    let cli_out = run_ok(&[
        "name", "email", "--seed", "xloose", "--locale", "en", "--ctx", "loose", "-n", "10",
    ]);
    let records = mcp_generate(
        r#"{"fields":["name","email"],"n":10,"seed":"xloose","locale":"en","ctx":"loose"}"#,
    );
    let mcp_lines = mcp_records_to_tsv(&records, &["name", "email"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP ctx=loose differs");
}

// ---------------------------------------------------------------------------
// CLI vs MCP: inline range
// ---------------------------------------------------------------------------

#[test]
fn cli_vs_mcp_inline_range() {
    let cli_out = run_ok(&["integer:10..50", "date:2020..2022", "--seed", "xrange", "-n", "5"]);
    let records =
        mcp_generate(r#"{"fields":["integer:10..50","date:2020..2022"],"n":5,"seed":"xrange"}"#);
    // MCP display_name strips range: "integer", "date"
    let mcp_lines = mcp_records_to_tsv(&records, &["integer", "date"]);
    let cli_lines: Vec<&str> = cli_out.lines().collect();
    assert_eq!(cli_lines, mcp_lines, "CLI and MCP inline range differs");
}
