//! Boolean Query Syntax Regression Tests
//!
//! These tests ensure boolean query syntax works correctly in all execution modes
//! and prevent regression of parser migration issues.
//!
//! **Purpose:** Verify that the new boolean query parser is properly integrated
//! throughout the codebase, especially in hybrid search.
//!
//! **Coverage:**
//! - AND operator: `kind:function AND name:test`
//! - OR operator: `kind:function OR kind:struct`
//! - NOT operator: `kind:function AND NOT name~=/test/`
//! - Parentheses: `(kind:function OR kind:method) AND lang:rust`
//! - Regex with boolean: `kind:function AND name~=/^get_/`
//! - Complex expressions: Multiple operators combined

mod common;

use assert_cmd::Command;
use common::sqry_bin;
use serde_json::Value;
use sqry_core::test_support::verbosity;
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::Path;
use std::sync::Once;
use tempfile::TempDir;

static INIT: Once = Once::new();

fn init_logging() {
    INIT.call_once(|| {
        verbosity::init(env!("CARGO_PKG_NAME"));
    });
}

fn create_test_project(files: &[(&str, &str)]) -> TempDir {
    let dir = TempDir::new().unwrap();
    for (path, content) in files {
        let file_path = dir.path().join(path);
        fs::create_dir_all(file_path.parent().unwrap()).unwrap();
        fs::write(&file_path, content).unwrap();
    }
    dir
}

fn sqry_cmd() -> Command {
    Command::new(sqry_bin())
}

/// Helper: Create project and build index
fn create_indexed_project(files: &[(&str, &str)]) -> TempDir {
    let project = create_test_project(files);

    // Build index
    sqry_cmd()
        .arg("index")
        .current_dir(project.path())
        .assert()
        .success();

    project
}

fn copy_fixture_dir(relative: &str) -> TempDir {
    let source = Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("workspace root")
        .join(relative);
    let temp = TempDir::new().expect("create temp dir");
    copy_dir_all(&source, temp.path()).expect("copy fixture into temp dir");
    temp
}

fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> {
    fs::create_dir_all(dst)?;
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        let dest_path = dst.join(entry.file_name());
        if ty.is_dir() {
            copy_dir_all(&entry.path(), &dest_path)?;
        } else if ty.is_file() {
            fs::copy(entry.path(), dest_path)?;
        }
    }
    Ok(())
}

fn extract_symbol_names(json: &Value) -> Vec<String> {
    json["results"]
        .as_array()
        .map(|arr| {
            arr.iter()
                .filter_map(|item| item.get("name").and_then(|n| n.as_str()))
                .map(std::string::ToString::to_string)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

// ============================================================================
// AND Operator Tests
// ============================================================================

#[test]
fn test_boolean_and_basic() {
    init_logging();
    log::info!("REGRESSION TEST: Boolean AND operator (kind:function AND name:public)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        pub fn public_func() {}
        fn private_func() {}
        pub struct PublicStruct {}
        ",
    )]);

    // Query with AND: must match both conditions
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND name~=/public/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "Boolean AND query failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(
        stdout.contains("public_func"),
        "Expected public_func, got: {stdout}"
    );
    assert!(
        !stdout.contains("private_func"),
        "Should not contain private_func, got: {stdout}"
    );

    log::info!("✓ PASS: AND operator correctly filters by both conditions");
}

#[test]
fn test_boolean_and_with_regex() {
    init_logging();
    log::info!("REGRESSION TEST: AND with regex patterns (kind:function AND name~=/^public_/)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        pub fn public_async() {}
        pub fn public_sync() {}
        fn private_func() {}
        ",
    )]);

    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND name~=/^public_/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "Boolean AND with regex failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find functions starting with "public_"
    assert!(
        stdout.contains("public_async") || stdout.contains("public_sync"),
        "Expected public_ functions, got: {stdout}"
    );
    // Should NOT find private functions
    assert!(
        !stdout.contains("private_func"),
        "Should not contain private_func, got: {stdout}"
    );

    log::info!("✓ PASS: AND with regex patterns works correctly");
}

