solverforge-cli 1.1.3

CLI for scaffolding and managing SolverForge constraint solver projects
use super::{
    generators::{generate_entity, generate_fact, generate_solution},
    run::{generate_data_loader_stub, remove_default_scaffold},
    utils::{pluralize, snake_to_pascal, validate_score_type},
    wiring::{add_import, replace_score_type},
};
use crate::test_support;

#[test]
fn test_snake_to_pascal() {
    assert_eq!(snake_to_pascal("shift"), "Shift");
    assert_eq!(snake_to_pascal("employee_schedule"), "EmployeeSchedule");
    assert_eq!(
        snake_to_pascal("vehicle_routing_plan"),
        "VehicleRoutingPlan"
    );
    assert_eq!(snake_to_pascal("plan"), "Plan");
}

#[test]
fn test_pluralize() {
    assert_eq!(pluralize("shift"), "shifts");
    assert_eq!(pluralize("employee"), "employees");
    assert_eq!(pluralize("bus"), "buses");
    assert_eq!(pluralize("category"), "categories");
    assert_eq!(pluralize("day"), "days");
    assert_eq!(pluralize("key"), "keys");
    assert_eq!(pluralize("task"), "tasks");
}

#[test]
fn test_validate_score_type() {
    assert!(validate_score_type("HardSoftScore").is_ok());
    assert!(validate_score_type("HardSoftDecimalScore").is_ok());
    assert!(validate_score_type("HardMediumSoftScore").is_ok());
    assert!(validate_score_type("SimpleScore").is_ok());
    assert!(validate_score_type("BendableScore").is_ok());
    assert!(validate_score_type("FakeScore").is_err());
}

#[test]
fn test_generate_entity_no_var() {
    let src = generate_entity("Shift", None, &[]);
    assert!(src.contains("#[planning_entity]"));
    assert!(src.contains("pub struct Shift"));
    assert!(src.contains("#[planning_id]"));
    assert!(src.contains("pub id: String"));
    assert!(!src.contains("#[planning_variable]"));
}

#[test]
fn test_generate_entity_with_var() {
    let src = generate_entity("Shift", Some("employee_idx"), &[]);
    assert!(src.contains("#[planning_variable(allows_unassigned = true)]"));
    assert!(src.contains("pub employee_idx: Option<usize>"));
    assert!(src.contains("employee_idx: None"));
}

#[test]
fn test_generate_fact() {
    let src = generate_fact("Employee", &[]);
    assert!(src.contains("#[problem_fact]"));
    assert!(src.contains("pub struct Employee"));
    assert!(src.contains("#[planning_id]"));
    assert!(src.contains("pub id: String"));
    assert!(src.contains("pub name: String"));
}

#[test]
fn test_generate_solution() {
    let src = generate_solution("Schedule", "HardSoftDecimalScore");
    assert!(src.contains("#[planning_solution]"));
    assert!(src.contains("pub struct Schedule"));
    assert!(src.contains("#[planning_score]"));
    assert!(src.contains("pub score: Option<HardSoftDecimalScore>"));
}

#[test]
fn test_add_import_new() {
    let src = "use solverforge::prelude::*;\n\nstruct Foo;\n";
    let result = add_import(src, "use super::Bar;");
    assert!(result.contains("use super::Bar;"));
    let use_pos = result.find("use solverforge").unwrap();
    let bar_pos = result.find("use super::Bar;").unwrap();
    assert!(bar_pos > use_pos);
}

#[test]
fn test_add_import_idempotent() {
    let src = "use super::Bar;\nstruct Foo;\n";
    let result = add_import(src, "use super::Bar;");
    assert_eq!(result.matches("use super::Bar;").count(), 1);
}

#[test]
fn test_replace_score_type() {
    let src = "pub score: Option<HardSoftScore>,\n";
    let result = replace_score_type(src, "HardSoftScore", "HardSoftDecimalScore").unwrap();
    assert!(result.contains("HardSoftDecimalScore"));
    assert!(!result.contains("HardSoftScore"));
}

#[test]
fn test_replace_score_type_missing() {
    let src = "pub score: Option<HardSoftScore>,\n";
    let result = replace_score_type(src, "SimpleScore", "HardSoftScore");
    assert!(result.is_err());
}

#[test]
fn test_inject_second_planning_variable() {
    use super::wiring::inject_planning_variable;

    let src = generate_entity("Surgery", Some("room_idx"), &[]);
    let result =
        inject_planning_variable(&src, "Surgery", "slot_idx").expect("inject should succeed");

    assert!(
        result.contains("slot_idx: None"),
        "slot_idx: None not found in output"
    );

    let self_start = result.find("Self {").expect("Self { not found");
    let self_block = &result[self_start..];
    let close = self_block.find('}').expect("} not found after Self {");
    let self_literal = &self_block[..=close];
    assert!(
        self_literal.contains("room_idx: None"),
        "room_idx: None not inside Self {{ }}: got: {self_literal}"
    );
    assert!(
        self_literal.contains("slot_idx: None"),
        "slot_idx: None not inside Self {{ }}: got:\n{result}"
    );
}

