//! Consolidated read-only integration tests.
//!
//! All tests that only READ from the Ghidra project share a single
//! DaemonTestHarness to avoid redundant import+analyze cycles.

use serial_test::serial;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;

#[macro_use]
mod common;
use common::{
    ensure_test_project, get_function_address, ghidra,
    helpers::matches_function_name,
    schemas::{DisasmResult, Function, GraphResult, MemoryBlock, StringData, Validate, XRef},
    DaemonTestHarness, GhidraCommand,
};

const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";

/// Known exported function names from sample_binary
const KNOWN_FUNCTIONS: &[&str] = &[
    "add_numbers",
    "multiply",
    "factorial",
    "fibonacci",
    "process_string",
    "xor_encrypt",
    "simple_hash",
    "init_struct",
    "main",
];

static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();

fn harness() -> &'static DaemonTestHarness {
    HARNESS.get_or_init(|| {
        ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
        DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
    })
}

// ============================================================================
// Function List Tests
// ============================================================================

#[test]
#[serial]
fn test_function_list_schema_validation() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("function")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let functions: Vec<Function> = result.json();
    assert!(!functions.is_empty(), "Function list should not be empty");

    for func in &functions {
        func.assert_valid();
    }

    let has_main = functions.iter().any(|f| f.name == "main");
    assert!(has_main, "Should contain main function");
}

#[test]
#[serial]
fn test_function_list_contains_expected_functions() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("function")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let functions: Vec<Function> = result.json();
    let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();

    // main must always be present
    assert!(
        names.iter().any(|n| matches_function_name(n, "main")),
        "Should have main function. Found: {:?}",
        &names[..names.len().min(20)]
    );

    // Binary should have many functions (stdlib + user code)
    assert!(
        functions.len() >= 5,
        "Should have at least 5 functions, found {}",
        functions.len()
    );

    // Check how many known functions we can find (informational, not hard failure)
    let mut found_count = 0;
    for expected in KNOWN_FUNCTIONS {
        if names.iter().any(|n| matches_function_name(n, expected)) {
            found_count += 1;
        }
    }
    eprintln!(
        "Found {}/{} known functions from sample_binary",
        found_count,
        KNOWN_FUNCTIONS.len()
    );
}

#[test]
#[serial]
fn test_function_list_limit() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("function")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .arg("--limit")
        .arg("3")
        .run();

    result.assert_success();

    let functions: Vec<Function> = result.json();
    assert!(
        functions.len() <= 3,
        "Limit 3 should return at most 3 functions, got {}",
        functions.len()
    );
}

#[test]
#[serial]
fn test_function_list_filter() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("function")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .arg("--filter")
        .arg("main")
        .run();

    result.assert_success();

    let functions: Vec<Function> = result.json();
    assert!(
        !functions.is_empty(),
        "Filter 'main' should match at least one function"
    );

    let has_main = functions
        .iter()
        .any(|f| f.name.to_lowercase().contains("main"));
    assert!(
        has_main,
        "At least one filtered result should contain 'main'. Got: {:?}",
        functions.iter().map(|f| &f.name).collect::<Vec<_>>()
    );
}

// ============================================================================
// Strings List Tests
// ============================================================================

#[test]
#[serial]
fn test_strings_list_schema_validation() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("strings")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .arg("--limit")
        .arg("50")
        .run();

    result.assert_success();

    let strings: Vec<StringData> = result.json();
    assert!(!strings.is_empty(), "Should have at least one string");

    for s in &strings {
        s.assert_valid();
    }

    // Check if any known strings are present (informational)
    let known = ["Hello", "test_binary", "super_secret"];
    let found: Vec<_> = known
        .iter()
        .filter(|k| strings.iter().any(|s| s.value.contains(*k)))
        .collect();
    if !found.is_empty() {
        eprintln!("Found known strings: {:?}", found);
    }
}

// ============================================================================
// Memory Map Tests
// ============================================================================