// ============================================================================
// OR Operator Tests
// ============================================================================

#[test]
fn test_boolean_or_basic() {
    init_logging();
    log::info!("REGRESSION TEST: Boolean OR operator (kind:function OR kind:struct)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        fn test_function() {}
        struct TestStruct {}
        mod test_module {}
        ",
    )]);

    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function OR kind:struct")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find function or struct (or both)
    assert!(
        stdout.contains("test_function") || stdout.contains("TestStruct"),
        "Expected function or struct, got: {stdout}"
    );
    // Should NOT find module
    assert!(
        !stdout.contains("test_module"),
        "Should not contain module, got: {stdout}"
    );

    log::info!("✓ PASS: OR operator matches either condition");
}

// ============================================================================
// NOT Operator Tests
// ============================================================================

#[test]
fn test_boolean_not_basic() {
    init_logging();
    log::info!("REGRESSION TEST: Boolean NOT operator (kind:function AND NOT name~=/test/)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        fn test_one() {}
        fn test_two() {}
        fn production_code() {}
        ",
    )]);

    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND NOT name~=/test/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find production_code (function but NOT test)
    assert!(
        stdout.contains("production"),
        "Expected production_code, got: {stdout}"
    );
    // Should NOT find test functions
    assert!(
        !stdout.contains("test_one") && !stdout.contains("test_two"),
        "Should not contain test functions, got: {stdout}"
    );

    log::info!("✓ PASS: NOT operator correctly excludes matches");
}

// ============================================================================
// Parentheses and Grouping Tests
// ============================================================================

#[test]
fn test_boolean_parentheses_grouping() {
    init_logging();
    log::info!("REGRESSION TEST: Parentheses grouping ((A OR B) AND C)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        pub fn public_function() {}
        fn private_function() {}
        pub struct PublicStruct {}
        ",
    )]);

    // (kind:function OR kind:struct) AND name~=/public/i
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("(kind:function OR kind:struct) AND name~=/public/i")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find symbols that are (function OR struct) AND contain "public"
    assert!(
        stdout.contains("public") || stdout.contains("Public"),
        "Expected public symbols, got: {stdout}"
    );

    log::info!("✓ PASS: Parentheses correctly group OR before AND");
}

// ============================================================================
// Regex with Boolean Operators
// ============================================================================

#[test]
fn test_boolean_regex_pattern() {
    init_logging();
    log::info!("REGRESSION TEST: Regex with AND (kind:function AND name~=/^get_/)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        fn get_user() {}
        fn get_data() {}
        fn set_user() {}
        fn user_getter() {}
        ",
    )]);

    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND name~=/^get_/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find functions starting with "get_"
    assert!(
        stdout.contains("get_user") || stdout.contains("get_data"),
        "Expected get_* functions, got: {stdout}"
    );
    // Should NOT find set_user or user_getter
    assert!(
        !stdout.contains("set_user") && !stdout.contains("user_getter"),
        "Should not contain set_ or *getter, got: {stdout}"
    );

    log::info!("✓ PASS: Regex patterns work correctly with boolean operators");
}

// ============================================================================
// Complex Expression Tests
// ============================================================================

#[test]
fn test_boolean_complex_expression() {
    init_logging();
    log::info!("REGRESSION TEST: Complex expression (A AND (B OR C) AND NOT D)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        pub fn public_prod() {}
        pub fn public_test() {}
        fn private_prod() {}
        fn private_test() {}
        ",
    )]);

    // kind:function AND NOT name~=/test/
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND NOT name~=/test/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();

    // Should find prod functions (public_prod, private_prod)
    assert!(
        stdout.contains("prod"),
        "Expected prod functions, got: {stdout}"
    );
    // Should NOT find test in function names (ignore "test.rs" in path)
    assert!(
        !stdout.contains("public_test") && !stdout.contains("private_test"),
        "Should not contain test functions, got: {stdout}"
    );

    log::info!("✓ PASS: Complex boolean expressions evaluate correctly");
}

