aprs-cli 1.1.0

Command-line APRS packet inspector
use std::process::Command;

use libaprs_engine::DEFAULT_TRANSPORT_READ_LIMIT;

#[test]
fn cli_reads_json_packets_from_stdin() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("--json")
        .output_with_stdin(b"N0CALL>APRS:>hello\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("\"semantic\":\"status\""));
}

#[test]
fn cli_preserves_non_utf8_packet_bytes_from_stdin() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("--json")
        .output_with_stdin(b"N0CALL>APRS:>\xff\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("\"raw\":\"N0CALL>APRS:>\\u00ff\""));
}

#[test]
fn cli_filters_accepted_packets_by_semantic_kind() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .args(["--filter", "status"])
        .output_with_stdin(b"N0CALL>APRS:>hello\nN0CALL>APRS:!4903.50N/07201.75W-Test\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("semantic=status"));
    assert!(!stdout.contains("semantic=position"));
}

#[test]
fn cli_permissive_mode_accepts_unsupported_semantics() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("--permissive")
        .output_with_stdin(b"N0CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("semantic=unsupported"));
}

#[test]
fn cli_summary_prints_counters_to_stdout() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("--summary")
        .output_with_stdin(b"N0CALL>APRS:>hello\nN0CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(!output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("summary accepted=1 rejected=1 malformed=0"));
}

#[test]
fn cli_explain_prints_stable_rejection_codes() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("--explain")
        .output_with_stdin(b"N0CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(!output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("code=policy.unsupported_semantics"));
}

#[test]
fn cli_fail_on_none_allows_observability_without_failure_exit() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .args(["--fail-on", "none"])
        .output_with_stdin(b"N0CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(output.status.success());
}

#[test]
fn cli_rejects_oversized_file_input() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let path = std::env::temp_dir().join(format!("libaprs-cli-limit-{}", std::process::id()));
    std::fs::write(&path, vec![b'A'; DEFAULT_TRANSPORT_READ_LIMIT + 1]).expect("write");

    let output = Command::new(binary)
        .arg(&path)
        .output()
        .expect("CLI should run");

    assert_eq!(output.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("transport.oversized_input"));
    let _ = std::fs::remove_file(path);
}

#[test]
fn cli_validate_command_reports_validity() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("validate")
        .output_with_stdin(b"N0CALL>APRS:>hello\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("valid accepted=1 rejected=0 malformed=0"));
}

#[test]
fn cli_stats_command_prints_only_summary_to_stdout() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("stats")
        .output_with_stdin(b"N0CALL>APRS:>hello\n")
        .expect("CLI should run");

    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert_eq!(stdout.trim(), "summary accepted=1 rejected=0 malformed=0");
}

#[test]
fn cli_explain_command_prints_stable_codes() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("explain")
        .output_with_stdin(b"N0CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(!output.status.success());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("code=policy.unsupported_semantics"));
}

#[test]
fn cli_replay_command_preserves_accepted_packet_bytes() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("replay")
        .arg("--permissive")
        .output_with_stdin(b"N0CALL>APRS:>\xff\nN1CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(output.status.success());
    assert_eq!(
        output.stdout,
        b"N0CALL>APRS:>\xff\nN1CALL>APRS:~opaque\n".to_vec()
    );
}

#[test]
fn cli_replay_command_does_not_mix_rejections_into_packet_stream() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .arg("replay")
        .output_with_stdin(b"N0CALL>APRS:>ok\nN1CALL>APRS:~opaque\n")
        .expect("CLI should run");

    assert!(!output.status.success());
    assert_eq!(output.stdout, b"N0CALL>APRS:>ok\n".to_vec());
}

#[test]
fn cli_support_matrix_json_is_machine_readable_without_packet_input() {
    let binary = env!("CARGO_BIN_EXE_aprs-cli");
    let output = Command::new(binary)
        .args(["support-matrix", "--json"])
        .output()
        .expect("CLI should run");

    assert!(output.status.success());
    assert!(output.stderr.is_empty());
    let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
    assert!(stdout.contains("\"schema_version\":1"));
    assert!(stdout.contains("\"semantic_families\""));
    assert!(stdout.contains("\"kind\":\"status\""));
    assert!(stdout.contains("\"kind\":\"mic_e\""));
    assert!(stdout.contains("\"transport_adapters\""));
    assert!(stdout.contains("\"crate\":\"aprs-transport-kiss\""));
    assert!(stdout.contains("\"diagnostic_layers\""));
    assert!(stdout.contains("\"code\":\"parse\""));
    assert!(stdout.contains("\"code\":\"policy\""));
    assert!(stdout.contains("\"code\":\"transport\""));
}

trait CommandStdinExt {
    fn output_with_stdin(&mut self, input: &[u8]) -> std::io::Result<std::process::Output>;
}

impl CommandStdinExt for Command {
    fn output_with_stdin(&mut self, input: &[u8]) -> std::io::Result<std::process::Output> {
        use std::io::Write;
        use std::process::Stdio;

        let mut child = self.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?;
        child
            .stdin
            .as_mut()
            .expect("stdin should be piped")
            .write_all(input)?;
        child.wait_with_output()
    }
}