#[test]
#[serial]
fn test_memory_map_schema_validation() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("memory")
        .arg("map")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let blocks: Vec<MemoryBlock> = result.json();
    assert!(
        !blocks.is_empty(),
        "Memory map should have at least one block"
    );

    for block in &blocks {
        block.assert_valid();
    }

    let has_text = blocks
        .iter()
        .any(|b| b.name.contains("text") || b.name.contains("code") || b.name.contains(".text"));
    assert!(
        has_text,
        "Should have a text/code segment. Found: {:?}",
        blocks.iter().map(|b| &b.name).collect::<Vec<_>>()
    );
}

// ============================================================================
// Summary Tests
// ============================================================================

#[test]
#[serial]
fn test_summary_contains_expected_fields() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("summary")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    assert!(
        !result.stdout.trim().is_empty(),
        "Summary should produce output"
    );
    result.assert_stdout_contains("sample_binary");
}

// ============================================================================
// Decompile Tests
// ============================================================================

#[test]
#[serial]
fn test_decompile_by_name() {
    require_ghidra!();
    let harness = harness();

    // Use "main" instead of "add_numbers" since add_numbers may be inlined on macOS
    let result = ghidra(harness)
        .arg("decompile")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();

    assert!(
        result.stdout.contains("return")
            || result.stdout.contains("param")
            || result.stdout.contains("int")
            || result.stdout.contains("long")
            || result.stdout.contains("void"),
        "Decompiled output should contain C-like code keywords.\nGot: {}",
        result.stdout
    );
}

#[test]
#[serial]
fn test_decompile_by_address() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("decompile")
        .arg(&main_addr)
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    assert!(
        !result.stdout.trim().is_empty(),
        "Decompile should produce output"
    );
}

#[test]
#[serial]
fn test_decompile_nonexistent_function() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("decompile")
        .arg("this_function_definitely_does_not_exist_xyz123")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    if result.exit_code == 0 {
        assert!(
            result.stdout.to_lowercase().contains("not found")
                || result.stdout.to_lowercase().contains("error")
                || result.stdout.trim().is_empty(),
            "Should indicate function not found"
        );
    }
}

// ============================================================================
// XRef Tests
// ============================================================================

#[test]
#[serial]
fn test_xref_to() {
    require_ghidra!();
    let harness = harness();

    let addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "add_numbers");

    let result = ghidra(harness)
        .arg("xref")
        .arg("to")
        .arg(&addr)
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(xrefs) = result.try_json::<Vec<XRef>>() {
        if let Some(xref) = xrefs.iter().find(|x| {
            x.from_function
                .as_deref()
                .is_some_and(|f| f.contains("main"))
        }) {
            eprintln!("Found xref from main: {:?}", xref);
        } else {
            eprintln!(
                "No xref from main found (may vary by platform). Xrefs: {:?}",
                xrefs
            );
        }
    }
}

#[test]
#[serial]
fn test_xref_from() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("xref")
        .arg("from")
        .arg(&main_addr)
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    // main should have outgoing xrefs, but don't hard-fail if format differs
    if let Some(xrefs) = result.try_json::<Vec<XRef>>() {
        eprintln!("Found {} xrefs from main", xrefs.len());
    }
}

// ============================================================================
// Find Tests
// ============================================================================

#[test]
#[serial]
fn test_find_string() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("string")
        .arg("Hello")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("Hello, Ghidra CLI!");
}

#[test]
#[serial]
fn test_find_bytes() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("bytes")
        .arg("4883ec08")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
}

#[test]
#[serial]
fn test_find_function() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("function")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("main");
}

#[test]
#[serial]
fn test_find_function_glob() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("function")
        .arg("m*")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("main");
}

#[test]
#[serial]
fn test_find_calls() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("calls")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
}

#[test]
#[serial]
fn test_find_crypto() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("crypto")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let _: serde_json::Value = result.json();
}

#[test]
#[serial]
fn test_find_interesting() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("interesting")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let _: serde_json::Value = result.json();
}

#[test]
#[serial]
fn test_find_string_no_matches() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("find")
        .arg("string")
        .arg("nonexistent_string_xyz123")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(json) = result.try_json::<serde_json::Value>() {
        if let Some(arr) = json.as_array() {
            assert!(
                arr.is_empty(),
                "Should have no matches for nonexistent string"
            );
        }
    }
}

// ============================================================================
// Graph Tests
// ============================================================================

