provenance-cli 0.2.1

Command-line interface for Provenance requirements traceability
use super::fixtures::*;
use crate::wiki::model::GapKind;

#[test]
fn resolution_page_links_requirements_rules_and_spawned_work() {
    let corpus = fixture_corpus();
    let page = resolution_page(&corpus, "res_split");
    assert_eq!(page.resolves.len(), 1);
    assert_eq!(page.resolves[0].target.record_id, "req_child");
    assert_eq!(page.spawned.len(), 1);
    assert_eq!(page.spawned[0].target.record_id, "req_stuck");
    assert_eq!(page.produced_rules.len(), 1);
    assert_eq!(page.produced_rules[0].link.target.record_id, "rule_001");
    assert!(page.gaps.is_empty());
    assert_eq!(page.threads.len(), 1);
    assert_eq!(page.threads[0].thread_id, "thr_res_split");
}

#[test]
fn resolution_page_flags_orphaned_decisions() {
    let corpus = fixture_corpus();
    let page = resolution_page(&corpus, "res_orphan");
    assert_eq!(gap_kinds(&page.gaps), vec![GapKind::OrphanResolution]);
}

#[test]
fn rule_page_traces_back_to_requirements_and_sources() {
    let corpus = fixture_corpus();
    let page = rule_page(&corpus, "rule_001");
    assert_eq!(page.title, "Invoices grouped by participant");
    let produced_by: Vec<&str> = page
        .produced_by
        .iter()
        .map(|link| link.target.record_id.as_str())
        .collect();
    assert_eq!(produced_by, vec!["res_split", "req_child"]);
    assert_eq!(page.requirements.len(), 1);
    assert_eq!(page.requirements[0].target.record_id, "req_child");
    assert_eq!(page.sources.len(), 1);
    assert_eq!(page.sources[0].target.record_id, "source_schads");
    assert!(page.implementations.is_empty());
    assert!(page.verifications.is_empty());
    assert!(page.gaps.is_empty());
}

#[test]
fn rule_page_flags_orphan_rules_and_titles_a_nameless_rule_by_its_statement() {
    let corpus = fixture_corpus();
    let page = rule_page(&corpus, "rule_orphan");
    assert_eq!(page.title, "Claim items shall be grouped by participant");
    assert_eq!(gap_kinds(&page.gaps), vec![GapKind::OrphanRule]);
    assert!(page.produced_by.is_empty());
}

#[test]
fn requirement_titles_stop_at_the_first_clause_while_the_statement_stays_complete() {
    let mut state = empty_state();
    let statement = "Claims shall be grouped by participant before invoicing; later exports may preserve the original ordering for audit readers";
    state.requirements = vec![requirement(
        "req_long_title",
        statement,
        provenance_core::RequirementStatus::Active,
        vec![],
    )];

    let corpus = super::super::build_corpus(&state, &crate::wiki::links::LinkResolver::new(None));
    let page = &corpus.requirements[0];

    assert_eq!(
        page.title,
        "Claims shall be grouped by participant before invoicing"
    );
    assert_eq!(page.statement, statement);
    assert!(page.title.chars().count() <= 70);
}

#[test]
fn requirement_titles_handle_dotted_words_and_truncate_at_a_word_boundary() {
    let mut state = empty_state();
    state.requirements = vec![
        requirement(
            "req_dotnet",
            ".NET 8.0 services shall preserve participant identifiers across every invoice export",
            provenance_core::RequirementStatus::Active,
            vec![],
        ),
        requirement(
            "req_long_words",
            "Participant invoice exports shall preserve all original settlement references while remaining readable to audit reviewers",
            provenance_core::RequirementStatus::Active,
            vec![],
        ),
    ];

    let corpus = super::super::build_corpus(&state, &crate::wiki::links::LinkResolver::new(None));

    assert!(corpus.requirements[0]
        .title
        .starts_with(".NET 8.0 services"));
    assert!(corpus.requirements[1].title.ends_with('…'));
    assert!(corpus.requirements[1].title.chars().count() <= 70);
    assert!(!corpus.requirements[1].title.ends_with(" …"));

    let mut unbroken_state = empty_state();
    unbroken_state.requirements = vec![requirement(
        "req_unbroken",
        &"a".repeat(100),
        provenance_core::RequirementStatus::Active,
        vec![],
    )];
    let unbroken = super::super::build_corpus(
        &unbroken_state,
        &crate::wiki::links::LinkResolver::new(None),
    );
    assert!(unbroken.requirements[0].title.chars().count() <= 70);
    assert!(unbroken.requirements[0].title.ends_with('…'));
}

#[test]
fn source_page_lists_referencing_requirements_and_pins_links() {
    let corpus = fixture_corpus();
    let page = source_page(&corpus, "source_schads");
    assert_eq!(page.referenced_requirements.len(), 1);
    assert_eq!(
        page.referenced_requirements[0].target.record_id,
        "req_child"
    );
    assert_eq!(
        page.reference.as_ref().unwrap().href.as_deref(),
        Some("https://github.com/exampleorg/ex-api/blob/abc1234/docs/award.md")
    );
    assert!(page.gaps.is_empty());
}

#[test]
fn source_page_flags_unreferenced_sources() {
    let corpus = fixture_corpus();
    let page = source_page(&corpus, "source_unused");
    assert_eq!(gap_kinds(&page.gaps), vec![GapKind::UnreferencedSource]);
    assert!(page.referenced_requirements.is_empty());
}