//! Head-to-head: car-reason (with AST) vs neo
//!
//! Runs identical problems through both engines, captures metrics,
//! and prints a comparison table. Tests the full pipeline end-to-end:
//! classify → locate (AST) → diagnose → fix → explain.
//!
//! Run with: cargo test -p car-reason --test head_to_head -- --nocapture --ignored

use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Instant;

struct TestProblem {
    id: &'static str,
    category: &'static str,
    prompt: &'static str,
}

struct EngineResult {
    engine: String,
    problem_id: String,
    latency_ms: u64,
    confidence: f64,
    output_len: usize,
    has_code: bool,
    error: Option<String>,
}

const PROBLEMS: &[TestProblem] = &[
    TestProblem {
        id: "bugfix_1",
        category: "BugFix",
        prompt: "Fix the off-by-one error in this function:\n```rust\nfn parse_items(data: &[u8], count: usize) -> Vec<Item> {\n    let mut items = Vec::new();\n    for i in 0..=count {\n        items.push(Item::from_bytes(&data[i*8..(i+1)*8]));\n    }\n    items\n}\n```\nThe function reads one extra item past the end of the intended range.",
    },
    TestProblem {
        id: "architecture_1",
        category: "Architecture",
        prompt: "What are the trade-offs between storing code symbols as graph nodes (like petgraph with spreading activation) vs a flat inverted index (like tantivy/lucene)? Consider: query latency, relationship traversal, memory usage, incremental updates.",
    },
    TestProblem {
        id: "explain_1",
        category: "Explanation",
        prompt: "Explain why tree-sitter is preferred over regex-based parsing for code analysis tools. What specific advantages does incremental parsing provide?",
    },
    TestProblem {
        id: "perf_1",
        category: "Performance",
        prompt: "Optimize this Python function that finds duplicate files by content hash:\n```python\nimport hashlib\nimport os\n\ndef find_duplicates(directory):\n    hashes = {}\n    duplicates = []\n    for root, dirs, files in os.walk(directory):\n        for filename in files:\n            filepath = os.path.join(root, filename)\n            with open(filepath, 'rb') as f:\n                content = f.read()\n            file_hash = hashlib.sha256(content).hexdigest()\n            if file_hash in hashes:\n                duplicates.append((filepath, hashes[file_hash]))\n            else:\n                hashes[file_hash] = filepath\n    return duplicates\n```\nThis is slow on directories with many large files.",
    },
    TestProblem {
        id: "refactor_1",
        category: "Refactor",
        prompt: "Refactor this Go error handling to reduce repetition:\n```go\nfunc processOrder(ctx context.Context, orderID string) error {\n    order, err := fetchOrder(ctx, orderID)\n    if err != nil {\n        log.Printf(\"failed to fetch order %s: %v\", orderID, err)\n        return fmt.Errorf(\"failed to fetch order: %w\", err)\n    }\n    items, err := fetchItems(ctx, order.ItemIDs)\n    if err != nil {\n        log.Printf(\"failed to fetch items for order %s: %v\", orderID, err)\n        return fmt.Errorf(\"failed to fetch items: %w\", err)\n    }\n    total, err := calculateTotal(items)\n    if err != nil {\n        log.Printf(\"failed to calculate total for order %s: %v\", orderID, err)\n        return fmt.Errorf(\"failed to calculate total: %w\", err)\n    }\n    return chargeCustomer(ctx, order.CustomerID, total)\n}\n```",
    },
];

/// Run a problem through neo via CLI.
fn run_neo(problem: &TestProblem) -> EngineResult {
    let start = Instant::now();

    let output = Command::new("neo")
        .arg("--json")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .and_then(|mut child| {
            use std::io::Write;
            if let Some(ref mut stdin) = child.stdin {
                let _ = stdin.write_all(problem.prompt.as_bytes());
            }
            child.wait_with_output()
        });

    let latency = start.elapsed().as_millis() as u64;

    match output {
        Ok(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout).to_string();
            let stderr = String::from_utf8_lossy(&out.stderr).to_string();
            let combined = format!("{}{}", stdout, stderr);

            // Parse confidence from neo's JSON output
            let confidence = extract_neo_confidence(&combined);
            let has_code = combined.contains("```")
                || combined.contains("fn ")
                || combined.contains("def ")
                || combined.contains("func ");

            EngineResult {
                engine: "neo".into(),
                problem_id: problem.id.into(),
                latency_ms: latency,
                confidence,
                output_len: combined.len(),
                has_code,
                error: if out.status.success() {
                    None
                } else {
                    Some(format!("exit {}", out.status.code().unwrap_or(-1)))
                },
            }
        }
        Err(e) => EngineResult {
            engine: "neo".into(),
            problem_id: problem.id.into(),
            latency_ms: latency,
            confidence: 0.0,
            output_len: 0,
            has_code: false,
            error: Some(e.to_string()),
        },
    }
}

