//! roplat_build 代码生成器功能测试

use roplat_build::codegen::*;
use roplat_build::manifest::*;
use std::fs;
use tempfile::TempDir;

// ═══════════════════════════════════════════════════════════════════
// MsgCodeGen 测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn msg_codegen_writes_c_header() {
    let dir = TempDir::new().unwrap();
    let cpp_dir = dir.path().join("cpp");
    let py_dir = dir.path().join("py");

    let cg = MsgCodeGen::new(&cpp_dir, &py_dir);
    cg.write_c_header("SensorData", "#ifndef SENSOR\n#endif")
        .unwrap();

    let path = cpp_dir.join("sensor_data.h");
    assert!(path.exists());
    assert_eq!(fs::read_to_string(&path).unwrap(), "#ifndef SENSOR\n#endif");
}

#[test]
fn msg_codegen_writes_python_stub() {
    let dir = TempDir::new().unwrap();
    let cg = MsgCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));
    cg.write_python_stub("MotorCommand", "class MotorCommand: pass")
        .unwrap();

    let path = dir.path().join("py").join("motor_command.py");
    assert!(path.exists());
    assert_eq!(
        fs::read_to_string(&path).unwrap(),
        "class MotorCommand: pass"
    );
}

#[test]
fn msg_codegen_write_all() {
    let dir = TempDir::new().unwrap();
    let cg = MsgCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));
    cg.write_all("Foo", "c_header", "py_stub").unwrap();

    assert!(dir.path().join("cpp").join("foo.h").exists());
    assert!(dir.path().join("py").join("foo.py").exists());
}

// ═══════════════════════════════════════════════════════════════════
// OpaqueCodeGen 测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn opaque_codegen_legacy_new_puts_all_in_same_dir() {
    let dir = TempDir::new().unwrap();
    let cpp_dir = dir.path().join("cpp");
    let py_dir = dir.path().join("py");

    let cg = OpaqueCodeGen::new(&cpp_dir, &py_dir);
    let fields = vec![OpaqueField::f64("value"), OpaqueField::i32("count")];

    cg.generate_all("MyData", &fields).unwrap();

    assert!(cpp_dir.join("my_data_base.h").exists());
    assert!(cpp_dir.join("my_data.h").exists());
    assert!(py_dir.join("my_data_base.py").exists());
    assert!(py_dir.join("my_data.py").exists());
}

#[test]
fn opaque_codegen_with_user_dirs_separates_output() {
    let dir = TempDir::new().unwrap();
    let base_cpp = dir.path().join("generated/cpp");
    let base_py = dir.path().join("generated/py");
    let user_cpp = dir.path().join("cpp/src");
    let user_py = dir.path().join("py");

    let cg = OpaqueCodeGen::with_user_dirs(&base_cpp, &base_py, &user_cpp, &user_py);
    let fields = vec![OpaqueField::f64("scale")];

    cg.generate_all("CppData", &fields).unwrap();

    // 基类在 generated/
    assert!(base_cpp.join("cpp_data_base.h").exists());
    assert!(base_py.join("cpp_data_base.py").exists());

    // 用户文件在 user dirs
    assert!(user_cpp.join("cpp_data.h").exists());
    assert!(user_py.join("cpp_data.py").exists());
}

#[test]
fn opaque_cpp_base_header_contains_expected_content() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));
    let fields = vec![OpaqueField::f64("gain"), OpaqueField::bool("active")];

    cg.write_cpp_base_header("MyThing", &fields).unwrap();

    let content = fs::read_to_string(dir.path().join("cpp/my_thing_base.h")).unwrap();
    assert!(content.contains("#ifndef ROPLAT_OPAQUE_MYTHING_BASE_H"));
    assert!(content.contains("class MyThingBase"));
    assert!(content.contains("double gain = {};"));
    assert!(content.contains("bool active = {};"));
    assert!(content.contains("double getGain() const"));
    assert!(content.contains("void setGain(double value)"));
    assert!(content.contains("bool getActive() const"));
    assert!(content.contains("roplat_opaque_my_thing_create"));
}

#[test]
fn opaque_user_file_not_overwritten_on_second_call() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));

    cg.write_cpp_user_file("Foo").unwrap();
    let path = dir.path().join("cpp/foo.h");
    let original = fs::read_to_string(&path).unwrap();

    fs::write(&path, "// user modified").unwrap();

    cg.write_cpp_user_file("Foo").unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "// user modified");
    assert_ne!(fs::read_to_string(&path).unwrap(), original);
}

