govctl 0.9.0

Project governance CLI for RFC, ADR, and Work Item management
use super::super::ArtifactType;
use super::super::path::PathSegment;
use super::*;
use crate::diagnostic::DiagnosticCode;

#[test]
fn test_plan_simple_path() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("ADR-0001", Some("title"))?;
    assert_eq!(plan.artifact, ArtifactType::Adr);
    assert_eq!(
        plan.field_path.as_ref().and_then(FieldPath::as_simple),
        Some("title")
    );
    assert_eq!(plan.verb, None);
    assert_eq!(
        plan.target,
        Some(ResolvedTarget::Node {
            origin: TargetOrigin::Simple,
            path: FieldPath {
                segments: vec![PathSegment {
                    name: "title".to_string(),
                    index: None,
                }],
            },
            kind: TargetKind::Scalar,
            status_list: false,
        })
    );
    Ok(())
}

#[test]
fn test_plan_nested_path() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("ADR-0001", Some("alt[0].pro[1]"))?;
    let fp = plan
        .field_path
        .as_ref()
        .ok_or("nested field should exist")?;
    assert_eq!(fp.segments[0].name, "alternatives");
    assert_eq!(fp.segments[1].name, "pros");
    assert_eq!(plan.verb, None);
    Ok(())
}

#[test]
fn test_plan_without_field() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("ADR-0001", None)?;
    assert_eq!(plan.artifact, ArtifactType::Adr);
    assert!(plan.field_path.is_none());
    assert_eq!(plan.verb, None);
    assert_eq!(plan.target, None);
    Ok(())
}

#[test]
fn test_plan_unknown_artifact_fails() -> Result<(), Box<dyn std::error::Error>> {
    let diag = match plan_request("UNKNOWN", Some("title")) {
        Ok(plan) => return Err(format!("unknown artifact should fail, got {plan:?}").into()),
        Err(diag) => diag,
    };
    assert_eq!(diag.code, DiagnosticCode::E0819UnknownArtifactType);
    assert!(diag.message.contains("Unknown artifact type"));
    Ok(())
}

#[test]
fn test_scope_aware_alias_only_applies_when_valid_for_artifact()
-> Result<(), Box<dyn std::error::Error>> {
    let diag = match plan_request("ADR-0001", Some("desc")) {
        Ok(plan) => return Err(format!("unknown ADR field should fail, got {plan:?}").into()),
        Err(diag) => diag,
    };
    assert_eq!(diag.code, DiagnosticCode::E0803UnknownField);
    assert!(diag.message.contains("Unknown ADR field"));
    Ok(())
}

#[test]
fn test_scope_aware_alias_keeps_work_short_name() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("WI-2026-01-01-001", Some("desc"))?;
    let fp = plan.field_path.ok_or("field path should exist")?;
    assert_eq!(fp.as_simple(), Some("description"));
    Ok(())
}

#[test]
fn test_scope_aware_alias_under_legacy_prefix() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("WI-2026-01-01-001", Some("content.desc"))?;
    let fp = plan.field_path.ok_or("field path should exist")?;
    assert_eq!(fp.as_simple(), Some("description"));
    Ok(())
}

#[test]
fn test_unknown_alias_in_scope_is_not_rewritten() -> Result<(), Box<dyn std::error::Error>> {
    let diag = match plan_request("WI-2026-01-01-001", Some("alt[0].pro[0]")) {
        Ok(plan) => {
            return Err(format!("unknown work item field should fail, got {plan:?}").into());
        }
        Err(diag) => diag,
    };
    assert_eq!(diag.code, DiagnosticCode::E0803UnknownField);
    assert!(diag.message.contains("Unknown work item field"));
    Ok(())
}

#[test]
fn test_plan_mutation_request_records_verb() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_mutation_request("ADR-0001", "content.decision", Verb::Set)?;
    assert_eq!(plan.verb, Some(Verb::Set));
    assert_eq!(
        plan.field_path
            .and_then(|fp| fp.as_simple().map(str::to_owned)),
        Some("decision".to_string())
    );
    assert_eq!(
        plan.target,
        Some(ResolvedTarget::Node {
            origin: TargetOrigin::Simple,
            path: FieldPath {
                segments: vec![PathSegment {
                    name: "decision".to_string(),
                    index: None,
                }],
            },
            kind: TargetKind::Scalar,
            status_list: false,
        })
    );
    Ok(())
}

#[test]
fn test_plan_mutation_request_classifies_nested_root_item_target()
-> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_mutation_request("ADR-0001", "alternatives[0]", Verb::Remove)?;
    assert_eq!(
        plan.target,
        Some(ResolvedTarget::IndexedItem {
            origin: TargetOrigin::Nested,
            path: FieldPath {
                segments: vec![PathSegment {
                    name: "alternatives".to_string(),
                    index: Some(0),
                }],
            },
            container_path: FieldPath {
                segments: vec![PathSegment {
                    name: "alternatives".to_string(),
                    index: None,
                }],
            },
            index: 0,
            item_kind: TargetKind::Object,
            status_list: true,
        })
    );
    Ok(())
}

#[test]
fn test_plan_mutation_request_classifies_nested_list_item_target()
-> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_mutation_request("ADR-0001", "alternatives[0].pros[1]", Verb::Remove)?;
    assert_eq!(
        plan.target,
        Some(ResolvedTarget::IndexedItem {
            origin: TargetOrigin::Nested,
            path: FieldPath {
                segments: vec![
                    PathSegment {
                        name: "alternatives".to_string(),
                        index: Some(0),
                    },
                    PathSegment {
                        name: "pros".to_string(),
                        index: Some(1),
                    },
                ],
            },
            container_path: FieldPath {
                segments: vec![
                    PathSegment {
                        name: "alternatives".to_string(),
                        index: Some(0),
                    },
                    PathSegment {
                        name: "pros".to_string(),
                        index: None,
                    },
                ],
            },
            index: 1,
            item_kind: TargetKind::Scalar,
            status_list: false,
        })
    );
    Ok(())
}