// ============================================================================
// Hybrid Search Integration Tests
// ============================================================================

#[test]
fn test_hybrid_mode_preserves_boolean_syntax() {
    init_logging();
    log::info!("REGRESSION TEST: Hybrid mode preserves boolean syntax (no Query::parse fallback)");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        fn helper() {}
        fn test_helper() {}
        // TODO: implement features
        ",
    )]);

    // This query should use semantic search with boolean syntax
    // Previously failed with Query::parse() which doesn't support AND
    let output = sqry_cmd()
        .arg("query")
        .arg("kind:function AND name:helper")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "Hybrid mode boolean query failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).unwrap();
    let stderr = String::from_utf8(output.stderr).unwrap();

    // Should attempt semantic search (not immediately fallback to text)
    assert!(
        stderr.contains("[Semantic") || stderr.contains("[Hybrid") || stderr.is_empty(),
        "Expected semantic attempt, got: {stderr}"
    );

    // Verify actual results - should find "helper" but not "test_helper"
    assert!(
        stdout.contains("helper"),
        "Expected to find helper function, got: {stdout}"
    );
    // Note: "test_helper" might also match if query is too broad, but we verify helper exists
    log::info!("✓ PASS: Hybrid mode correctly handles boolean query syntax with correct results");
}

#[test]
fn test_semantic_flag_boolean_query() {
    init_logging();
    log::info!("REGRESSION TEST: --semantic flag with boolean query");

    let project = create_indexed_project(&[(
        "test.rs",
        r"
        pub fn public_func() {}
        fn private_func() {}
        ",
    )]);

    // Force semantic mode with boolean query
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND name~=/public/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "Semantic mode boolean query failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    log::info!("✓ PASS: --semantic flag works with boolean syntax");
}

#[test]
fn test_relation_callers_supports_fqn_and_simple_names() {
    init_logging();
    log::info!("REGRESSION TEST: Relation callers predicate supports FQN + simple names");

    let project = copy_fixture_dir("tests/fixtures/java/relation_tracking/calls");
    let project_path = project.path();

    // Build index to populate relation store (force rebuild to override any copied fixtures)
    sqry_cmd()
        .arg("index")
        .arg("--force")
        .arg(project_path)
        .assert()
        .success();

    // Note: Direct legacy index verification removed (index deleted).
    // This test now relies solely on CLI query output assertions.

    let fqn_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("callers:com.example.service.UserService.findById")
        .arg(project_path)
        .output()
        .expect("run callers query with FQN");
    assert!(
        fqn_output.status.success(),
        "FQN callers query failed: {}",
        String::from_utf8_lossy(&fqn_output.stderr)
    );
    let fqn_json: Value = serde_json::from_slice(&fqn_output.stdout).expect("valid JSON output");
    let fqn_names = extract_symbol_names(&fqn_json);
    let fqn_set: BTreeSet<_> = fqn_names.iter().cloned().collect();
    assert!(
        !fqn_set.is_empty(),
        "FQN callers query should return at least one caller"
    );

    let simple_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("callers:findById")
        .arg(project_path)
        .output()
        .expect("run callers query with simple name");
    assert!(
        simple_output.status.success(),
        "Simple callers query failed: {}",
        String::from_utf8_lossy(&simple_output.stderr)
    );
    let simple_json: Value =
        serde_json::from_slice(&simple_output.stdout).expect("valid JSON output");
    let simple_names = extract_symbol_names(&simple_json);
    let simple_set: BTreeSet<_> = simple_names.iter().cloned().collect();

    // NOTE: Simple name queries may return MORE results than FQN queries
    // because they match ALL symbols with that name, not just one specific symbol.
    // In this case, `callers:findById` matches callers of BOTH:
    // - UserService.findById (the FQN target)
    // - UserRepository.findById (another method with same simple name)
    //
    // This is correct behavior. FQN results should be a SUBSET of simple name results.
    assert!(
        fqn_set.is_subset(&simple_set),
        "FQN results should be subset of simple name results. FQN: {fqn_set:?}, Simple: {simple_set:?}"
    );
    assert!(
        fqn_set.iter().any(|name| name.contains("getUser")),
        "Expected caller list to include controller method, got: {fqn_set:?}"
    );
    assert!(
        simple_set.iter().any(|name| name.contains("getUser")),
        "Expected simple name caller list to include controller method, got: {simple_set:?}"
    );
}