fn extract_neo_confidence(output: &str) -> f64 {
    // Neo outputs confidence in JSON: "confidence": 0.85
    for line in output.lines() {
        if let Some(pos) = line.find("confidence") {
            let rest = &line[pos..];
            // Find the number after the colon
            if let Some(colon) = rest.find(':') {
                let num_str: String = rest[colon + 1..]
                    .chars()
                    .filter(|c| c.is_ascii_digit() || *c == '.')
                    .take(6)
                    .collect();
                if let Ok(conf) = num_str.parse::<f64>() {
                    return conf;
                }
            }
        }
    }
    0.5 // default
}

/// Run a problem through car-reason.
fn run_car_reason(problem: &TestProblem, rt: &tokio::runtime::Runtime) -> EngineResult {
    rt.block_on(async {
        let start = Instant::now();

        // Build the engine stack
        let inference = Arc::new(car_inference::InferenceEngine::new(Default::default()));
        let mut memgine = car_memgine::MemgineEngine::new(None);
        car_reason::skills::seed_defaults(&mut memgine);
        let memgine = Arc::new(Mutex::new(memgine));

        let session = car_reason::ReasoningSession::new(memgine, inference);

        match session.reason(problem.prompt).await {
            Ok(result) => {
                let latency = start.elapsed().as_millis() as u64;
                let output = format!(
                    "{}\n{}\n{}",
                    result.diagnosis,
                    result
                        .suggestions
                        .iter()
                        .map(|s| s.suggested.as_str())
                        .collect::<Vec<_>>()
                        .join("\n"),
                    result.explanation,
                );
                let has_code = output.contains("```")
                    || output.contains("fn ")
                    || output.contains("def ")
                    || output.contains("func ");

                EngineResult {
                    engine: "car-reason+ast".into(),
                    problem_id: problem.id.into(),
                    latency_ms: latency,
                    confidence: result.overall_confidence,
                    output_len: output.len(),
                    has_code,
                    error: None,
                }
            }
            Err(e) => EngineResult {
                engine: "car-reason+ast".into(),
                problem_id: problem.id.into(),
                latency_ms: start.elapsed().as_millis() as u64,
                confidence: 0.0,
                output_len: 0,
                has_code: false,
                error: Some(e.to_string()),
            },
        }
    })
}

