use std::collections::{BTreeSet, HashSet};
use super::reqfile::{gen_reqfile, parse_reqfile_str};
use crate::Stat;
use crate::model::req::PrereqGroup;
#[test]
fn reqfile_prereqs() {
let content = r"
# base reqs
base := 25 STR
armor := 90 FTD
# dep for an inline req
base => advanced := 50 INT
# dep for an identifier
base, armor => upgraded
# the identifier in question
upgraded := 75 WLL
base, armor => 100 CHA
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.general.len(), 5);
let base = payload
.general
.iter()
.find(|r| r.name == Some("base".to_string()))
.unwrap();
assert!(base.prereqs.is_empty());
let armor = payload
.general
.iter()
.find(|r| r.name == Some("armor".to_string()))
.unwrap();
assert!(armor.prereqs.is_empty());
let advanced = payload
.general
.iter()
.find(|r| r.name == Some("advanced".to_string()))
.unwrap();
assert_eq!(
advanced.prereqs,
BTreeSet::from([PrereqGroup::single("base")])
);
let upgraded = payload
.general
.iter()
.find(|r| r.name == Some("upgraded".to_string()))
.unwrap();
assert_eq!(
upgraded.prereqs,
BTreeSet::from([PrereqGroup::single("base"), PrereqGroup::single("armor")])
);
let anon = payload.general.iter().find(|r| r.name.is_none()).unwrap();
assert_eq!(
anon.prereqs,
BTreeSet::from([PrereqGroup::single("base"), PrereqGroup::single("armor")])
);
}
#[test]
fn reqfile_gen_no_optional() {
let content = r"
Free:
crystal := 40 ice
surge := 40 ltn
scrapsinger := 35 mtl
fulgurite_formation := 50 ice, 50 ltn
90 ltn, 90r hvy
40 ftd
25 cha
25 int
Post:
75r hvy
20r ftd, 20r flm, 20r ltn
80r flm
20r mtl
# prereqs
crystal, surge => fulgurite_formation
battleaxe := ()
()
";
let payload = parse_reqfile_str(content).unwrap();
let gen_content = gen_reqfile(&payload);
let new_payload = parse_reqfile_str(&gen_content).expect(&gen_content);
let a = payload.general.into_iter().collect::<HashSet<_>>();
let b = new_payload.general.into_iter().collect::<HashSet<_>>();
assert_eq!(a, b);
let a = payload.post.into_iter().collect::<HashSet<_>>();
let b = new_payload.post.into_iter().collect::<HashSet<_>>();
assert_eq!(a, b);
}
#[test]
fn reqfile_gen_with_optional() {
let content = r"
Free:
crystal := 40 ice
+ shared_prereq := 10 str
1; opt_a := 20 int
2; opt_b := 30 ftd
shared_prereq => opt_a
shared_prereq => opt_b
Post:
75r hvy
3; post_opt := 50 wll
";
let payload = parse_reqfile_str(content).unwrap();
let gen_content = gen_reqfile(&payload);
let user_at = gen_content.find("# USER REQS").expect(&gen_content);
let opt_at = gen_content.find("# OPTIONAL PRESETS").expect(&gen_content);
assert!(user_at < opt_at);
let new_payload = parse_reqfile_str(&gen_content).expect(&gen_content);
let a = payload.general.iter().cloned().collect::<HashSet<_>>();
let b = new_payload.general.iter().cloned().collect::<HashSet<_>>();
assert_eq!(a, b);
let a = payload.post.iter().cloned().collect::<HashSet<_>>();
let b = new_payload.post.iter().cloned().collect::<HashSet<_>>();
assert_eq!(a, b);
assert_eq!(payload.optional.len(), new_payload.optional.len());
for group in &payload.optional {
let new_group = new_payload
.optional
.iter()
.find(|g| g.weight == group.weight)
.expect(&gen_content);
assert_eq!(group.general, new_group.general);
assert_eq!(group.post, new_group.post);
}
}
#[test]
fn reqfile_gen_optional_shared_prereq() {
let content = r"
Free:
shared := 10 str
1; opt_a := 20 int
2; opt_b := 30 ftd
shared => opt_a
shared => opt_b
";
let payload = parse_reqfile_str(content).unwrap();
let gen_content = gen_reqfile(&payload);
let new_payload = parse_reqfile_str(&gen_content).expect(&gen_content);
assert!(new_payload.general.is_empty());
assert_eq!(new_payload.optional.len(), 2);
for group in &payload.optional {
let new_group = new_payload
.optional
.iter()
.find(|g| g.weight == group.weight)
.expect(&gen_content);
assert_eq!(group.general, new_group.general);
}
}
#[test]
fn reqfile_gen_optional_transitive_timing() {
let content = r"
Free:
root := 5 cha
mid := 20 int
Post:
1; leaf := 30 ftd
root => mid
mid => leaf
";
let payload = parse_reqfile_str(content).unwrap();
let gen_content = gen_reqfile(&payload);
let new_payload = parse_reqfile_str(&gen_content).expect(&gen_content);
assert!(new_payload.general.is_empty());
assert_eq!(new_payload.optional.len(), 1);
let group = &payload.optional[0];
let new_group = &new_payload.optional[0];
assert_eq!(group.weight, new_group.weight);
assert_eq!(group.general, new_group.general);
assert_eq!(group.post, new_group.post);
}
#[test]
fn reqfile_gen_final_ranges() {
let content = r"
Free:
crystal := 40 ice
Post:
75r hvy
5 <= INT <= 20
";
let payload = parse_reqfile_str(content).unwrap();
let gen_content = gen_reqfile(&payload);
let new_payload = parse_reqfile_str(&gen_content).expect(&gen_content);
assert_eq!(new_payload.final_ranges.len(), 1);
assert_eq!(new_payload.final_ranges[0].stat, Stat::Intelligence);
assert_eq!(new_payload.final_ranges[0].range, 5..=20);
}
#[test]
fn optional_basic_parsing() {
let content = r"
Free:
1; exoskeleton := 40 ftd
neural_overload := 85 int
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.general.len(), 1);
let neural = payload
.general
.iter()
.find(|r| r.name == Some("neural_overload".to_string()));
assert!(neural.is_some());
assert_eq!(payload.optional.len(), 1);
let opt_group = &payload.optional[0];
assert_eq!(opt_group.weight, 1);
assert_eq!(opt_group.general.len(), 1);
let exo = opt_group
.general
.iter()
.find(|r| r.name == Some("exoskeleton".to_string()));
assert!(exo.is_some());
}
#[test]
fn optional_weight_range() {
for w in 1..=20 {
let content = format!(
r"
Free:
{w}; some_req := 40 ftd
"
);
let payload = parse_reqfile_str(&content).unwrap();
assert_eq!(payload.optional.len(), 1);
assert_eq!(payload.optional[0].weight, w);
}
let content = r"
Free:
21; some_req := 40 ftd
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
let content = r"
Free:
0; some_req := 40 ftd
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
}
#[test]
fn optional_prereq_of_required_is_invalid() {
let content = r"
Free:
1; optional_prereq := 30 ftd
required_dependent := 50 int
optional_prereq => required_dependent
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("optional") || err_msg.contains("dependents are required"));
}
#[test]
fn optional_prereqs_become_optional() {
let content = r"
Free:
p1 := 10 str
p2 := 20 int
p3 := 30 ftd
1; has_prereqs := 42 hvy
p1, p2, p3 => has_prereqs
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.weight, 1);
assert_eq!(group.general.len(), 4);
let names: HashSet<_> = group
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(names.contains("p1"));
assert!(names.contains("p2"));
assert!(names.contains("p3"));
assert!(names.contains("has_prereqs"));
}
#[test]
fn optional_force_required_directive() {
let content = r"
Free:
p1 := 10 str
+ p2 := 20 int
p3 := 30 ftd
1; has_prereqs := 42 hvy
p1, p2, p3 => has_prereqs
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.general.len(), 1);
let p2 = payload
.general
.iter()
.find(|r| r.name == Some("p2".to_string()));
assert!(p2.is_some());
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.general.len(), 3);
let opt_names: HashSet<_> = group
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(opt_names.contains("p1"));
assert!(opt_names.contains("p3"));
assert!(opt_names.contains("has_prereqs"));
assert!(!opt_names.contains("p2"));
}
#[test]
fn optional_inline_prereqs_syntax() {
let content = r"
Free:
p1 := 10 str
p2 := 20 int
1; p1, p2 => 42 hvy
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.general.len(), 3);
}
#[test]
fn optional_timing_respected() {
let content = r"
Free:
1; free_opt := 40 ftd
Post:
2; post_opt := 50 int
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.optional.len(), 2);
let free_group = payload.optional.iter().find(|g| g.weight == 1).unwrap();
let post_group = payload.optional.iter().find(|g| g.weight == 2).unwrap();
assert_eq!(free_group.general.len(), 1);
assert!(free_group.post.is_empty());
assert!(post_group.general.is_empty());
assert_eq!(post_group.post.len(), 1);
}
#[test]
fn optional_annotation_on_dependency_statement_errors() {
let content = r"
Free:
prereq := 10 str
dependent := 20 int
1; prereq => dependent
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("definition") || err_msg.contains("dependency statement"));
}
#[test]
fn optional_empty_req_with_prereqs() {
let content = r"
Free:
scrapsinger := 35 mtl
crystal := 40 ice
surge := 40 ltn
1; golden_age := ()
scrapsinger, crystal, surge => golden_age
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.general.len(), 4);
}
#[test]
fn optional_transitive_prereqs() {
let content = r"
Free:
root := 10 str
mid := 20 int
1; leaf := 30 ftd
root => mid
mid => leaf
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.general.len(), 3);
let names: HashSet<_> = group
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(names.contains("root"));
assert!(names.contains("mid"));
assert!(names.contains("leaf"));
}
#[test]
fn optional_force_required_transitive() {
let content = r"
Free:
grandparent := 5 cha
parent := 10 str
+ child := 20 int
1; grandchild := 30 ftd
grandparent => parent
parent => child
child => grandchild
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.general.len(), 3);
let req_names: HashSet<_> = payload
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(req_names.contains("grandparent"));
assert!(req_names.contains("parent"));
assert!(req_names.contains("child"));
assert_eq!(payload.optional.len(), 1);
let group = &payload.optional[0];
assert_eq!(group.general.len(), 1);
let opt_names: HashSet<_> = group
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(opt_names.contains("grandchild"));
}
#[test]
fn optional_shared_prereq_duplicated() {
let content = r"
Free:
shared := 10 str
1; opt_a := 20 int
2; opt_b := 30 ftd
shared => opt_a
shared => opt_b
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 2);
let group_a = payload.optional.iter().find(|g| g.weight == 1).unwrap();
let group_b = payload.optional.iter().find(|g| g.weight == 2).unwrap();
assert_eq!(group_a.general.len(), 2);
assert_eq!(group_b.general.len(), 2);
let names_a: HashSet<_> = group_a
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
let names_b: HashSet<_> = group_b
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(names_a.contains("shared"));
assert!(names_a.contains("opt_a"));
assert!(names_b.contains("shared"));
assert!(names_b.contains("opt_b"));
}
#[test]
fn optional_shared_prereq() {
let content = r"
Free:
root := 5 cha
shared := 10 str
1; opt_a := 20 int
2; opt_b := 30 ftd
root => shared
shared => opt_a
shared => opt_b
";
let payload = parse_reqfile_str(content).unwrap();
assert!(payload.general.is_empty());
assert_eq!(payload.optional.len(), 2);
let group_a = payload.optional.iter().find(|g| g.weight == 1).unwrap();
let group_b = payload.optional.iter().find(|g| g.weight == 2).unwrap();
assert_eq!(group_a.general.len(), 3);
assert_eq!(group_b.general.len(), 3);
let names_a: HashSet<_> = group_a
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
let names_b: HashSet<_> = group_b
.general
.iter()
.filter_map(|r| r.name.clone())
.collect();
assert!(names_a.contains("root"));
assert!(names_a.contains("shared"));
assert!(names_a.contains("opt_a"));
assert!(names_b.contains("root"));
assert!(names_b.contains("shared"));
assert!(names_b.contains("opt_b"));
}
#[test]
fn invalid_dependence_cycle() {
let content = r"
Free:
a := 10 str
b := 20 int
c := 30 ftd
a => b
b => c
c => a
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
let Err(err) = result else { panic!() };
let err_msg = err.to_string();
assert!(err_msg.contains("cycle") || err_msg.contains("Cycle"));
}
#[test]
fn invalid_dependence_cycle_2() {
let content = r"
Free:
1; a := 10 str
b := 20 int
a => b
b => a
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
}
#[test]
fn invalid_annotations_on_deps() {
let content = r"
Free:
prereq := 10 str
dependent := 20 int
+ prereq => dependent
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
let content = r"
Free:
prereq := 10 str
dependent := 20 int
1 ; prereq => dependent
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
}
#[test]
fn range_specifier_basic_parsing() {
let content = r"
Post:
5 <= INT <= 20
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.final_ranges.len(), 1);
let range = &payload.final_ranges[0];
assert_eq!(range.stat, Stat::Intelligence);
assert_eq!(range.range, 5..=20);
}
#[test]
fn range_specifier_inclusive_bounds() {
let cases = [
("5 <= INT <= 20", Stat::Intelligence, 5..=20),
("10 <= AGL <= 30", Stat::Agility, 10..=30),
("42 <= FTD <= 42", Stat::Fortitude, 42..=42),
];
for (line, stat, expected) in cases {
let content = format!("Post:\n{line}");
let payload =
parse_reqfile_str(&content).unwrap_or_else(|_| panic!("failed to parse: {line}"));
assert_eq!(payload.final_ranges.len(), 1, "{line}");
assert_eq!(payload.final_ranges[0].stat, stat, "{line}");
assert_eq!(payload.final_ranges[0].range, expected, "{line}");
}
assert!(parse_reqfile_str("Post:\n5 < INT < 20").is_err());
}
#[test]
fn range_specifier_only_post() {
let content = r"
Free:
5 <= INT <= 20
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
}
#[test]
fn range_specifier_duplicate_stat_errors() {
let content = r"
Post:
5 <= INT <= 20
8 <= INT <= 40
";
let result = parse_reqfile_str(content);
assert!(result.is_err());
}
#[test]
fn range_specifier_distinct_stats_ok() {
let content = r"
Post:
5 <= INT <= 20
3 <= STR <= 10
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.final_ranges.len(), 2);
}
#[test]
fn range_specifier_inverted_errors() {
let content = r"
Post:
20 <= INT <= 5
";
assert!(parse_reqfile_str(content).is_err());
}
#[test]
fn range_specifier_coexists_with_reqs() {
let content = r"
Free:
crystal := 40 ice
Post:
75r hvy
5 <= INT <= 20
";
let payload = parse_reqfile_str(content).unwrap();
assert_eq!(payload.general.len(), 1);
assert_eq!(payload.post.len(), 1);
assert_eq!(payload.final_ranges.len(), 1);
assert_eq!(payload.final_ranges[0].stat, Stat::Intelligence);
}