#[test]
fn opaque_python_user_file_not_overwritten() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));

    cg.write_python_user_file("Bar").unwrap();
    let path = dir.path().join("py/bar.py");

    fs::write(&path, "# custom code").unwrap();
    cg.write_python_user_file("Bar").unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "# custom code");
}

#[test]
fn opaque_python_base_contains_expected_content() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::new(dir.path().join("cpp"), dir.path().join("py"));
    let fields = vec![OpaqueField::f64("x"), OpaqueField::u64("id")];

    cg.write_python_base("Sensor", &fields).unwrap();

    let content = fs::read_to_string(dir.path().join("py/sensor_base.py")).unwrap();
    assert!(content.contains("class SensorBase:"));
    assert!(content.contains("self.x: float = 0.0"));
    assert!(content.contains("self.id: int = 0"));
    assert!(content.contains("DO NOT EDIT"));
}

#[test]
fn opaque_cpp_user_only_contains_alias_comment_and_skips_overwrite() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::with_user_dirs(
        dir.path().join("generated/cpp"),
        dir.path().join("generated/py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    cg.write_cpp_user_only("CppData", &[]).unwrap();
    let path = dir.path().join("cpp/src/cpp_data.h");
    let content = fs::read_to_string(&path).unwrap();
    assert!(content.contains("Default pattern: define the concrete fields directly here."));
    assert!(content.contains("Alternative pattern:"));

    fs::write(&path, "// custom user opaque").unwrap();
    cg.write_cpp_user_only("CppData", &[]).unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "// custom user opaque");
}

#[test]
fn opaque_python_user_only_contains_alias_comment_and_skips_overwrite() {
    let dir = TempDir::new().unwrap();
    let cg = OpaqueCodeGen::with_user_dirs(
        dir.path().join("generated/cpp"),
        dir.path().join("generated/py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    cg.write_python_user_only("CppData", &[]).unwrap();
    let path = dir.path().join("py/cpp_data.py");
    let content = fs::read_to_string(&path).unwrap();
    assert!(content.contains("Default pattern: define the concrete fields directly here."));
    assert!(content.contains("Alternative pattern:"));

    fs::write(&path, "# custom user opaque").unwrap();
    cg.write_python_user_only("CppData", &[]).unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "# custom user opaque");
}

// ═══════════════════════════════════════════════════════════════════
// NodeCodeGen 测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn node_spec_builder_pattern() {
    let spec = NodeSpec::new("Controller")
        .input("sensor_in", "SensorData")
        .output("cmd_out", "MotorCommand")
        .field(OpaqueField::f64("gain"));

    assert_eq!(spec.name, "Controller");
    assert_eq!(spec.ports.len(), 2);
    assert_eq!(spec.fields.len(), 1);
    assert_eq!(spec.ports[0].direction, PortDirection::Input);
    assert_eq!(spec.ports[1].direction, PortDirection::Output);
}

#[test]
fn node_codegen_generates_cpp_base() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("MyNode")
        .input("data_in", "SensorData")
        .output("cmd_out", "MotorCommand")
        .field(OpaqueField::f64("kp"))
        .field(OpaqueField::f64("kd"));

    cg.write_cpp_base(&spec).unwrap();

    let content = fs::read_to_string(dir.path().join("base_cpp/my_node_base.h")).unwrap();

    // include guard
    assert!(content.contains("#ifndef ROPLAT_NODE_MYNODE_BASE_H"));
    assert!(content.contains("#define ROPLAT_NODE_MYNODE_BASE_H"));

    // 类定义
    assert!(content.contains("class MyNodeBase"));
    assert!(content.contains("double kp = {};"));
    assert!(content.contains("double kd = {};"));

    // getter/setter
    assert!(content.contains("double getKp() const"));
    assert!(content.contains("void setKp(double value)"));
    assert!(!content.contains("Subscriber<SensorData>"));
    assert!(!content.contains("Publisher<MotorCommand>"));
    assert!(!content.contains("virtual void process()"));
}