#[test]
fn test_relation_returns_supports_fqn_and_simple_names() {
    init_logging();
    log::info!("REGRESSION TEST: Relation returns predicate supports FQN + simple names");

    let project = copy_fixture_dir("tests/fixtures/java/relation_tracking/return_types");
    let project_path = project.path();

    // Build index to populate return type metadata
    sqry_cmd().arg("index").arg(project_path).assert().success();

    // Test FQN query: returns:Optional<User>
    let fqn_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("returns:Optional<User>")
        .arg(project_path)
        .output()
        .expect("run returns query with FQN");
    assert!(
        fqn_output.status.success(),
        "FQN returns query failed: {}",
        String::from_utf8_lossy(&fqn_output.stderr)
    );
    let fqn_json: Value = serde_json::from_slice(&fqn_output.stdout).expect("valid JSON output");
    let fqn_results = fqn_json["results"]
        .as_array()
        .expect("results should be array");
    let fqn_names = extract_symbol_names(&fqn_json);
    let fqn_set: BTreeSet<_> = fqn_names.iter().cloned().collect();
    assert!(
        !fqn_set.is_empty(),
        "FQN returns query should return at least one method"
    );

    // Test simple query: returns:Optional
    let simple_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("returns:Optional")
        .arg(project_path)
        .output()
        .expect("run returns query with simple name");
    assert!(
        simple_output.status.success(),
        "Simple returns query failed: {}",
        String::from_utf8_lossy(&simple_output.stderr)
    );
    let simple_json: Value =
        serde_json::from_slice(&simple_output.stdout).expect("valid JSON output");
    let simple_results = simple_json["results"]
        .as_array()
        .expect("results should be array");
    let simple_names = extract_symbol_names(&simple_json);
    let simple_set: BTreeSet<_> = simple_names.iter().cloned().collect();

    // Verify FQN results: 4 methods returning Optional<User>
    assert_eq!(
        fqn_set.len(),
        4,
        "FQN query should match exactly 4 methods returning Optional<User>, got: {}",
        fqn_set.len()
    );
    assert_eq!(
        fqn_names.len(),
        fqn_set.len(),
        "FQN query should not emit duplicate semantic results, got: {fqn_names:?}"
    );

    // Verify simple query returns more matches (all Optional<*> variants)
    assert_eq!(
        simple_set.len(),
        9,
        "Simple query should match exactly 9 methods returning Optional<*>, got: {}",
        simple_set.len()
    );
    assert_eq!(
        simple_names.len(),
        simple_set.len(),
        "Simple query should not emit duplicate semantic results, got: {simple_names:?}"
    );

    // HIGH PRIORITY (Codex): Verify FQN results are a subset of simple results
    assert!(
        fqn_set.is_subset(&simple_set),
        "FQN results must be a subset of simple name results. Missing: {:?}",
        fqn_set.difference(&simple_set).collect::<Vec<_>>()
    );

    // MEDIUM PRIORITY (Codex): Verify specific fixture methods exist in FQN results
    // Note: Results use qualified names, so check for the full path
    assert!(
        fqn_set.iter().any(|name| name.ends_with("findUser")),
        "Expected findUser in returns:Optional<User> results, got: {fqn_set:?}"
    );
    assert!(
        fqn_set.iter().any(|name| name.ends_with("maybeFind")),
        "Expected maybeFind in returns:Optional<User> results, got: {fqn_set:?}"
    );
    assert!(
        fqn_set
            .iter()
            .any(|name| name.ends_with("annotatedOptional")),
        "Expected annotatedOptional in returns:Optional<User> results, got: {fqn_set:?}"
    );

    // Verify simple query includes additional methods
    assert!(
        simple_set.iter().any(|name| name.ends_with("findUser"))
            && simple_set.iter().any(|name| name.ends_with("maybeFind")),
        "Expected findUser and maybeFind in simple query results, got: {simple_set:?}"
    );
    assert!(
        simple_set
            .iter()
            .any(|name| name.ends_with("nestedOptionals"))
            || simple_set
                .iter()
                .any(|name| name.ends_with("wildcardOptional")),
        "Expected broader Optional<*> matches in simple query, got: {simple_set:?}"
    );

    let find_user_result = fqn_results
        .iter()
        .find(|result| result["name"].as_str() == Some("findUser"))
        .expect("findUser should be present");
    let qualified_name = find_user_result["qualified_name"]
        .as_str()
        .expect("qualified_name should be string");
    assert_eq!(
        qualified_name, "com.example.relations.ReturnTypes.findUser",
        "Expected Java-native qualified_name display"
    );
    assert!(
        !qualified_name.contains("::"),
        "JSON qualified_name should not leak canonical separators: {qualified_name}"
    );
    assert_eq!(
        simple_results.len(),
        simple_set.len(),
        "Simple query should not contain duplicate rows, got: {simple_results:?}"
    );

    let name_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("name:com.example.relations.ReturnTypes.Nested.nestedOptional")
        .arg(project_path)
        .output()
        .expect("run native-qualified name query");
    assert!(
        name_output.status.success(),
        "Native-qualified name query failed: {}",
        String::from_utf8_lossy(&name_output.stderr)
    );
    let name_json: Value = serde_json::from_slice(&name_output.stdout).expect("valid JSON output");
    let name_results = name_json["results"]
        .as_array()
        .expect("results should be array");
    assert_eq!(
        name_results.len(),
        1,
        "Expected one nestedOptional match for native-qualified name query, got: {name_results:?}"
    );
}