#[test]
#[serial]
fn test_graph_calls() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("graph")
        .arg("calls")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("nodes");
    result.assert_stdout_contains("edges");
}

#[test]
#[serial]
fn test_graph_callers() {
    require_ghidra!();
    let harness = harness();

    // Use "main" instead of "add_numbers" since add_numbers may be inlined on macOS
    let result = ghidra(harness)
        .arg("graph")
        .arg("callers")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(graph) = result.try_json::<GraphResult>() {
        eprintln!("Callers graph for main has {} nodes", graph.nodes.len());
    }
}

#[test]
#[serial]
fn test_graph_callees() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("graph")
        .arg("callees")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(graph) = result.try_json::<GraphResult>() {
        let node_labels: Vec<_> = graph
            .nodes
            .iter()
            .filter_map(|n| n.label.as_deref())
            .collect();

        let has_add_numbers = node_labels
            .iter()
            .any(|l| l.contains("add_numbers") || l.contains("_add_numbers"));
        let has_multiply = node_labels
            .iter()
            .any(|l| l.contains("multiply") || l.contains("_multiply"));

        if has_add_numbers {
            eprintln!("Found add_numbers in callees");
        }
        if has_multiply {
            eprintln!("Found multiply in callees");
        }
    }
}

#[test]
#[serial]
fn test_graph_export_dot() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("graph")
        .arg("export")
        .arg("dot")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("digraph");
}

// ============================================================================
// Stats Tests
// ============================================================================

#[test]
#[serial]
fn test_stats_normal() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("stats")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();
    result.assert_stdout_contains("stats");
    result.assert_stdout_contains("functions");
    result.assert_stdout_contains("symbols");
}

#[test]
#[serial]
fn test_stats_has_all_fields() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("stats")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();

    // Stats may be returned as flat object or wrapped: [{"stats": {...}}]
    let obj = if let Some(obj) = json.as_object() {
        obj.clone()
    } else if let Some(arr) = json.as_array() {
        arr.first()
            .and_then(|v| v.as_object())
            .and_then(|o| o.get("stats"))
            .and_then(|v| v.as_object())
            .expect("Expected stats object in array wrapper")
            .clone()
    } else {
        panic!("Stats should be a JSON object or array");
    };

    // Verify key fields exist
    for key in &["functions", "strings", "symbols"] {
        assert!(obj.contains_key(*key), "Missing stats field: {}", key);
    }

    let functions = obj.get("functions").and_then(|v| v.as_u64()).unwrap_or(0);
    assert!(
        functions > 0,
        "functions count should be > 0, got {}",
        functions
    );

    let strings = obj.get("strings").and_then(|v| v.as_u64()).unwrap_or(0);
    assert!(strings > 0, "strings count should be > 0, got {}", strings);
}

#[test]
#[serial]
fn test_stats_json_format() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("stats")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();

    // Verify output is valid JSON
    let json: serde_json::Value = result.json();

    // Extract stats object (may be flat or wrapped)
    let stats = if json.is_object() {
        json.clone()
    } else if let Some(arr) = json.as_array() {
        arr.first()
            .and_then(|v| v.as_object())
            .and_then(|o| o.get("stats"))
            .cloned()
            .expect("Expected stats in array wrapper")
    } else {
        panic!("Expected JSON object or array");
    };

    // Verify it has numeric function count
    let functions = stats
        .get("functions")
        .and_then(|v| v.as_u64())
        .expect("Should have numeric functions field");
    assert!(
        functions >= 8,
        "Should have at least 8 functions, got {}",
        functions
    );

    let strings = stats
        .get("strings")
        .and_then(|v| v.as_u64())
        .expect("Should have numeric strings field");
    assert!(
        strings >= 3,
        "Should have at least 3 strings, got {}",
        strings
    );
}

// ============================================================================
// Disassembly Tests
// ============================================================================

#[test]
#[serial]
fn test_disasm_at_main() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(disasm) = result.try_json::<DisasmResult>() {
        assert!(
            !disasm.results.is_empty(),
            "Should have at least one instruction"
        );
        for instr in &disasm.results {
            instr.assert_valid();
        }
    } else if let Some(instructions) = result.try_json::<Vec<common::schemas::Instruction>>() {
        assert!(
            !instructions.is_empty(),
            "Should have at least one instruction"
        );
        for instr in &instructions {
            instr.assert_valid();
        }
    }
}