#[test]
fn test_inject_list_variable() {
    use super::wiring::inject_list_variable;

    let src = generate_entity("Route", None, &[]);
    let result =
        inject_list_variable(&src, "Route", "stops", "visits").expect("inject should succeed");

    assert!(result.contains("#[planning_list_variable(element_collection = \"visits\")]"));
    assert!(result.contains("pub stops: Vec<usize>"));
    assert!(result.contains("stops: Vec::new()"));
}

#[test]
fn test_remove_variable_field() {
    use super::wiring::{inject_list_variable, inject_planning_variable, remove_variable_field};

    let src = generate_entity("Route", Some("driver_idx"), &[]);
    let src = inject_list_variable(&src, "Route", "stops", "visits").expect("list inject");
    let src = inject_planning_variable(&src, "Route", "backup_idx").expect("var inject");
    let result = remove_variable_field(&src, "stops").expect("remove should succeed");

    assert!(!result.contains("pub stops: Vec<usize>"));
    assert!(!result.contains("#[planning_list_variable"));
    assert!(!result.contains("stops: Vec::new()"));
    assert!(result.contains("pub driver_idx: Option<usize>"));
    assert!(result.contains("pub backup_idx: Option<usize>"));
}

#[test]
fn test_update_domain_mod_format() {
    let mod_line = format!("mod {};", "shift");
    let use_line = format!("pub use {}::{};", "shift", "Shift");
    assert_eq!(mod_line, "mod shift;");
    assert_eq!(use_line, "pub use shift::Shift;");
}

#[test]
fn test_wire_collection_into_solution_updates_neutral_constructor() {
    use super::wiring::insert_field_and_import;

    let src = r#"use serde::{Deserialize, Serialize};
use solverforge::prelude::*;

#[planning_solution]
#[derive(Serialize, Deserialize)]
pub struct Plan {
    #[planning_score]
    pub score: Option<HardSoftScore>,
}

impl Plan {
    pub fn new() -> Self {
        Self { score: None }
    }
}
"#;

    let result = insert_field_and_import(
        src,
        "Plan",
        "Resource",
        "resources",
        "    #[problem_fact_collection]\n    pub resources: Vec<Resource>,",
    )
    .expect("insert should succeed");

    assert!(
        result.contains("pub fn new(resources: Vec<Resource>) -> Self"),
        "{result}"
    );
    assert!(result.contains("resources: resources"), "{result}");
}

#[test]
fn test_generate_data_loader_stub_is_compile_safe() {
    let stub = generate_data_loader_stub();
    assert!(stub.contains("pub fn load() -> Result<(), Box<dyn std::error::Error>>"));
    assert!(stub.contains("Ok(())"));
    assert!(!stub.contains("todo!"));
}

#[test]
fn test_remove_default_scaffold_rewrites_data_module_without_todo() {
    let guard = test_support::lock_cwd();

    let tmp = tempfile::tempdir().expect("failed to create temp dir");
    let original_dir = std::env::current_dir().expect("failed to get current dir");

    std::fs::create_dir_all(tmp.path().join("src/domain")).expect("failed to create domain dir");
    std::fs::create_dir_all(tmp.path().join("src/constraints"))
        .expect("failed to create constraints dir");
    std::fs::create_dir_all(tmp.path().join("src/data")).expect("failed to create data dir");
    std::fs::write(
        tmp.path().join("src/domain/mod.rs"),
        "pub mod plan;\npub mod task;\npub mod resource;\n",
    )
    .expect("failed to write domain mod");
    std::fs::write(
        tmp.path().join("src/domain/plan.rs"),
        "// Rename this to something domain-specific\n",
    )
    .expect("failed to write plan");
    std::fs::write(
        tmp.path().join("src/constraints/all_assigned.rs"),
        "placeholder",
    )
    .expect("failed to write all_assigned");
    std::fs::write(
        tmp.path().join("src/constraints/mod.rs"),
        "mod all_assigned;\n(all_assigned::constraint(),)\n",
    )
    .expect("failed to write constraints mod");
    std::fs::write(
        tmp.path().join("src/data/mod.rs"),
        "todo!(\"Implement data loading\")\n",
    )
    .expect("failed to write data mod");

    std::env::set_current_dir(tmp.path()).expect("failed to enter temp dir");
    let result = remove_default_scaffold();
    std::env::set_current_dir(original_dir).expect("failed to restore current dir");
    drop(guard);

    result.expect("remove_default_scaffold should succeed");

    let data_mod = std::fs::read_to_string(tmp.path().join("src/data/mod.rs"))
        .expect("failed to read rewritten data mod");
    assert!(data_mod.contains("Ok(())"));
    assert!(!data_mod.contains("todo!"));
}