//! Test that const, unsafe, and extern fn are skipped during instrumentation.
//! The instrumented project should build and run successfully, with only
//! normal functions appearing in the output.

mod common;

use std::fs;
use std::path::Path;
use std::process::Command;

fn create_project_with_special_fns(dir: &Path) {
    fs::create_dir_all(dir.join("src")).unwrap();

    fs::write(
        dir.join("Cargo.toml"),
        r#"[package]
name = "special"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "special"
path = "src/main.rs"
"#,
    )
    .unwrap();

    fs::write(
        dir.join("src").join("main.rs"),
        r#"const fn fixed_size() -> usize { 42 }

unsafe fn dangerous() -> i32 { 99 }

extern "C" fn ffi_callback() -> i32 { 7 }

fn normal_work() -> u64 {
    let mut sum = 0u64;
    for i in 0..1000 {
        sum += i;
    }
    sum
}

fn main() {
    let a = fixed_size();
    let b = unsafe { dangerous() };
    let c = ffi_callback();
    let d = normal_work();
    println!("results: {a} {b} {c} {d}");
}
"#,
    )
    .unwrap();
}

#[test]
fn special_fns_are_skipped_during_instrumentation() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("special");
    create_project_with_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    // Build with no target filter (instrument everything).
    let output = Command::new(piano_bin)
        .args(["build", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build");

    let stderr = String::from_utf8_lossy(&output.stderr);
    let stdout = String::from_utf8_lossy(&output.stdout);

    assert!(
        output.status.success(),
        "piano build failed:\nstderr: {stderr}\nstdout: {stdout}"
    );

    // Run the instrumented binary.
    let runs_dir = tmp.path().join("runs");
    fs::create_dir_all(&runs_dir).unwrap();

    let binary_path = stdout.trim();
    let run_output = Command::new(binary_path)
        .env("PIANO_RUNS_DIR", &runs_dir)
        .output()
        .expect("failed to run instrumented binary");

    assert!(
        run_output.status.success(),
        "instrumented binary failed:\n{}",
        String::from_utf8_lossy(&run_output.stderr)
    );

    // Program should produce correct output.
    let program_stdout = String::from_utf8_lossy(&run_output.stdout);
    assert!(
        program_stdout.contains("results: 42 99 7 499500"),
        "program should produce correct output, got: {program_stdout}"
    );

    // Verify run file contains normal_work but NOT const/unsafe/extern fns.
    let run_file = common::largest_ndjson_file(&runs_dir);
    let content = fs::read_to_string(&run_file).unwrap();
    assert!(
        content.contains("\"normal_work\""),
        "output should contain normal_work"
    );
    assert!(
        !content.contains("\"fixed_size\""),
        "output should NOT contain const fn fixed_size"
    );
    assert!(
        !content.contains("\"dangerous\""),
        "output should NOT contain unsafe fn dangerous"
    );
    assert!(
        !content.contains("\"ffi_callback\""),
        "output should NOT contain extern fn ffi_callback"
    );
}

#[test]
fn list_skipped_shows_excluded_functions() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("special");
    create_project_with_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    let output = Command::new(piano_bin)
        .args(["build", "--list-skipped", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build --list-skipped");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "piano build --list-skipped failed:\nstderr: {stderr}\nstdout: {stdout}"
    );

    // Should list all skipped functions with file paths and reasons.
    assert!(
        stdout.contains("src/main.rs: dangerous (unsafe)"),
        "should list unsafe fn with path: {stdout}"
    );
    assert!(
        stdout.contains("src/main.rs: fixed_size (const)"),
        "should list const fn with path: {stdout}"
    );
    assert!(
        stdout.contains("src/main.rs: ffi_callback (extern)"),
        "should list extern fn with path: {stdout}"
    );

    // Should NOT list normal functions.
    assert!(
        !stdout.contains("normal_work"),
        "should not list normal fn: {stdout}"
    );
    assert!(!stdout.contains("main ("), "should not list main: {stdout}");
}

#[test]
fn build_warns_about_skipped_functions() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("special");
    create_project_with_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    // Use a broad --fn pattern that matches both normal and skipped functions.
    // The warning should only fire when specs are explicit, not in instrument-everything mode.
    let output = Command::new(piano_bin)
        .args(["build", "--fn", "o", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build");

    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "piano build failed:\nstderr: {stderr}"
    );

    assert!(
        stderr.contains("function(s) skipped"),
        "should warn about skipped functions: {stderr}"
    );
    assert!(
        stderr.contains("--list-skipped"),
        "warning should mention --list-skipped: {stderr}"
    );
}

#[test]
fn build_no_warning_in_instrument_everything_mode() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("special");
    create_project_with_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    // No --fn/--file/--mod: instrument-everything mode should NOT warn about skipped functions.
    let output = Command::new(piano_bin)
        .args(["build", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build");

    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "piano build failed:\nstderr: {stderr}"
    );

    assert!(
        !stderr.contains("function(s) skipped"),
        "should NOT warn about skipped functions in instrument-everything mode: {stderr}"
    );
}

fn create_project_without_special_fns(dir: &Path) {
    fs::create_dir_all(dir.join("src")).unwrap();

    fs::write(
        dir.join("Cargo.toml"),
        r#"[package]
name = "normal"
version = "0.1.0"
edition = "2024"

[[bin]]
name = "normal"
path = "src/main.rs"
"#,
    )
    .unwrap();

    fs::write(
        dir.join("src").join("main.rs"),
        r#"fn work() -> u64 {
    let mut sum = 0u64;
    for i in 0..1000 {
        sum += i;
    }
    sum
}

fn main() {
    let result = work();
    println!("{result}");
}
"#,
    )
    .unwrap();
}

