govctl 0.19.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,
            verbs: &["get", "set"],
        })
    );
    Ok(())
}

#[test]
fn test_plan_nested_path() -> Result<(), Box<dyn std::error::Error>> {
    let plan = plan_request("ADR-0001", Some("alternatives[0].pros[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_noncanonical_work_short_name_is_rejected() -> Result<(), Box<dyn std::error::Error>> {
    let diag = match plan_request("WI-2026-01-01-001", Some("desc")) {
        Ok(plan) => return Err(format!("noncanonical field should fail, got {plan:?}").into()),
        Err(diag) => diag,
    };
    assert_eq!(diag.code, DiagnosticCode::E0803UnknownField);
    Ok(())
}

#[test]
fn test_legacy_storage_prefix_is_rejected() -> Result<(), Box<dyn std::error::Error>> {
    let diag = match plan_request("WI-2026-01-01-001", Some("content.description")) {
        Ok(plan) => return Err(format!("storage-prefixed field should fail, got {plan:?}").into()),
        Err(diag) => diag,
    };
    assert_eq!(diag.code, DiagnosticCode::E0803UnknownField);
    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", "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,
            verbs: &["get", "set"],
        })
    );
    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,
            object_set_mode: None,
            status_list: true,
            container_verbs: &["get", "add", "remove", "tick"],
            item_verbs: &["get"],
        })
    );
    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,
            object_set_mode: None,
            status_list: false,
            container_verbs: &["get", "add", "remove"],
            item_verbs: &["get", "set"],
        })
    );
    Ok(())
}

#[test]
fn test_unknown_root_reports_registry_fields() -> Result<(), Box<dyn std::error::Error>> {
    let diagnostic = match plan_request("CONF-CASE", Some("requirement")) {
        Err(diagnostic) => diagnostic,
        Ok(plan) => return Err(format!("unknown field should fail, got {plan:?}").into()),
    };

    assert_eq!(diagnostic.code, DiagnosticCode::E0803UnknownField);
    assert!(diagnostic.message.contains("Known fields:"));
    for field in [
        "guards",
        "path",
        "requirements",
        "selector",
        "tags",
        "title",
    ] {
        assert!(diagnostic.message.contains(field), "{diagnostic:?}");
    }
    Ok(())
}

#[test]
fn test_unknown_nested_field_reports_sibling_fields() -> Result<(), Box<dyn std::error::Error>> {
    let diagnostic = match plan_request("WI-2026-01-01-001", Some("acceptance_criteria[0].bogus")) {
        Err(diagnostic) => diagnostic,
        Ok(plan) => return Err(format!("unknown nested field should fail, got {plan:?}").into()),
    };

    assert_eq!(diagnostic.code, DiagnosticCode::E0815PathFieldNotFound);
    assert!(
        diagnostic
            .message
            .contains("under 'acceptance_criteria[0]'")
    );
    assert!(
        diagnostic
            .message
            .contains("Known fields: category, status, text")
    );
    Ok(())
}

#[test]
fn test_unsupported_verb_reports_path_and_supported_operations()
-> Result<(), Box<dyn std::error::Error>> {
    let target = plan_mutation_request("CONF-CASE", "requirements", Verb::Set)?
        .target
        .ok_or("mutation target should exist")?;
    let diagnostic = match target.ensure_supports(Verb::Set, "CONF-CASE") {
        Err(diagnostic) => diagnostic,
        Ok(()) => return Err("requirements should reject set".into()),
    };

    assert_eq!(diagnostic.code, DiagnosticCode::E0817PathTypeMismatch);
    assert!(
        diagnostic
            .message
            .contains("Path 'requirements' does not support --set")
    );
    assert!(
        diagnostic
            .message
            .contains("Supported operations: --add, --remove")
    );
    Ok(())
}

#[test]
fn test_indexed_target_combines_item_and_container_operations()
-> Result<(), Box<dyn std::error::Error>> {
    let target = plan_mutation_request("ADR-0001", "alternatives[0].pros[1]", Verb::Tick)?
        .target
        .ok_or("mutation target should exist")?;
    let diagnostic = match target.ensure_supports(Verb::Tick, "ADR-0001") {
        Err(diagnostic) => diagnostic,
        Ok(()) => return Err("pros item should reject tick".into()),
    };

    assert!(
        diagnostic
            .message
            .contains("Supported operations: --set, --remove")
    );
    Ok(())
}

#[test]
fn test_indexed_set_support_follows_list_item_shape() -> Result<(), Box<dyn std::error::Error>> {
    for (id, path) in [
        ("RFC-0001", "owners[0]"),
        ("RFC-0001", "changelog.fixed[0]"),
        ("ADR-0001", "alternatives[0].pros[0]"),
        ("WI-2026-01-01-001", "notes[0]"),
        ("WI-2026-01-01-001", "acceptance_criteria[0]"),
        ("WI-2026-01-01-001", "verification.required_guards[0]"),
    ] {
        let target = plan_mutation_request(id, path, Verb::Set)?
            .target
            .ok_or("mutation target should exist")?;
        target.ensure_supports(Verb::Set, id)?;
    }

    for (id, path) in [
        ("ADR-0001", "alternatives[0]"),
        ("WI-2026-01-01-001", "verification.waivers[0]"),
        ("CONF-CASE", "requirements[0]"),
    ] {
        let target = plan_mutation_request(id, path, Verb::Set)?
            .target
            .ok_or("mutation target should exist")?;
        assert!(
            target.ensure_supports(Verb::Set, id).is_err(),
            "structured list item should reject indexed set: {path}"
        );
    }
    Ok(())
}

#[test]
fn test_acceptance_criterion_set_handler_is_ssot_driven() -> Result<(), Box<dyn std::error::Error>>
{
    let target = plan_mutation_request("WI-2026-01-01-001", "acceptance_criteria[0]", Verb::Set)?
        .target
        .ok_or("acceptance criterion target missing")?;

    assert!(matches!(
        target,
        ResolvedTarget::IndexedItem {
            object_set_mode: Some(NestedObjectSetMode::AcceptanceCriterion),
            ..
        }
    ));
    Ok(())
}