govctl 0.10.1

Project governance CLI for RFC, ADR, and Work Item management
use super::canonical_json::canonicalize_json;
use super::*;
use crate::model::{
    ChangelogEntry, ClauseEntry, ClauseKind, ClauseSpec, ClauseStatus, RfcIndex, RfcPhase, RfcSpec,
    RfcStatus, SectionSpec,
};
use serde_json::Value;
use std::path::PathBuf;

#[test]
fn test_canonicalize_sorts_keys() -> Result<(), Box<dyn std::error::Error>> {
    let json: Value = serde_json::from_str(r#"{"z": 1, "a": 2, "m": 3}"#)?;
    let canonical = canonicalize_json(&json);
    assert_eq!(canonical, r#"{"a":2,"m":3,"z":1}"#);
    Ok(())
}

#[test]
fn test_canonicalize_nested_objects() -> Result<(), Box<dyn std::error::Error>> {
    let json: Value = serde_json::from_str(r#"{"outer": {"z": 1, "a": 2}, "inner": {"b": 3}}"#)?;
    let canonical = canonicalize_json(&json);
    assert_eq!(canonical, r#"{"inner":{"b":3},"outer":{"a":2,"z":1}}"#);
    Ok(())
}

#[test]
fn test_extract_signature() {
    let md = r#"---
status: normative
---

<!-- GENERATED: do not edit. Source: RFC-0000 -->
<!-- SIGNATURE: sha256:abcd1234 -->

# RFC-0000
"#;
    assert_eq!(extract_signature(md), Some("abcd1234".to_string()));
}

#[test]
fn test_extract_signature_not_found() {
    let md = "# Just a plain markdown file";
    assert_eq!(extract_signature(md), None);
}

#[test]
fn test_rfc_content_signature_ignores_bump_bookkeeping() -> Result<(), Diagnostic> {
    let mut rfc = test_rfc_index();
    let baseline = compute_rfc_content_signature(&rfc)?;

    rfc.rfc.version = "0.1.1".to_string();
    rfc.rfc.signature = Some("legacy-or-current-signature".to_string());
    rfc.rfc.changelog.push(ChangelogEntry {
        version: "0.1.1".to_string(),
        date: "2026-06-15".to_string(),
        notes: Some("Bookkeeping only".to_string()),
        added: vec![],
        changed: vec![],
        deprecated: vec![],
        removed: vec![],
        fixed: vec!["no content change".to_string()],
        security: vec![],
    });

    assert_eq!(compute_rfc_content_signature(&rfc)?, baseline);
    assert_ne!(compute_rfc_signature(&rfc)?, baseline);
    Ok(())
}

#[test]
fn test_rfc_content_signature_includes_clause_content() -> Result<(), Diagnostic> {
    let mut rfc = test_rfc_index();
    let baseline = compute_rfc_content_signature(&rfc)?;

    rfc.clauses[0].spec.text = "Updated normative behavior.".to_string();

    assert_ne!(compute_rfc_content_signature(&rfc)?, baseline);
    Ok(())
}

#[test]
fn test_rfc_amended_accepts_legacy_full_signature_baseline() -> Result<(), Diagnostic> {
    let mut rfc = test_rfc_index();
    rfc.rfc.signature = Some(compute_rfc_signature(&rfc)?);

    assert!(!is_rfc_amended(&rfc));
    Ok(())
}

fn test_rfc_index() -> RfcIndex {
    RfcIndex {
        rfc: RfcSpec {
            rfc_id: "RFC-0001".to_string(),
            title: "Test RFC".to_string(),
            version: "0.1.0".to_string(),
            status: RfcStatus::Normative,
            phase: RfcPhase::Impl,
            owners: vec!["@test-user".to_string()],
            created: "2026-06-15".to_string(),
            updated: None,
            supersedes: None,
            refs: vec![],
            tags: vec![],
            sections: vec![SectionSpec {
                title: "Specification".to_string(),
                clauses: vec!["C-TEST".to_string()],
            }],
            changelog: vec![],
            signature: None,
        },
        clauses: vec![ClauseEntry {
            spec: ClauseSpec {
                clause_id: "C-TEST".to_string(),
                title: "Test Clause".to_string(),
                kind: ClauseKind::Normative,
                status: ClauseStatus::Active,
                text: "Original normative behavior.".to_string(),
                anchors: vec![],
                superseded_by: None,
                since: Some("0.1.0".to_string()),
                tags: vec![],
            },
            path: PathBuf::from("gov/rfc/RFC-0001/clauses/C-TEST.toml"),
        }],
        path: PathBuf::from("gov/rfc/RFC-0001/rfc.toml"),
    }
}