twitcher 0.2.2

Find template switch mutations in genomic data
use bstr::ByteSlice;

use crate::common::twitcher_cmd;

mod common;

const BAM: &str = "tests/data/reads/test.bam";
const REF: &str = "tests/data/reads/test.fa.gz";

fn csv_data_rows(output: &std::process::Output) -> usize {
    // Header is always the first line; all subsequent lines are data rows.
    output.stdout.as_bstr().lines().count().saturating_sub(1)
}

#[test]
fn test_reads_produces_ts_rows() {
    // Default run: output only clusters with detected template switches.
    let cmd = twitcher_cmd(&["reads", BAM, "--reference", REF]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    let rows = csv_data_rows(&cmd);
    assert!(rows > 0, "expected at least one TS row in CSV output");
    // Every data line must be parseable CSV (has the id field, a non-empty uuid-like string).
    for line in cmd.stdout.as_bstr().lines().skip(1) {
        assert!(
            !line.is_empty(),
            "unexpected empty data line in CSV output"
        );
    }
}

#[test]
fn test_reads_all_clusters_includes_non_ts() {
    // --all-clusters emits every cluster regardless of TS detection.
    // The fixture BAM has 8 TS rows (default) and 34 rows total (all clusters).
    let cmd = twitcher_cmd(&["reads", BAM, "--reference", REF, "--all-clusters"]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    let rows = csv_data_rows(&cmd);
    assert!(
        rows > 8,
        "--all-clusters should produce more rows than the TS-only default (8); got {rows}"
    );
}

#[test]
fn test_reads_n_flag_limits_processed_reads() {
    // -n 5 restricts processing to the first 5 reads.
    // The fixture BAM has 8 TS rows without -n; -n 5 must produce fewer.
    let cmd = twitcher_cmd(&["reads", BAM, "--reference", REF, "-n", "5"]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    let rows = csv_data_rows(&cmd);
    assert!(rows < 8, "-n 5 should produce fewer than 8 TS rows; got {rows}");
}

#[test]
fn test_reads_no_ts_suppresses_output() {
    // --no-ts disables TS detection; default output (TS-only) must be empty.
    let cmd = twitcher_cmd(&["reads", BAM, "--reference", REF, "--no-ts"]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    let rows = csv_data_rows(&cmd);
    assert_eq!(rows, 0, "expected no TS rows when --no-ts is set");
}

#[test]
fn test_reads_fpa_detects_template_switches() {
    // --fpa uses the Four-Point-Aligner; must still produce TS rows.
    let cmd = twitcher_cmd(&["reads", BAM, "--reference", REF, "--fpa"]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    let rows = csv_data_rows(&cmd);
    assert!(rows > 0, "expected TS rows with --fpa; got {rows}");
}

#[test]
fn test_reads_output_file_flag() {
    // -o writes CSV to a file instead of stdout.
    let dir = tempfile::tempdir().unwrap();
    let csv_path = dir.path().join("out.csv");
    let cmd = twitcher_cmd(&[
        "reads",
        BAM,
        "--reference",
        REF,
        "-o",
        csv_path.to_str().unwrap(),
    ]);
    let stderr = cmd.stderr.as_bstr();
    eprintln!("{stderr}");
    assert!(cmd.status.success());
    assert!(csv_path.exists(), "output CSV file was not created");
    let content = std::fs::read_to_string(&csv_path).unwrap();
    let mut lines = content.lines();
    let header = lines.next().expect("CSV must have a header row");
    assert!(!header.is_empty());
    assert!(lines.next().is_some(), "expected at least one data row");
}