solverforge-cli 1.1.2

CLI for scaffolding and managing SolverForge constraint solver projects
use super::{
    domain::{
        find_annotated_struct, find_score_type, parse_vec_field, DomainModel, EntityInfo, FactInfo,
        StandardVarInfo,
    },
    mod_rewriter::{extend_tuple, extract_types, insert_mod_decl_assemble},
    skeleton::{generate_skeleton, Pattern},
    utils::{snake_to_title, validate_name},
};

#[test]
fn test_validate_name() {
    assert!(validate_name("max_hours").is_ok());
    assert!(validate_name("required_skill").is_ok());
    assert!(validate_name("a").is_ok());
    assert!(validate_name("MaxHours").is_err());
    assert!(validate_name("1bad").is_err());
    assert!(validate_name("bad-name").is_err());
    assert!(validate_name("").is_err());
}

#[test]
fn test_snake_to_title() {
    assert_eq!(snake_to_title("max_hours"), "Max Hours");
    assert_eq!(snake_to_title("required_skill"), "Required Skill");
    assert_eq!(snake_to_title("all_assigned"), "All Assigned");
    assert_eq!(snake_to_title("capacity"), "Capacity");
}

#[test]
fn test_extract_types_assemble() {
    let src = r#"mod assemble {
    pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
        (all_assigned::constraint(),)
    }
}"#;
    let (s, t) = extract_types(src);
    assert_eq!(s, "Plan");
    assert_eq!(t, "HardSoftScore");
}

#[test]
fn test_extract_types_flat() {
    let src = r#"pub fn create_constraints() -> impl ConstraintSet<VrpPlan, HardSoftScore> {
    (capacity, distance)
}"#;
    let (s, t) = extract_types(src);
    assert_eq!(s, "VrpPlan");
    assert_eq!(t, "HardSoftScore");
}

#[test]
fn test_extend_tuple_single_trailing_comma() {
    let src = r#"mod assemble {
    pub fn create_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
        (all_assigned::constraint(),)
    }
}"#;
    let result = extend_tuple(src, "max_hours::constraint()");
    assert!(result.contains("all_assigned::constraint(), max_hours::constraint()"));
}

#[test]
fn test_extend_tuple_flat_no_trailing_comma() {
    let src = r#"pub fn create_constraints() -> impl ConstraintSet<VrpPlan, HardSoftScore> {
    (capacity, distance)
}"#;
    let result = extend_tuple(src, "max_hours::constraint()");
    assert!(result.contains("distance, max_hours::constraint()"));
}

#[test]
fn test_insert_mod_decl_assemble() {
    let src = "mod all_assigned;\n\npub use self::assemble::create_constraints;\n";
    let result = insert_mod_decl_assemble(src, "mod max_hours;");
    assert!(
        result.contains("mod all_assigned;\n\nmod max_hours;"),
        "got: {:?}",
        result
    );
    assert!(result.contains("pub use self::assemble::create_constraints;"));
}

#[test]
fn test_parse_vec_field() {
    assert_eq!(
        parse_vec_field("    pub shifts: Vec<Shift>,"),
        Some(("shifts".to_string(), "Shift".to_string()))
    );
    assert_eq!(
        parse_vec_field("    employees: Vec<Employee>"),
        Some(("employees".to_string(), "Employee".to_string()))
    );
}

#[test]
fn test_find_annotated_struct() {
    let src = "#[planning_solution]\npub struct EmployeeSchedule {\n    pub score: Option<HardSoftDecimalScore>,\n}\n";
    assert_eq!(
        find_annotated_struct(src, "planning_solution"),
        Some("EmployeeSchedule".to_string())
    );
}

#[test]
fn test_find_annotated_struct_skips_multiline_attrs() {
    let src = r#"#[planning_solution(constraints = "crate::constraints::create_constraints")]
pub struct EmployeeSchedule {
    pub score: Option<HardSoftDecimalScore>,
}
"#;
    assert_eq!(
        find_annotated_struct(src, "planning_solution"),
        Some("EmployeeSchedule".to_string())
    );
}

#[test]
fn test_find_score_type() {
    let src = "#[planning_solution]\npub struct Plan {\n    pub score: Option<HardSoftScore>,\n}\n";
    assert_eq!(
        find_score_type(src, "Plan"),
        Some("HardSoftScore".to_string())
    );
}

#[test]
fn test_generate_skeleton_unary_hard() {
    let domain = DomainModel {
        solution_type: "EmployeeSchedule".to_string(),
        score_type: "HardSoftDecimalScore".to_string(),
        entities: vec![EntityInfo {
            field_name: "shifts".to_string(),
            item_type: "Shift".to_string(),
            planning_vars: vec![StandardVarInfo {
                field: "employee_idx".to_string(),
                value_range: "employees".to_string(),
                allows_unassigned: true,
            }],
            list_vars: vec![],
        }],
        facts: vec![],
    };
    let result = generate_skeleton(
        "no_overlap",
        Pattern::Unary,
        false,
        "EmployeeSchedule",
        "HardSoftDecimalScore",
        "No Overlap",
        Some(&domain),
    );
    assert!(result.contains("for_each(|s: &EmployeeSchedule| s.shifts.as_slice())"));
    assert!(result.contains("HardSoftDecimalScore::ONE_HARD"));
    assert!(result.contains("HARD:"));
    assert!(!result.contains("todo!"));
    assert!(result
        .contains("panic!(\"replace placeholder condition before enabling this constraint\")"));
}