#[test]
fn list_skipped_shows_message_when_none_skipped() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("normal");
    create_project_without_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    let output = Command::new(piano_bin)
        .args(["build", "--list-skipped", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build --list-skipped");

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "piano build --list-skipped should succeed:\nstderr: {stderr}\nstdout: {stdout}"
    );

    assert!(
        stdout.is_empty(),
        "stdout should be empty when no functions skipped: {stdout}"
    );

    assert!(
        stderr.contains("no functions skipped"),
        "stderr should say 'no functions skipped': {stderr}"
    );
}

#[test]
fn no_targets_found_error_prints_once() {
    let tmp = tempfile::tempdir().unwrap();
    let project_dir = tmp.path().join("special");
    create_project_with_special_fns(&project_dir);
    common::prepopulate_deps(&project_dir, common::mini_seed());

    let piano_bin = env!("CARGO_BIN_EXE_piano");
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let runtime_path = manifest_dir.join("piano-runtime");

    // --fn dangerous matches only the unsafe fn, which gets skipped.
    // The error should appear exactly once (no duplicate warning).
    let output = Command::new(piano_bin)
        .args(["build", "--fn", "dangerous", "--project"])
        .arg(&project_dir)
        .arg("--runtime-path")
        .arg(&runtime_path)
        .output()
        .expect("failed to run piano build");

    assert!(
        !output.status.success(),
        "should fail when all matches are skipped"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);

    // The error message should appear exactly once.
    assert!(
        stderr.contains("no functions matched"),
        "should contain the error message: {stderr}"
    );

    // The skipped-functions warning should NOT appear separately -- the error
    // message already includes skip information.
    assert!(
        !stderr.contains("function(s) skipped"),
        "should not print a separate skipped warning when the error already includes skip info: {stderr}"
    );

    // Count occurrences of "error:" to verify single error output.
    let error_count = stderr.matches("error:").count();
    assert_eq!(
        error_count, 1,
        "error should appear exactly once, got {error_count}: {stderr}"
    );
}
