use super::*;
use std::collections::{HashMap, HashSet};
use crate::ast::{Term, Clause};
use crate::engine::PrologEngine;
#[test]
fn test_pretty_printer_format_term() {
let atom = Term::Atom("hello".to_string());
assert_eq!(PrettyPrinter::format_term(&atom, 0), "hello");
let var = Term::Variable("X".to_string());
assert_eq!(PrettyPrinter::format_term(&var, 0), "X");
let num = Term::Number(42);
assert_eq!(PrettyPrinter::format_term(&num, 0), "42");
let neg = Term::Number(-17);
assert_eq!(PrettyPrinter::format_term(&neg, 0), "-17");
let compound_empty = Term::Compound("foo".to_string(), vec![]);
assert_eq!(PrettyPrinter::format_term(&compound_empty, 0), "foo()");
let compound_single = Term::Compound("foo".to_string(), vec![
Term::Atom("bar".to_string())
]);
assert_eq!(PrettyPrinter::format_term(&compound_single, 0), "foo(bar)");
let compound_multi = Term::Compound("foo".to_string(), vec![
Term::Atom("bar".to_string()),
Term::Variable("X".to_string())
]);
assert_eq!(PrettyPrinter::format_term(&compound_multi, 0), "foo(bar, X)");
let nested = Term::Compound("f".to_string(), vec![
Term::Compound("g".to_string(), vec![Term::Number(1)])
]);
assert_eq!(PrettyPrinter::format_term(&nested, 0), "f(g(1))");
}
#[test]
fn test_pretty_printer_long_format() {
let long_args = vec![
Term::Atom("very_long_atom_name_that_exceeds_limit".to_string()),
Term::Atom("another_very_long_atom_name".to_string()),
Term::Atom("third_long_atom_name".to_string()),
];
let compound = Term::Compound("predicate".to_string(), long_args);
let formatted = PrettyPrinter::format_term(&compound, 0);
assert!(formatted.contains("\n")); }
#[test]
fn test_pretty_printer_format_clause() {
let fact = Clause::fact(Term::Compound("parent".to_string(), vec![
Term::Atom("tom".to_string()),
Term::Atom("bob".to_string())
]));
assert_eq!(PrettyPrinter::format_clause(&fact, 0), "parent(tom, bob).");
let rule = Clause::rule(
Term::Compound("grandparent".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Z".to_string())
]),
vec![
Term::Compound("parent".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Y".to_string())
])
]
);
let formatted = PrettyPrinter::format_clause(&rule, 0);
assert!(formatted.contains(":-"));
assert!(formatted.ends_with("."));
}
#[test]
fn test_pretty_printer_format_substitution() {
let empty = HashMap::new();
assert_eq!(PrettyPrinter::format_substitution(&empty), "{}");
let mut single = HashMap::new();
single.insert("X".to_string(), Term::Atom("hello".to_string()));
assert_eq!(PrettyPrinter::format_substitution(&single), "{X -> hello}");
let mut multi = HashMap::new();
multi.insert("X".to_string(), Term::Number(42));
multi.insert("Y".to_string(), Term::Atom("world".to_string()));
let formatted = PrettyPrinter::format_substitution(&multi);
assert!(formatted.contains("X -> 42"));
assert!(formatted.contains("Y -> world"));
}
#[test]
fn test_pretty_printer_format_solutions() {
let empty_solutions: Vec<Substitution> = vec![];
let vars = vec!["X".to_string()];
assert_eq!(PrettyPrinter::format_solutions(&empty_solutions, &vars), "No solutions.");
let solution = HashMap::new();
let solutions = vec![solution];
let no_vars: Vec<String> = vec![];
assert_eq!(PrettyPrinter::format_solutions(&solutions, &no_vars), "1 solution(s): true");
let mut sol1 = HashMap::new();
sol1.insert("X".to_string(), Term::Number(1));
sol1.insert("Y".to_string(), Term::Atom("a".to_string()));
let mut sol2 = HashMap::new();
sol2.insert("X".to_string(), Term::Number(2));
sol2.insert("Y".to_string(), Term::Atom("b".to_string()));
let solutions = vec![sol1, sol2];
let vars = vec!["X".to_string(), "Y".to_string()];
let formatted = PrettyPrinter::format_solutions(&solutions, &vars);
assert!(formatted.contains("Found 2 solution(s)"));
assert!(formatted.contains("X")); assert!(formatted.contains("Y")); assert!(formatted.contains("1")); assert!(formatted.contains("2")); assert!(formatted.contains("a")); assert!(formatted.contains("b")); }
#[test]
fn test_term_utils_get_all_variables() {
let atom = Term::Atom("hello".to_string());
assert!(TermUtils::get_all_variables(&atom).is_empty());
let var = Term::Variable("X".to_string());
let vars = TermUtils::get_all_variables(&var);
assert_eq!(vars.len(), 1);
assert!(vars.contains("X"));
let compound = Term::Compound("foo".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Y".to_string()),
Term::Variable("X".to_string()), ]);
let vars = TermUtils::get_all_variables(&compound);
assert_eq!(vars.len(), 2); assert!(vars.contains("X"));
assert!(vars.contains("Y"));
let nested = Term::Compound("f".to_string(), vec![
Term::Compound("g".to_string(), vec![
Term::Variable("Z".to_string())
])
]);
let vars = TermUtils::get_all_variables(&nested);
assert_eq!(vars.len(), 1);
assert!(vars.contains("Z"));
}
#[test]
fn test_term_utils_depth() {
assert_eq!(TermUtils::term_depth(&Term::Atom("a".to_string())), 1);
assert_eq!(TermUtils::term_depth(&Term::Variable("X".to_string())), 1);
assert_eq!(TermUtils::term_depth(&Term::Number(42)), 1);
let empty = Term::Compound("f".to_string(), vec![]);
assert_eq!(TermUtils::term_depth(&empty), 1);
let flat = Term::Compound("f".to_string(), vec![
Term::Atom("a".to_string()),
Term::Atom("b".to_string())
]);
assert_eq!(TermUtils::term_depth(&flat), 2);
let nested = Term::Compound("f".to_string(), vec![
Term::Compound("g".to_string(), vec![
Term::Compound("h".to_string(), vec![
Term::Atom("a".to_string())
])
])
]);
assert_eq!(TermUtils::term_depth(&nested), 4);
}
#[test]
fn test_term_utils_size() {
assert_eq!(TermUtils::term_size(&Term::Atom("a".to_string())), 1);
assert_eq!(TermUtils::term_size(&Term::Variable("X".to_string())), 1);
assert_eq!(TermUtils::term_size(&Term::Number(42)), 1);
let compound = Term::Compound("foo".to_string(), vec![
Term::Variable("X".to_string()),
Term::Compound("bar".to_string(), vec![
Term::Variable("Y".to_string()),
Term::Number(42)
])
]);
assert_eq!(TermUtils::term_size(&compound), 5);
}
#[test]
fn test_term_utils_contains_variable() {
let term = Term::Compound("foo".to_string(), vec![
Term::Variable("X".to_string()),
Term::Atom("a".to_string())
]);
assert!(TermUtils::contains_variable(&term, "X"));
assert!(!TermUtils::contains_variable(&term, "Y"));
assert!(!TermUtils::contains_variable(&Term::Atom("a".to_string()), "X"));
}
#[test]
fn test_term_utils_replace_variable() {
let term = Term::Compound("foo".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Y".to_string()),
Term::Variable("X".to_string()), ]);
let replacement = Term::Atom("replaced".to_string());
let result = TermUtils::replace_variable(&term, "X", &replacement);
match result {
Term::Compound(_, args) => {
assert_eq!(args[0], Term::Atom("replaced".to_string()));
assert_eq!(args[1], Term::Variable("Y".to_string())); assert_eq!(args[2], Term::Atom("replaced".to_string()));
}
_ => panic!("Expected compound term"),
}
}
#[test]
fn test_term_utils_is_ground() {
assert!(TermUtils::is_ground(&Term::Atom("a".to_string())));
assert!(TermUtils::is_ground(&Term::Number(42)));
let ground_compound = Term::Compound("f".to_string(), vec![
Term::Atom("a".to_string()),
Term::Number(42)
]);
assert!(TermUtils::is_ground(&ground_compound));
assert!(!TermUtils::is_ground(&Term::Variable("X".to_string())));
let non_ground = Term::Compound("f".to_string(), vec![
Term::Variable("X".to_string())
]);
assert!(!TermUtils::is_ground(&non_ground));
}
#[test]
fn test_term_utils_list_conversion() {
let empty_vec: Vec<Term> = vec![];
let empty_list = TermUtils::vec_to_list(empty_vec.clone());
assert_eq!(empty_list, Term::Atom("[]".to_string()));
assert_eq!(TermUtils::list_to_vec(&empty_list), Some(empty_vec));
let vec = vec![
Term::Number(1),
Term::Number(2),
Term::Number(3)
];
let list = TermUtils::vec_to_list(vec.clone());
assert_eq!(TermUtils::list_to_vec(&list), Some(vec));
let not_a_list = Term::Atom("not_a_list".to_string());
assert_eq!(TermUtils::list_to_vec(¬_a_list), None);
let improper = Term::Compound(".".to_string(), vec![
Term::Number(1),
Term::Variable("X".to_string())
]);
assert_eq!(TermUtils::list_to_vec(&improper), None);
}
#[test]
fn test_term_utils_fresh_variable() {
let mut existing = HashSet::new();
existing.insert("X".to_string());
existing.insert("Y".to_string());
existing.insert("X1".to_string());
let fresh = TermUtils::fresh_variable(&existing, "X");
assert_eq!(fresh, "X2");
let fresh_new = TermUtils::fresh_variable(&existing, "Z");
assert_eq!(fresh_new, "Z");
let empty = HashSet::new();
let fresh_empty = TermUtils::fresh_variable(&empty, "Var");
assert_eq!(fresh_empty, "Var");
}
#[test]
fn test_clause_utils_group_by_predicate() {
let clause1 = Clause::fact(Term::Compound("parent".to_string(), vec![
Term::Atom("tom".to_string()),
Term::Atom("bob".to_string())
]));
let clause2 = Clause::fact(Term::Compound("parent".to_string(), vec![
Term::Atom("bob".to_string()),
Term::Atom("ann".to_string())
]));
let clause3 = Clause::fact(Term::Compound("likes".to_string(), vec![
Term::Atom("mary".to_string()),
Term::Atom("wine".to_string())
]));
let clauses = vec![clause1, clause2, clause3];
let groups = ClauseUtils::group_by_predicate(&clauses);
assert_eq!(groups.len(), 2);
assert_eq!(groups.get("parent/2").map(|v| v.len()), Some(2));
assert_eq!(groups.get("likes/2").map(|v| v.len()), Some(1));
}
#[test]
fn test_clause_utils_find_dependencies() {
let clause1 = Clause::rule(
Term::Compound("grandparent".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Z".to_string())
]),
vec![
Term::Compound("parent".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Y".to_string())
]),
Term::Compound("parent".to_string(), vec![
Term::Variable("Y".to_string()),
Term::Variable("Z".to_string())
])
]
);
let clause2 = Clause::fact(Term::Compound("parent".to_string(), vec![
Term::Atom("tom".to_string()),
Term::Atom("bob".to_string())
]));
let clauses = vec![clause1, clause2];
let deps = ClauseUtils::find_dependencies(&clauses, "parent", 2);
assert_eq!(deps.len(), 1);
assert!(deps.contains(&"grandparent/2".to_string()));
}
#[test]
fn test_clause_utils_find_recursive_predicates() {
let non_recursive = Clause::fact(Term::Compound("fact".to_string(), vec![
Term::Atom("a".to_string())
]));
let recursive = Clause::rule(
Term::Compound("ancestor".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Z".to_string())
]),
vec![
Term::Compound("parent".to_string(), vec![
Term::Variable("X".to_string()),
Term::Variable("Y".to_string())
]),
Term::Compound("ancestor".to_string(), vec![ Term::Variable("Y".to_string()),
Term::Variable("Z".to_string())
])
]
);
let clauses = vec![non_recursive, recursive];
let recursive_preds = ClauseUtils::find_recursive_predicates(&clauses);
assert_eq!(recursive_preds.len(), 1);
assert!(recursive_preds.contains(&"ancestor/2".to_string()));
}
#[test]
fn test_clause_utils_validate_clauses() {
let valid = Clause::fact(Term::Compound("fact".to_string(), vec![
Term::Atom("a".to_string())
]));
let invalid_head = Clause::fact(Term::Variable("X".to_string()));
let singleton = Clause::rule(
Term::Compound("pred".to_string(), vec![
Term::Variable("X".to_string())
]),
vec![
Term::Compound("other".to_string(), vec![
Term::Variable("Y".to_string()) ])
]
);
let clauses = vec![valid, invalid_head, singleton];
let errors = ClauseUtils::validate_clauses(&clauses);
assert!(errors.len() >= 2);
assert!(errors.iter().any(|e| e.contains("Head cannot be a variable")));
assert!(errors.iter().any(|e| e.contains("Singleton variable")));
}
#[test]
fn test_clause_utils_with_underscore_variables() {
let clause = Clause::rule(
Term::Compound("pred".to_string(), vec![
Term::Variable("X".to_string())
]),
vec![
Term::Compound("other".to_string(), vec![
Term::Variable("_Temp".to_string()) ])
]
);
let clauses = vec![clause];
let errors = ClauseUtils::validate_clauses(&clauses);
assert!(!errors.iter().any(|e| e.contains("_Temp")));
}
#[test]
fn test_engine_utils_load_program() {
let mut engine = PrologEngine::new();
let valid_program = "parent(tom, bob).\nparent(bob, ann).";
let result = EngineUtils::load_program(&mut engine, valid_program);
assert!(result.is_ok());
assert_eq!(engine.get_clauses().len(), 2);
let mut engine2 = PrologEngine::new();
let program_with_comments = "% Comment\nparent(tom, bob).\n\nparent(bob, ann).\n% Another comment";
let result = EngineUtils::load_program(&mut engine2, program_with_comments);
assert!(result.is_ok());
assert_eq!(engine2.get_clauses().len(), 2);
let mut engine3 = PrologEngine::new();
let invalid_program = "parent(tom, bob\nmissing_closing_paren";
let result = EngineUtils::load_program(&mut engine3, invalid_program);
assert!(result.is_err());
if let Err(errors) = result {
assert!(!errors.is_empty());
}
}
#[test]
fn test_engine_utils_batch_query() {
let mut engine = PrologEngine::new();
engine.parse_and_add("parent(tom, bob).").unwrap();
engine.parse_and_add("parent(bob, ann).").unwrap();
let queries = vec![
"parent(tom, bob)?", "parent(tom, ann)?", "parent(X, bob)?", ];
let results = EngineUtils::batch_query(&mut engine, &queries);
assert_eq!(results.len(), 3);
assert!(results[0].1.is_ok());
if let Ok(solutions) = &results[0].1 {
assert!(!solutions.is_empty());
}
assert!(results[1].1.is_ok());
if let Ok(solutions) = &results[1].1 {
assert!(solutions.is_empty());
}
assert!(results[2].1.is_ok());
if let Ok(solutions) = &results[2].1 {
assert!(!solutions.is_empty());
}
}
#[test]
fn test_engine_utils_analyze_database() {
let mut engine = PrologEngine::new();
engine.parse_and_add("parent(tom, bob).").unwrap();
engine.parse_and_add("parent(bob, ann).").unwrap();
engine.parse_and_add("parent(ann, joe).").unwrap();
engine.parse_and_add("likes(mary, wine).").unwrap();
engine.parse_and_add("ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).").unwrap();
engine.parse_and_add("ancestor(X, Y) :- parent(X, Y).").unwrap();
let analysis = EngineUtils::analyze_database(&engine);
assert_eq!(analysis.total_clauses, 6);
assert_eq!(analysis.total_predicates, 3);
assert_eq!(analysis.recursive_predicates.len(), 1);
assert!(analysis.recursive_predicates.contains(&"ancestor/2".to_string()));
assert!(analysis.largest_predicate.is_some());
if let Some((name, count)) = analysis.largest_predicate {
assert_eq!(name, "parent/2");
assert_eq!(count, 3);
}
}
#[test]
fn test_engine_utils_extract_query_variables() {
let vars = EngineUtils::extract_query_variables("parent(X, Y)");
assert_eq!(vars.len(), 2);
assert!(vars.contains(&"X".to_string()));
assert!(vars.contains(&"Y".to_string()));
let vars = EngineUtils::extract_query_variables("foo(X, X, Y)");
assert_eq!(vars.len(), 2);
let vars = EngineUtils::extract_query_variables("foo(_, X, _Temp)");
assert_eq!(vars.len(), 2); assert!(vars.contains(&"X".to_string()));
assert!(vars.contains(&"_Temp".to_string()));
let vars = EngineUtils::extract_query_variables("parent(tom, bob)");
assert!(vars.is_empty());
let vars = EngineUtils::extract_query_variables("parent(X, Y), ancestor(Y, Z123)");
assert_eq!(vars.len(), 3);
assert!(vars.contains(&"X".to_string()));
assert!(vars.contains(&"Y".to_string()));
assert!(vars.contains(&"Z123".to_string())); }
#[test]
fn test_database_analysis_display() {
let analysis = DatabaseAnalysis {
total_clauses: 10,
total_predicates: 3,
recursive_predicates: vec!["ancestor/2".to_string()],
largest_predicate: Some(("parent/2".to_string(), 5)),
average_clause_size: 3.5,
predicate_distribution: vec![
("parent/2".to_string(), 5),
("likes/2".to_string(), 3),
("ancestor/2".to_string(), 2),
],
};
let display = format!("{}", analysis);
assert!(display.contains("Total clauses: 10"));
assert!(display.contains("Total predicates: 3"));
assert!(display.contains("Average clause size: 3.5"));
assert!(display.contains("Largest predicate: parent/2 (5 clauses)"));
assert!(display.contains("Recursive predicates: ancestor/2"));
assert!(display.contains("parent/2: 5 clauses"));
}
#[test]
fn test_database_analysis_empty() {
let analysis = DatabaseAnalysis {
total_clauses: 0,
total_predicates: 0,
recursive_predicates: vec![],
largest_predicate: None,
average_clause_size: 0.0,
predicate_distribution: vec![],
};
let display = format!("{}", analysis);
assert!(display.contains("Total clauses: 0"));
assert!(display.contains("Total predicates: 0"));
assert!(!display.contains("Largest predicate:")); assert!(!display.contains("Recursive predicates:")); }
#[test]
fn test_edge_case_empty_inputs() {
assert!(TermUtils::get_all_variables(&Term::Atom("a".to_string())).is_empty());
let empty_clauses: Vec<Clause> = vec![];
assert!(ClauseUtils::group_by_predicate(&empty_clauses).is_empty());
assert!(ClauseUtils::find_recursive_predicates(&empty_clauses).is_empty());
assert!(ClauseUtils::validate_clauses(&empty_clauses).is_empty());
let empty_subst = HashMap::new();
assert_eq!(PrettyPrinter::format_substitution(&empty_subst), "{}");
let empty_vars = HashSet::new();
assert_eq!(TermUtils::fresh_variable(&empty_vars, "X"), "X");
}
#[test]
fn test_edge_case_circular_references() {
let recursive_clause = Clause::rule(
Term::Compound("loop".to_string(), vec![Term::Variable("X".to_string())]),
vec![Term::Compound("loop".to_string(), vec![Term::Variable("X".to_string())])]
);
let clauses = vec![recursive_clause];
let recursive_preds = ClauseUtils::find_recursive_predicates(&clauses);
assert!(recursive_preds.contains(&"loop/1".to_string()));
}
#[test]
fn test_edge_case_very_deep_nesting() {
let mut term = Term::Atom("base".to_string());
for i in 0..100 {
term = Term::Compound(format!("f{}", i), vec![term]);
}
assert_eq!(TermUtils::term_depth(&term), 101); assert_eq!(TermUtils::term_size(&term), 101); assert!(TermUtils::is_ground(&term)); }
#[test]
fn test_edge_case_large_collections() {
let mut large_set = HashSet::new();
large_set.insert("Var".to_string());
for i in 0..1000 {
large_set.insert(format!("Var{}", i));
}
let fresh = TermUtils::fresh_variable(&large_set, "Var");
assert_eq!(fresh, "Var1000");
let mut many_clauses = Vec::new();
for i in 0..100 {
many_clauses.push(Clause::fact(Term::Compound(
format!("pred{}", i),
vec![Term::Number(i as i64)]
)));
}
let groups = ClauseUtils::group_by_predicate(&many_clauses);
assert_eq!(groups.len(), 100);
}
#[test]
fn test_edge_case_special_characters_in_atoms() {
let special = Term::Atom("atom-with-special_chars!".to_string());
assert_eq!(PrettyPrinter::format_term(&special, 0), "atom-with-special_chars!");
let empty = Term::Atom("".to_string());
assert_eq!(PrettyPrinter::format_term(&empty, 0), "");
}
#[test]
fn test_edge_case_boundary_numbers() {
let max = Term::Number(i64::MAX);
let min = Term::Number(i64::MIN);
assert_eq!(PrettyPrinter::format_term(&max, 0), i64::MAX.to_string());
assert_eq!(PrettyPrinter::format_term(&min, 0), i64::MIN.to_string());
}
#[test]
fn test_replace_variable_with_self_reference() {
let term = Term::Variable("X".to_string());
let replacement = Term::Compound("f".to_string(), vec![
Term::Variable("X".to_string()) ]);
let result = TermUtils::replace_variable(&term, "X", &replacement);
match result {
Term::Compound(functor, args) => {
assert_eq!(functor, "f");
assert_eq!(args[0], Term::Variable("X".to_string()));
}
_ => panic!("Expected compound"),
}
}
#[test]
fn test_validate_clauses_with_head_as_number() {
let invalid = Clause::fact(Term::Number(42));
let clauses = vec![invalid];
let errors = ClauseUtils::validate_clauses(&clauses);
assert!(!errors.is_empty());
assert!(errors[0].contains("Head cannot be a number"));
}
#[test]
fn test_list_operations_with_nested_lists() {
let nested_list = vec![
Term::Number(1),
TermUtils::vec_to_list(vec![Term::Number(2), Term::Number(3)]), Term::Number(4)
];
let prolog_list = TermUtils::vec_to_list(nested_list.clone());
let back = TermUtils::list_to_vec(&prolog_list);
assert_eq!(back, Some(nested_list));
}