#[test]
#[serial]
fn test_disasm_with_instruction_limit() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
    let limit = 5;

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .arg("--instructions")
        .arg(limit.to_string())
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(disasm) = result.try_json::<DisasmResult>() {
        assert!(
            disasm.results.len() <= limit,
            "Should return at most {} instructions, got {}",
            limit,
            disasm.results.len()
        );
        for instr in &disasm.results {
            instr.assert_valid();
        }
    } else if let Some(instructions) = result.try_json::<Vec<common::schemas::Instruction>>() {
        assert!(
            instructions.len() <= limit,
            "Should return at most {} instructions, got {}",
            limit,
            instructions.len()
        );
    }
}

#[test]
#[serial]
fn test_disasm_small_count() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .arg("--instructions")
        .arg("1")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(disasm) = result.try_json::<DisasmResult>() {
        assert!(
            disasm.results.len() <= 1,
            "Should return at most 1 instruction, got {}",
            disasm.results.len()
        );
    }
}

#[test]
#[serial]
fn test_disasm_instruction_fields() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .arg("--instructions")
        .arg("10")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    if let Some(disasm) = result.try_json::<DisasmResult>() {
        assert!(!disasm.results.is_empty(), "Should have instructions");

        let first = &disasm.results[0];
        assert!(!first.mnemonic.is_empty(), "Mnemonic should not be empty");
        assert!(!first.address.is_empty(), "Address should not be empty");

        let addr_hex = first
            .address
            .strip_prefix("0x")
            .or_else(|| first.address.strip_prefix("0X"))
            .unwrap_or(&first.address);
        assert!(
            !addr_hex.is_empty() && addr_hex.bytes().all(|b| b.is_ascii_hexdigit()),
            "Address should be hex format, got: {}",
            first.address
        );

        let common_first_instr = [
            "PUSH", "SUB", "MOV", "ENDBR", "LEA", "XOR", "JMP", // x86
            "STP", "STR", "BL", "NOP", "ADRP", "ADD", "RET", // ARM64
        ];
        let mnemonic_upper = first.mnemonic.to_uppercase();

        if !common_first_instr
            .iter()
            .any(|&m| mnemonic_upper.starts_with(m))
        {
            eprintln!(
                "Note: First instruction is '{}' - unusual but not necessarily wrong",
                first.mnemonic
            );
        }
    }
}

#[test]
#[serial]
fn test_disasm_invalid_address() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("disasm")
        .arg("0xFFFFFFFFFFFFFFFF")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    if result.exit_code == 0 {
        if let Some(_disasm) = result.try_json::<DisasmResult>() {
            // Empty results are acceptable for unmapped address
        }
    } else {
        assert!(
            !result.stderr.is_empty() || !result.stdout.is_empty(),
            "Should provide some output explaining the error"
        );
    }
}

#[test]
#[serial]
fn test_disasm_missing_program() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness).arg("disasm").arg("0x101000").run();

    result.assert_failure();
}

#[test]
#[serial]
fn test_disasm_zero_instructions() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .arg("--instructions")
        .arg("0")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    if result.exit_code == 0 {
        if let Some(disasm) = result.try_json::<DisasmResult>() {
            assert!(
                disasm.results.is_empty(),
                "Zero instruction count should return empty results"
            );
        }
    }
}

// ============================================================================
// Diff Tests
// ============================================================================

#[test]
#[serial]
fn test_diff_programs() {
    require_ghidra!();
    harness();

    let result = GhidraCommand::new()
        .arg("diff")
        .arg("programs")
        .arg(TEST_PROGRAM)
        .arg(TEST_PROGRAM)
        .arg("--project")
        .arg(TEST_PROJECT)
        .run();

    result.assert_success();

    let output_lower = result.stdout.to_lowercase();
    assert!(
        output_lower.contains("identical")
            || output_lower.contains("0")
            || result.stdout.trim().is_empty()
            || output_lower.contains("no diff")
            || output_lower.contains("same"),
        "Self-diff should indicate identical/no differences. Got: {}",
        result.stdout
    );
}

