govctl 0.9.4

Project governance CLI for RFC, ADR, and Work Item management
use super::*;

#[test]
fn resolves_case_insensitive_substring_match() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["first item", "Second Item", "third"];
    let opts = MatchOptions {
        pattern: Some("second"),
        ..Default::default()
    };

    let indices = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::TickSingle,
    )?;

    assert_eq!(indices, vec![1]);
    Ok(())
}

#[test]
fn strips_known_category_prefixes_before_matching() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["some text", "other text"];
    let opts = MatchOptions {
        pattern: Some("fixed: some text"),
        ..Default::default()
    };

    let indices = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::TickSingle,
    )?;

    assert_eq!(indices, vec![0]);
    Ok(())
}

#[test]
fn does_not_strip_non_rendered_category_aliases() {
    let items = ["some text"];
    let opts = MatchOptions {
        pattern: Some("fix: some text"),
        ..Default::default()
    };

    let result = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::Remove,
    );

    assert!(
        result.is_err(),
        "fix: is a parser alias, not a rendered prefix"
    );
}

#[test]
fn allows_remove_all_for_multiple_matches() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["apple", "apricot", "banana"];
    let opts = MatchOptions {
        pattern: Some("ap"),
        all: true,
        ..Default::default()
    };

    let indices = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::Remove,
    )?;

    assert_eq!(indices, vec![0, 1]);
    Ok(())
}

#[test]
fn resolves_negative_indices_from_end() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["first", "second", "third"];
    let opts = MatchOptions {
        at: Some(-1),
        ..Default::default()
    };

    let indices = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::TickSingle,
    )?;

    assert_eq!(indices, vec![2]);
    Ok(())
}

#[test]
fn rejects_ambiguous_tick_matches_without_all() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["test-one", "test-two", "other"];
    let opts = MatchOptions {
        pattern: Some("test"),
        ..Default::default()
    };

    let Err(err) = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::TickSingle,
    ) else {
        return Err("ambiguous tick match should fail".into());
    };

    assert!(err.to_string().contains("2 items match"));
    Ok(())
}

#[test]
fn rejects_invalid_regex_patterns() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["a", "b"];
    let opts = MatchOptions {
        pattern: Some("[invalid"),
        regex: true,
        ..Default::default()
    };

    let Err(err) = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::TickSingle,
    ) else {
        return Err("invalid regex should fail".into());
    };

    assert!(err.to_string().contains("Invalid regex"));
    Ok(())
}

#[test]
fn rejects_no_match_patterns() -> Result<(), Box<dyn std::error::Error>> {
    let items = ["apple", "banana", "cherry"];
    let opts = MatchOptions {
        pattern: Some("xyz"),
        ..Default::default()
    };

    let Err(err) = resolve_match_indices(
        "WI-1",
        "acceptance_criteria",
        &items,
        &opts,
        MatchUse::Remove,
    ) else {
        return Err("missing match should fail".into());
    };

    assert!(err.to_string().contains("No items match"));
    Ok(())
}