#[test]
fn node_codegen_generates_cpp_user() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("Controller")
        .input("sensor_in", "SensorData")
        .output("cmd_out", "MotorCommand");

    cg.write_cpp_user(&spec).unwrap();

    let content = fs::read_to_string(dir.path().join("cpp/src/controller.h")).unwrap();

    assert!(content.contains("#ifndef ROPLAT_NODE_CONTROLLER_H"));
    assert!(content.contains("#include \"roplat/node.h\""));
    assert!(content.contains("sensor_data.h"));
    assert!(content.contains("motor_command.h"));
    assert!(content.contains(
        "class Controller : public ControllerBase, public roplat::Node<SensorData, MotorCommand>"
    ));
    assert!(content.contains("using Input = SensorData;"));
    assert!(content.contains("using Output = MotorCommand;"));
    assert!(content.contains("MotorCommand process(const SensorData &input) override"));
    assert!(content.contains("Input port: sensor_in -> SensorData"));
    assert!(content.contains("Output port: cmd_out -> MotorCommand"));
}

#[test]
fn node_codegen_cpp_user_not_overwritten() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("MyNode");

    cg.write_cpp_user(&spec).unwrap();
    let path = dir.path().join("cpp/src/my_node.h");

    fs::write(&path, "// user code").unwrap();
    cg.write_cpp_user(&spec).unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "// user code");
}

#[test]
fn node_codegen_generates_py_base() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("Planner")
        .input("sensor_in", "SensorData")
        .output("plan_out", "MotorCommand")
        .field(OpaqueField::f64("max_speed"));

    cg.write_py_base(&spec).unwrap();

    let content = fs::read_to_string(dir.path().join("base_py/planner_base.py")).unwrap();

    assert!(content.contains("class PlannerBase:"));
    assert!(content.contains("self.max_speed: float = 0.0"));
    assert!(!content.contains("self._sensor_in = None"));
    assert!(!content.contains("self._plan_out = None"));
    assert!(!content.contains("@abstractmethod"));
    assert!(!content.contains("def process(self):"));
}

#[test]
fn node_codegen_generates_py_user() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("Planner")
        .input("sensor_in", "SensorData")
        .output("plan_out", "MotorCommand");

    cg.write_py_user(&spec).unwrap();

    let content = fs::read_to_string(dir.path().join("py/planner.py")).unwrap();

    assert!(content.contains("from planner_base import PlannerBase"));
    assert!(content.contains("class Planner(PlannerBase, Node):"));
    assert!(content.contains("super().__init__()"));
    assert!(content.contains("def process(self, input_data: 'SensorData') -> 'MotorCommand':"));
    assert!(content.contains("Input port: sensor_in -> SensorData"));
    assert!(content.contains("Output port: plan_out -> MotorCommand"));
}

#[test]
fn node_codegen_py_user_not_overwritten() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("Planner");

    cg.write_py_user(&spec).unwrap();
    let path = dir.path().join("py/planner.py");

    fs::write(&path, "# user code").unwrap();
    cg.write_py_user(&spec).unwrap();
    assert_eq!(fs::read_to_string(&path).unwrap(), "# user code");
}

#[test]
fn node_codegen_generate_all() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("FooNode")
        .input("in1", "MsgA")
        .output("out1", "MsgB")
        .field(OpaqueField::f64("param"));

    cg.generate_all(&spec).unwrap();

    assert!(dir.path().join("base_cpp/foo_node_base.h").exists());
    assert!(dir.path().join("cpp/src/foo_node.h").exists());
    assert!(dir.path().join("base_py/foo_node_base.py").exists());
    assert!(dir.path().join("py/foo_node.py").exists());
}

#[test]
fn node_codegen_generate_cpp_only() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("CppOnly").input("x", "Msg");
    cg.generate_cpp(&spec).unwrap();

    assert!(dir.path().join("base_cpp/cpp_only_base.h").exists());
    assert!(dir.path().join("cpp/src/cpp_only.h").exists());
    // Python 文件不应被生成
    assert!(!dir.path().join("base_py/cpp_only_base.py").exists());
    assert!(!dir.path().join("py/cpp_only.py").exists());
}

#[test]
fn node_codegen_generate_py_only() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("PyOnly").output("y", "Msg");
    cg.generate_py(&spec).unwrap();

    assert!(!dir.path().join("base_cpp/py_only_base.h").exists());
    assert!(dir.path().join("base_py/py_only_base.py").exists());
    assert!(dir.path().join("py/py_only.py").exists());
}