#[test]
fn test_csharp_phase2_callers_and_returns() {
    init_logging();
    log::info!("REGRESSION TEST: C# relation queries (Phase 2)");

    let project = copy_fixture_dir("sqry-lang-csharp/tests/fixtures/relation_tracking");
    let project_path = project.path();

    sqry_cmd().arg("index").arg(project_path).assert().success();

    let callers_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("callers:LoadAsync")
        .arg(project_path)
        .output()
        .expect("run callers query");
    assert!(
        callers_output.status.success(),
        "callers query failed: {}",
        String::from_utf8_lossy(&callers_output.stderr)
    );
    let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
    let caller_names = extract_symbol_names(&callers_json);
    assert!(
        caller_names.iter().any(|name| name.contains("FetchAsync")),
        "expected Service.FetchAsync caller, got: {caller_names:?}"
    );

    let returns_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("returns:Task<User>")
        .arg(project_path)
        .output()
        .expect("run returns query");
    assert!(
        returns_output.status.success(),
        "returns query failed: {}",
        String::from_utf8_lossy(&returns_output.stderr)
    );
    let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
    let return_names = extract_symbol_names(&returns_json);
    assert!(
        return_names.iter().any(|name| name.contains("FetchAsync")),
        "expected FetchAsync in returns results, got: {return_names:?}"
    );
}