#[test]
#[ignore] // Run with --ignored (requires API keys + neo installed)
fn head_to_head_comparison() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let mut results: Vec<EngineResult> = Vec::new();

    println!("\n{}", "=".repeat(60));
    println!("  HEAD-TO-HEAD: car-reason+AST vs neo");
    println!("  {} problems across 5 categories", PROBLEMS.len());
    println!("{}\n", "=".repeat(60));

    for problem in PROBLEMS {
        println!(
            "[{}] {} — {}",
            problem.id,
            problem.category,
            &problem.prompt[..60.min(problem.prompt.len())]
        );

        // Run car-reason first
        print!("  car-reason+ast ... ");
        let car_result = run_car_reason(problem, &rt);
        println!(
            "{}ms conf={:.2} len={} code={} {}",
            car_result.latency_ms,
            car_result.confidence,
            car_result.output_len,
            car_result.has_code,
            car_result.error.as_deref().unwrap_or("OK")
        );
        results.push(car_result);

        // Then neo
        print!("  neo            ... ");
        let neo_result = run_neo(problem);
        println!(
            "{}ms conf={:.2} len={} code={} {}",
            neo_result.latency_ms,
            neo_result.confidence,
            neo_result.output_len,
            neo_result.has_code,
            neo_result.error.as_deref().unwrap_or("OK")
        );
        results.push(neo_result);

        println!();
    }

    // Print comparison table
    println!("\n{:=<80}", "");
    println!(
        "{:<15} {:>8} {:>8} {:>8} {:>8} {:>6} {:>6}",
        "Problem", "CAR ms", "Neo ms", "CAR conf", "Neo conf", "CAR cd", "Neo cd"
    );
    println!("{:-<80}", "");

    let mut car_wins = 0;
    let mut neo_wins = 0;
    let mut car_total_ms = 0u64;
    let mut neo_total_ms = 0u64;
    let mut car_total_conf = 0.0f64;
    let mut neo_total_conf = 0.0f64;

    for problem in PROBLEMS {
        let car = results
            .iter()
            .find(|r| r.engine.contains("car") && r.problem_id == problem.id)
            .unwrap();
        let neo = results
            .iter()
            .find(|r| r.engine == "neo" && r.problem_id == problem.id)
            .unwrap();

        println!(
            "{:<15} {:>7} {:>7}  {:>7.2} {:>7.2}  {:>5} {:>5}",
            problem.id,
            car.latency_ms,
            neo.latency_ms,
            car.confidence,
            neo.confidence,
            if car.has_code { "Y" } else { "N" },
            if neo.has_code { "Y" } else { "N" },
        );

        car_total_ms += car.latency_ms;
        neo_total_ms += neo.latency_ms;
        car_total_conf += car.confidence;
        neo_total_conf += neo.confidence;

        // Score: higher confidence wins; ties go to faster
        if car.confidence > neo.confidence + 0.05 {
            car_wins += 1;
        } else if neo.confidence > car.confidence + 0.05 {
            neo_wins += 1;
        } else if car.latency_ms < neo.latency_ms {
            car_wins += 1;
        } else {
            neo_wins += 1;
        }
    }

    let n = PROBLEMS.len() as f64;
    println!("{:-<80}", "");
    println!(
        "{:<15} {:>7} {:>7}  {:>7.2} {:>7.2}",
        "AVERAGE",
        car_total_ms / PROBLEMS.len() as u64,
        neo_total_ms / PROBLEMS.len() as u64,
        car_total_conf / n,
        neo_total_conf / n,
    );
    println!();
    println!("  car-reason+ast wins: {}", car_wins);
    println!("  neo wins:            {}", neo_wins);
    println!();

    // Assert car-reason wins on at least 3/5
    assert!(
        car_wins >= 3,
        "car-reason+ast should win at least 3 out of {} problems (won {})",
        PROBLEMS.len(),
        car_wins
    );
}

/// Lightweight test (no API keys needed): verify the Locate action
/// (grep + AST extraction) works on real code queries.
/// This is car-reason's core advantage — real codebase search with
/// structural understanding vs neo's flat text.
#[test]
fn locate_action_with_ast() {
    // Run in the car-rs workspace so grep finds real code
    let original_dir = std::env::current_dir().unwrap();
    let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .unwrap() // crates/
        .parent()
        .unwrap(); // car-rs/
    std::env::set_current_dir(workspace).unwrap();

    let test_queries = [
        (
            "AST parsing",
            "parse symbols from source code using tree-sitter",
        ),
        (
            "Graph memory",
            "MemoryGraph spreading activation retrieve nodes",
        ),
        (
            "Adaptive routing",
            "TaskComplexity assess model routing InferenceEngine",
        ),
        ("NAPI bindings", "ThreadsafeFunction executeProposal napi"),
        ("Formal verify", "verify simulate equivalent optimize plan"),
    ];

    println!("\n{}", "=".repeat(70));
    println!("  LOCATE ACTION (grep + AST) — no model calls needed");
    println!("{}\n", "=".repeat(70));

    for (label, query) in &test_queries {
        let start = Instant::now();
        let result = car_reason::executor::execute_locate_only(query);
        let latency = start.elapsed().as_millis() as u64;

        match result {
            Ok(outcome) => {
                // Check for AST markers in output
                let has_ast = outcome.output.contains("AST:");
                let has_symbols = outcome.output.contains("[Function]")
                    || outcome.output.contains("[Struct]")
                    || outcome.output.contains("[Method]")
                    || outcome.output.contains("[Impl]")
                    || outcome.output.contains("[Enum]")
                    || outcome.output.contains("[Trait]");
                let files_found = outcome
                    .output
                    .lines()
                    .filter(|l| l.starts_with("  "))
                    .count();

                println!(
                    "[{:<20}] {}ms  conf={:.2}  files={}  ast={}  symbols={}",
                    label, latency, outcome.confidence, files_found, has_ast, has_symbols
                );

                assert!(outcome.success, "{} should succeed", label);
                assert!(
                    outcome.confidence > 0.5,
                    "{} should have decent confidence",
                    label
                );
                assert!(files_found > 0, "{} should find files", label);
            }
            Err(e) => {
                println!("[{:<20}] ERROR: {}", label, e);
                panic!("{} failed: {}", label, e);
            }
        }
    }

    // Restore working dir
    std::env::set_current_dir(original_dir).unwrap();
}