#[test]
fn test_generate_skeleton_pair_hard() {
    let domain = DomainModel {
        solution_type: "EmployeeSchedule".to_string(),
        score_type: "HardSoftDecimalScore".to_string(),
        entities: vec![EntityInfo {
            field_name: "shifts".to_string(),
            item_type: "Shift".to_string(),
            planning_vars: vec![StandardVarInfo {
                field: "employee_idx".to_string(),
                value_range: "employees".to_string(),
                allows_unassigned: true,
            }],
            list_vars: vec![],
        }],
        facts: vec![],
    };
    let result = generate_skeleton(
        "no_overlap",
        Pattern::Pair,
        false,
        "EmployeeSchedule",
        "HardSoftDecimalScore",
        "No Overlap",
        Some(&domain),
    );
    assert!(result.contains("for_each(|s: &EmployeeSchedule| s.shifts.as_slice())"));
    assert!(result.contains("joiner::equal(|e: &Shift| e.employee_idx)"));
    assert!(result.contains(
        "panic!(\"replace placeholder pair condition before enabling this constraint\")"
    ));
    assert!(!result.contains("todo!"));
}

#[test]
fn test_generate_skeleton_join_hard() {
    let domain = DomainModel {
        solution_type: "EmployeeSchedule".to_string(),
        score_type: "HardSoftDecimalScore".to_string(),
        entities: vec![EntityInfo {
            field_name: "shifts".to_string(),
            item_type: "Shift".to_string(),
            planning_vars: vec![StandardVarInfo {
                field: "employee_idx".to_string(),
                value_range: "employees".to_string(),
                allows_unassigned: true,
            }],
            list_vars: vec![],
        }],
        facts: vec![FactInfo {
            field_name: "employees".to_string(),
            item_type: "Employee".to_string(),
        }],
    };
    let result = generate_skeleton(
        "required_skill",
        Pattern::Join,
        false,
        "EmployeeSchedule",
        "HardSoftDecimalScore",
        "Required Skill",
        Some(&domain),
    );
    assert!(result.contains("equal_bi"));
    assert!(result.contains("employees.as_slice()"));
    assert!(result.contains("Employee"));
    assert!(result.contains("|e: &Shift| e.employee_idx"));
    assert!(
        result.contains("replace placeholder join key extractor before enabling this constraint")
    );
    assert!(result.contains("replace placeholder join condition before enabling this constraint"));
    assert!(!result.contains("todo!"));
}

#[test]
fn test_generate_skeleton_balance_soft() {
    let domain = DomainModel {
        solution_type: "EmployeeSchedule".to_string(),
        score_type: "HardSoftDecimalScore".to_string(),
        entities: vec![EntityInfo {
            field_name: "shifts".to_string(),
            item_type: "Shift".to_string(),
            planning_vars: vec![StandardVarInfo {
                field: "employee_idx".to_string(),
                value_range: "employees".to_string(),
                allows_unassigned: true,
            }],
            list_vars: vec![],
        }],
        facts: vec![],
    };
    let result = generate_skeleton(
        "balance",
        Pattern::Balance,
        true,
        "EmployeeSchedule",
        "HardSoftDecimalScore",
        "Balance",
        Some(&domain),
    );
    assert!(result.contains(".balance(|e: &Shift| e.employee_idx)"));
    assert!(result.contains("SOFT:"));
}

#[test]
fn test_generate_skeleton_reward_soft_is_compile_safe() {
    let domain = DomainModel {
        solution_type: "EmployeeSchedule".to_string(),
        score_type: "HardSoftDecimalScore".to_string(),
        entities: vec![EntityInfo {
            field_name: "shifts".to_string(),
            item_type: "Shift".to_string(),
            planning_vars: vec![StandardVarInfo {
                field: "employee_idx".to_string(),
                value_range: "employees".to_string(),
                allows_unassigned: true,
            }],
            list_vars: vec![],
        }],
        facts: vec![],
    };
    let result = generate_skeleton(
        "preferred_assignment",
        Pattern::Reward,
        true,
        "EmployeeSchedule",
        "HardSoftDecimalScore",
        "Preferred Assignment",
        Some(&domain),
    );
    assert!(result.contains(".reward(HardSoftDecimalScore::ONE_SOFT)"));
    assert!(result.contains(
        "panic!(\"replace placeholder reward condition before enabling this constraint\")"
    ));
    assert!(!result.contains("todo!"));
}