#[test]
fn test_cpp_phase2_callers_and_returns() {
    init_logging();
    log::info!("REGRESSION TEST: C++ relation queries (Phase 2)");

    let project = copy_fixture_dir("sqry-lang-cpp/tests/fixtures/relation_tracking");
    let project_path = project.path();

    // Force rebuild to ensure we use the latest relation extraction logic
    sqry_cmd()
        .arg("index")
        .arg("--force")
        .arg(project_path)
        .assert()
        .success();

    let callers_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("callers:helper")
        .arg(project_path)
        .output()
        .expect("run callers query");
    assert!(callers_output.status.success());
    let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
    let caller_names = extract_symbol_names(&callers_json);
    assert!(
        caller_names.iter().any(|name| name.ends_with("process")),
        "expected Service::process caller, got: {caller_names:?}"
    );

    let returns_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("returns:int")
        .arg(project_path)
        .output()
        .expect("run returns query");
    assert!(returns_output.status.success());
    let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
    let return_names = extract_symbol_names(&returns_json);
    // Verify at least some int-returning functions are found
    // Note: helper may not have return type set due to how C++ function definitions are parsed
    assert!(
        return_names
            .iter()
            .any(|name| name.contains("save") || name.contains("process")),
        "expected int-returning functions in results, got: {return_names:?}"
    );
}

#[test]
fn test_kotlin_phase2_callers_and_returns() {
    init_logging();
    log::info!("REGRESSION TEST: Kotlin relation queries (Phase 2)");

    let project = copy_fixture_dir("sqry-lang-kotlin/tests/fixtures/relation_tracking");
    let project_path = project.path();

    sqry_cmd().arg("index").arg(project_path).assert().success();

    let callers_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("callers:fetch")
        .arg(project_path)
        .output()
        .expect("run callers query");
    assert!(callers_output.status.success());
    let callers_json: Value = serde_json::from_slice(&callers_output.stdout).expect("valid JSON");
    let caller_names = extract_symbol_names(&callers_json);
    assert!(
        caller_names.iter().any(|name| name.contains("process")),
        "expected Service.process caller, got: {caller_names:?}"
    );

    let returns_output = sqry_cmd()
        .arg("--json")
        .arg("query")
        .arg("returns:Deferred<Int>")
        .arg(project_path)
        .output()
        .expect("run returns query");
    assert!(returns_output.status.success());
    let returns_json: Value = serde_json::from_slice(&returns_output.stdout).expect("valid JSON");
    let return_names = extract_symbol_names(&returns_json);
    assert!(
        return_names.iter().any(|name| name.contains("process")),
        "expected Service.process in returns results, got: {return_names:?}"
    );
}

// ============================================================================
// Error Handling Tests
// ============================================================================

#[test]
fn test_boolean_syntax_error_reporting() {
    init_logging();
    log::info!("REGRESSION TEST: Boolean syntax errors are reported clearly");

    let project = create_indexed_project(&[("test.rs", "fn main() {}")]);

    // Invalid boolean query: AND without right operand
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    // Should fail with clear error message
    assert!(
        !output.status.success(),
        "Invalid boolean query should fail"
    );

    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(
        stderr.contains("Error") || stderr.contains("parse") || stderr.contains("syntax"),
        "Expected error message, got: {stderr}"
    );

    log::info!("✓ PASS: Syntax errors reported clearly");
}

// ============================================================================
// Security Tests
// ============================================================================

#[test]
fn test_boolean_regex_security_large_quantifier() {
    init_logging();
    log::info!("SECURITY TEST: Large regex quantifiers handled safely");

    let project = create_indexed_project(&[("test.rs", "fn test() {}")]);

    // Very large quantifier - Rust regex should handle safely (no ReDoS)
    let output = sqry_cmd()
        .arg("--semantic")
        .arg("query")
        .arg("kind:function AND name~=/[a-z]{10000}/")
        .arg("--limit")
        .arg("10")
        .current_dir(project.path())
        .output()
        .unwrap();

    // Should either succeed (compiles safely) or fail with InvalidRegex
    // Rust regex uses DFA, no catastrophic backtracking risk
    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(
        output.status.success() || stderr.contains("InvalidRegex") || stderr.contains("regex"),
        "Large quantifier should succeed or fail gracefully, got: {stderr}"
    );

    log::info!("✓ PASS: Large regex quantifiers handled safely (no DoS risk)");
}