#[test]
#[serial]
fn test_diff_functions() {
    require_ghidra!();
    harness();

    let result = GhidraCommand::new()
        .arg("diff")
        .arg("functions")
        .arg("main")
        .arg("main")
        .arg("--project")
        .arg(TEST_PROJECT)
        .run();

    result.assert_success();
}

#[test]
#[serial]
fn test_diff_functions_different() {
    require_ghidra!();
    harness();

    // Self-diff main vs main - use same function to avoid depending on
    // add_numbers which may be inlined on macOS
    let result = GhidraCommand::new()
        .arg("diff")
        .arg("functions")
        .arg("main")
        .arg("main")
        .arg("--project")
        .arg(TEST_PROJECT)
        .run();

    result.assert_success();
    // Self-diff should succeed (output may be empty for identical functions)
}

// ============================================================================
// Program Tests
// ============================================================================

#[test]
#[serial]
fn test_program_info() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("program")
        .arg("info")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();

    // Program info should mention the program name
    assert!(
        result.stdout.contains("sample_binary") || result.stdout.contains("name"),
        "Program info should contain program name or 'name' field. Got: {}",
        &result.stdout[..result.stdout.len().min(500)]
    );
}

#[test]
#[serial]
fn test_program_export_json() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("program")
        .arg("export")
        .arg("json")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    if result.exit_code == 0 {
        assert!(
            result.stdout.contains("functions") || !result.stdout.is_empty(),
            "Export should produce output"
        );
    }
    // Accept "Unknown command" gracefully
}

#[test]
#[serial]
fn test_program_close() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("program")
        .arg("close")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    assert!(
        result.exit_code == 0 || result.stderr.contains("Unknown command"),
        "Expected success or 'Unknown command', got: {}",
        result.stderr
    );
}

#[test]
#[serial]
fn test_program_info_no_program() {
    require_ghidra!();
    let harness = harness();

    let result = GhidraCommand::new()
        .arg("program")
        .arg("info")
        .with_daemon(harness)
        .run();

    result.assert_success();
}

// ============================================================================
// Batch Tests
// ============================================================================

fn create_batch_file(content: &str) -> PathBuf {
    let temp_dir = std::env::temp_dir();
    let batch_file = temp_dir.join(format!("ghidra_batch_{}.txt", std::process::id()));
    fs::write(&batch_file, content).expect("Failed to write batch file");
    batch_file
}

#[test]
#[serial]
fn test_batch_multiple_queries() {
    require_ghidra!();
    harness();

    let batch_content = r#"
# Test batch file
query --address 0x100000
query --function main
"#;

    let batch_file = create_batch_file(batch_content);

    let result = GhidraCommand::new()
        .arg("batch")
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg(batch_file.to_str().unwrap())
        .run();

    result.assert_success();
    result.assert_stdout_contains("commands_parsed");
    result.assert_stdout_contains("results");

    fs::remove_file(batch_file).ok();
}

#[test]
#[serial]
fn test_batch_empty_file() {
    require_ghidra!();
    harness();

    let batch_content = r#"
# Only comments


# More comments
"#;

    let batch_file = create_batch_file(batch_content);

    let result = GhidraCommand::new()
        .arg("batch")
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg(batch_file.to_str().unwrap())
        .run();

    result.assert_success();
    result.assert_stdout_contains("commands_parsed");

    fs::remove_file(batch_file).ok();
}

#[test]
#[serial]
fn test_batch_with_comments() {
    require_ghidra!();
    harness();

    let batch_content = r#"
# Query main function
query --function main
# Query by address
query --address 0x100000
# Another comment
"#;

    let batch_file = create_batch_file(batch_content);

    let result = GhidraCommand::new()
        .arg("batch")
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg(batch_file.to_str().unwrap())
        .run();

    result.assert_success();
    result.assert_stdout_contains("commands_parsed");
    result.assert_stdout_contains("2");

    fs::remove_file(batch_file).ok();
}