// ═══════════════════════════════════════════════════════════════════
// OpaqueField 测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn opaque_field_convenience_constructors() {
    let f64_field = OpaqueField::f64("x");
    assert_eq!(f64_field.cpp_type, "double");
    assert_eq!(f64_field.python_type_hint, "float");

    let f32_field = OpaqueField::f32("y");
    assert_eq!(f32_field.cpp_type, "float");

    let i32_field = OpaqueField::i32("count");
    assert_eq!(i32_field.cpp_type, "int32_t");
    assert_eq!(i32_field.python_type_hint, "int");

    let u32_field = OpaqueField::u32("id");
    assert_eq!(u32_field.cpp_type, "uint32_t");

    let u64_field = OpaqueField::u64("ts");
    assert_eq!(u64_field.cpp_type, "uint64_t");

    let bool_field = OpaqueField::bool("active");
    assert_eq!(bool_field.cpp_type, "bool");
    assert_eq!(bool_field.python_default, "False");
}

// ═══════════════════════════════════════════════════════════════════
// to_snake_case 测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn snake_case_conversion() {
    assert_eq!(to_snake_case("SensorData"), "sensor_data");
    assert_eq!(to_snake_case("MotorCommand"), "motor_command");
    assert_eq!(to_snake_case("CppController"), "cpp_controller");
    assert_eq!(to_snake_case("PyPlanner"), "py_planner");
    assert_eq!(to_snake_case("ABC"), "a_b_c");
    assert_eq!(to_snake_case("simple"), "simple");
}

// ═══════════════════════════════════════════════════════════════════
// Manifest 读取测试
// ═══════════════════════════════════════════════════════════════════

#[test]
fn manifest_read_msg() {
    let dir = TempDir::new().unwrap();
    let json = serde_json::json!({
        "kind": "msg",
        "name": "SensorData",
        "c_header": "#ifndef X\n#endif",
        "python_stub": "class X: pass"
    });
    fs::write(
        dir.path().join("msg_SensorData.json"),
        serde_json::to_string(&json).unwrap(),
    )
    .unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    assert_eq!(entries.len(), 1);
    match &entries[0] {
        ManifestEntry::Msg(m) => {
            assert_eq!(m.name, "SensorData");
            assert!(m.c_header.contains("#ifndef"));
            assert!(m.python_stub.contains("class X"));
        }
        _ => panic!("Expected Msg manifest"),
    }
}

#[test]
fn manifest_read_opaque() {
    let dir = TempDir::new().unwrap();
    let json = r#"{"kind":"opaque","name":"CppData","lang":"cpp","fields":[{"name":"scale","rust_type":"f64"},{"name":"mode","rust_type":"i32"}]}"#;
    fs::write(dir.path().join("opaque_CppData.json"), json).unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    assert_eq!(entries.len(), 1);
    match &entries[0] {
        ManifestEntry::Opaque(o) => {
            assert_eq!(o.name, "CppData");
            assert_eq!(o.fields.len(), 2);
            let fields = o.to_opaque_fields();
            assert_eq!(fields[0].cpp_type, "double");
            assert_eq!(fields[1].cpp_type, "int32_t");
        }
        _ => panic!("Expected Opaque manifest"),
    }
}

#[test]
fn manifest_read_node() {
    let dir = TempDir::new().unwrap();
    let json = r#"{"kind":"node","name":"CppController","lang":"cpp","ports":[{"name":"in","msg_type":"SensorData","direction":"input"},{"name":"out","msg_type":"MotorCommand","direction":"output"}],"fields":[{"name":"gain","rust_type":"f64"}]}"#;
    fs::write(dir.path().join("node_CppController.json"), json).unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    assert_eq!(entries.len(), 1);
    match &entries[0] {
        ManifestEntry::Node(n) => {
            assert_eq!(n.name, "CppController");
            assert_eq!(n.lang, "cpp");
            assert_eq!(n.ports.len(), 2);
            assert_eq!(n.fields.len(), 1);

            let spec = n.to_node_spec();
            assert_eq!(spec.name, "CppController");
            assert_eq!(spec.ports.len(), 2);
            assert_eq!(spec.ports[0].direction, PortDirection::Input);
            assert_eq!(spec.ports[1].direction, PortDirection::Output);
            assert_eq!(spec.fields[0].cpp_type, "double");
        }
        _ => panic!("Expected Node manifest"),
    }
}

#[test]
fn node_codegen_cpp_base_does_not_include_opaque_port_headers() {
    let dir = TempDir::new().unwrap();
    let cg = NodeCodeGen::new(
        dir.path().join("base_cpp"),
        dir.path().join("base_py"),
        dir.path().join("cpp/src"),
        dir.path().join("py"),
    );

    let spec = NodeSpec::new("OpaqueNode")
        .opaque_input("in", "CppData")
        .opaque_output("out", "CppData");

    cg.write_cpp_base(&spec).unwrap();

    let content = fs::read_to_string(dir.path().join("base_cpp/opaque_node_base.h")).unwrap();
    assert!(!content.contains("cpp_data.h"));
    assert!(content.contains("void  roplat_node_opaque_node_process(void* ptr);"));
}