#[test]
#[serial]
fn test_batch_invalid_file() {
    require_ghidra!();
    harness();

    let result = GhidraCommand::new()
        .arg("batch")
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg("/nonexistent/batch/file.txt")
        .run();

    result.assert_failure();
    assert!(
        result.stderr.contains("not found") || result.stderr.contains("No such file"),
        "Should contain file-not-found error. Got: {}",
        result.stderr
    );
}

#[test]
#[serial]
fn test_batch_with_invalid_command() {
    require_ghidra!();
    harness();

    let batch_content = r#"
query --function main
invalid-command --arg value
query --address 0x100000
"#;

    let batch_file = create_batch_file(batch_content);

    let result = GhidraCommand::new()
        .arg("batch")
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg(batch_file.to_str().unwrap())
        .run();

    result.assert_success();
    result.assert_stdout_contains("commands_parsed");
    result.assert_stdout_contains("3");

    fs::remove_file(batch_file).ok();
}

// ============================================================================
// Insta Snapshot Tests
// ============================================================================

#[test]
#[serial]
#[ignore] // Run `cargo insta test --review` to bootstrap snapshots
fn test_snapshot_function_list_structure() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("function")
        .arg("list")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .arg("--limit")
        .arg("1")
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();
    insta::assert_json_snapshot!("function_list_structure", json, {
        "[].address" => "[ADDR]",
        "[].entry_point" => "[ADDR]",
        "[].size" => "[SIZE]",
        "[].parameters[].ordinal" => "[N]",
        "[].local_variables[].stack_offset" => "[N]",
        "[].calls[]" => "[ADDR]",
        "[].called_by[]" => "[ADDR]",
    });
}

#[test]
#[serial]
#[ignore] // Run `cargo insta test --review` to bootstrap snapshots
fn test_snapshot_stats_structure() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("stats")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();
    insta::assert_json_snapshot!("stats_structure", json, {
        ".functions" => "[N]",
        ".instructions" => "[N]",
        ".strings" => "[N]",
        ".symbols" => "[N]",
        ".imports" => "[N]",
        ".exports" => "[N]",
        ".memory_blocks" => "[N]",
        ".memory_size" => "[N]",
        ".sections" => "[N]",
        ".data_types" => "[N]",
    });
}

#[test]
#[serial]
#[ignore] // Run `cargo insta test --review` to bootstrap snapshots
fn test_snapshot_memory_map_structure() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("memory")
        .arg("map")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();
    insta::assert_json_snapshot!("memory_map_structure", json, {
        "[].start" => "[ADDR]",
        "[].end" => "[ADDR]",
        "[].size" => "[SIZE]",
    });
}

#[test]
#[serial]
#[ignore] // Run `cargo insta test --review` to bootstrap snapshots
fn test_snapshot_disasm_structure() {
    require_ghidra!();
    let harness = harness();

    let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");

    let result = ghidra(harness)
        .arg("disasm")
        .arg(&main_addr)
        .arg("--instructions")
        .arg("3")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();
    insta::assert_json_snapshot!("disasm_structure", json, {
        ".results[].address" => "[ADDR]",
        ".results[].operands" => "[OPS]",
        ".results[].bytes" => "[BYTES]",
        ".results[].length" => "[N]",
        ".start_address" => "[ADDR]",
        ".end_address" => "[ADDR]",
        "[].address" => "[ADDR]",
        "[].operands" => "[OPS]",
        "[].bytes" => "[BYTES]",
        "[].length" => "[N]",
    });
}

#[test]
#[serial]
#[ignore] // Run `cargo insta test --review` to bootstrap snapshots
fn test_snapshot_graph_callees_structure() {
    require_ghidra!();
    let harness = harness();

    let result = ghidra(harness)
        .arg("graph")
        .arg("callees")
        .arg("main")
        .with_project(TEST_PROJECT, TEST_PROGRAM)
        .json_format()
        .run();

    result.assert_success();

    let json: serde_json::Value = result.json();
    insta::assert_json_snapshot!("graph_callees_structure", json, {
        ".nodes[].id" => "[ID]",
        ".nodes[].address" => "[ADDR]",
        ".edges[].from" => "[ID]",
        ".edges[].to" => "[ID]",
    });
}