#[test]
fn manifest_read_node_resolves_opaque_ports_from_same_manifest_dir() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("opaque_CppData.json"),
        r#"{"kind":"opaque","name":"CppData","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("node_CppForward.json"),
        r#"{"kind":"node","name":"CppForward","lang":"cpp","ports":[{"name":"in","msg_type":"CppData","direction":"input"},{"name":"out","msg_type":"CppData","direction":"output"}],"fields":[]}"#,
    )
    .unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    let node = entries
        .iter()
        .find_map(|entry| match entry {
            ManifestEntry::Node(node) => Some(node),
            _ => None,
        })
        .expect("node manifest");

    assert!(node.ports.iter().all(|port| port.is_opaque));

    let spec = node.to_node_spec();
    assert!(spec.ports.iter().all(|port| port.is_opaque));
}

#[test]
fn manifest_read_node_resolves_qualified_opaque_port() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("opaque_dep_CppData.json"),
        r#"{"kind":"opaque","name":"CppData","package":"dep-msgs","crate_name":"dep_msgs","rust_type":"dep_msgs::CppData","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("opaque_other_CppData.json"),
        r#"{"kind":"opaque","name":"CppData","package":"other-msgs","crate_name":"other_msgs","rust_type":"other_msgs::CppData","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("node_CppForward.json"),
        r#"{"kind":"node","name":"CppForward","package":"app","crate_name":"app","lang":"cpp","ports":[{"name":"in","msg_type":"CppData","rust_type":"dep_msgs :: CppData","direction":"input"}],"fields":[]}"#,
    )
    .unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    let node = entries
        .iter()
        .find_map(|entry| match entry {
            ManifestEntry::Node(node) => Some(node),
            _ => None,
        })
        .expect("node manifest");

    assert!(node.ports[0].is_opaque);
}

#[test]
fn manifest_read_node_reports_ambiguous_short_opaque_port() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("opaque_dep_CppData.json"),
        r#"{"kind":"opaque","name":"CppData","package":"dep-msgs","crate_name":"dep_msgs","rust_type":"dep_msgs::CppData","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("opaque_other_CppData.json"),
        r#"{"kind":"opaque","name":"CppData","package":"other-msgs","crate_name":"other_msgs","rust_type":"other_msgs::CppData","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("node_CppForward.json"),
        r#"{"kind":"node","name":"CppForward","package":"app","crate_name":"app","lang":"cpp","ports":[{"name":"in","msg_type":"CppData","direction":"input"}],"fields":[]}"#,
    )
    .unwrap();

    let err = read_all_manifests(dir.path()).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    assert!(err.to_string().contains("ambiguous roplat opaque type"));
}

#[test]
fn manifest_read_empty_dir() {
    let dir = TempDir::new().unwrap();
    let entries = read_all_manifests(dir.path()).unwrap();
    assert!(entries.is_empty());
}

#[test]
fn manifest_read_nonexistent_dir() {
    let entries = read_all_manifests(std::path::Path::new("/nonexistent/path")).unwrap();
    assert!(entries.is_empty());
}

#[test]
fn manifest_field_to_opaque_field_all_types() {
    let types = [
        ("f64", "double"),
        ("f32", "float"),
        ("i32", "int32_t"),
        ("u32", "uint32_t"),
        ("u64", "uint64_t"),
        ("bool", "bool"),
    ];
    for (rust_type, expected_cpp) in types {
        let field = FieldManifest { name: "x".to_string(), rust_type: rust_type.to_string() };
        assert_eq!(field.to_opaque_field().cpp_type, expected_cpp);
    }
}

#[test]
fn manifest_read_multiple_files() {
    let dir = TempDir::new().unwrap();
    fs::write(
        dir.path().join("msg_A.json"),
        r#"{"kind":"msg","name":"A","c_header":"h","python_stub":"s"}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("opaque_B.json"),
        r#"{"kind":"opaque","name":"B","lang":"cpp","fields":[]}"#,
    )
    .unwrap();
    fs::write(
        dir.path().join("node_C.json"),
        r#"{"kind":"node","name":"C","lang":"py","ports":[],"fields":[]}"#,
    )
    .unwrap();

    let entries = read_all_manifests(dir.path()).unwrap();
    assert_eq!(entries.len(), 3);
}
