namespace HSchool.People.Tests; public class SkillGrantTests { [Fact] public void FirstYear_HasCoreAndPrimary_ButNotChemistryOrEnglish() { var roster = Fixtures.Generate(Fixtures.Classrooms(11)); var pupils = PupilsIn(roster, 1).ToArray(); Assert.NotEmpty(pupils); Assert.All( pupils, person => { Assert.True(person.Skills.ContainsKey("Communication")); Assert.True(person.Skills.ContainsKey("RussianLanguage")); Assert.True(person.Skills.ContainsKey("Agility")); Assert.True(person.Skills.ContainsKey("Hauling")); Assert.True(person.Skills.ContainsKey("Mathematics")); Assert.False(person.Skills.ContainsKey("Chemistry")); Assert.False(person.Skills.ContainsKey("Physics")); Assert.False(person.Skills.ContainsKey("English")); Assert.False(person.Skills.ContainsKey("German")); Assert.False(person.Skills.ContainsKey("Pedagogy")); }); } [Fact] public void YearFive_HasEnglish_YearEight_HasChemistry() { var roster = Fixtures.Generate(Fixtures.Classrooms(11)); Assert.All( PupilsIn(roster, 5), person => { Assert.True(person.Skills.ContainsKey("English")); Assert.False(person.Skills.ContainsKey("Chemistry")); Assert.False(person.Skills.ContainsKey("German")); }); Assert.All(PupilsIn(roster, 8), person => Assert.True(person.Skills.ContainsKey("Chemistry"))); } [Fact] public void Adults_HaveNativeAndCommunication_ButNotEverySkill() { var catalog = Fixtures.Catalog(); var roster = Fixtures.Generate(Fixtures.Classrooms(11)); var adults = roster.People.Where(person => !person.IsStudent).ToArray(); Assert.NotEmpty(adults); var concrete = catalog.Skills.Values.Count(skill => !skill.Abstract); Assert.All( adults, person => { Assert.True(person.Skills.ContainsKey("Communication")); Assert.True(person.Skills.ContainsKey("RussianLanguage")); Assert.True(person.Skills.Count < concrete); Assert.InRange(person.Skills.Keys.Count(name => catalog.Skills[name].Work), 0, 2); }); Assert.Contains( adults, person => person.Skills.Keys.Any(name => catalog.Skills[name].Work)); } [Fact] public void BelarusianNative_IsGrantedToEveryone_PupilsStillStudyRussian() { var catalog = Fixtures.Catalog(); var roster = RosterGenerator.Generate( catalog, Fixtures.Classrooms(11), Fixtures.SchoolSeed, "Russia", Fixtures.AsOf, "BelarusianLanguage"); var max = catalog.Countries["Russia"].Names.RelatedLanguageMax; Assert.All( roster.People, person => { Assert.True(person.Skills.ContainsKey("BelarusianLanguage")); if (person.Skills.TryGetValue("UkrainianLanguage", out var ukrainian)) { Assert.InRange(ukrainian, 0, max); } }); Assert.All( roster.People.Where(person => person.IsStudent), person => Assert.True(person.Skills.ContainsKey("RussianLanguage"))); Assert.Contains( roster.People.Where(person => person.IsStudent), person => person.Skills["RussianLanguage"] > max); } [Fact] public void RelatedLanguages_AreCommonAndStayLow() { var catalog = Fixtures.Catalog(); var max = catalog.Countries["Russia"].Names.RelatedLanguageMax; var roster = Fixtures.Generate(Fixtures.Classrooms(11)); Assert.Contains( roster.People, person => person.Skills.ContainsKey("BelarusianLanguage") || person.Skills.ContainsKey("UkrainianLanguage")); Assert.Contains(roster.People, person => !person.Skills.ContainsKey("BelarusianLanguage")); Assert.Contains(roster.People, person => !person.Skills.ContainsKey("UkrainianLanguage")); Assert.True( roster.People.Count(person => person.Skills.ContainsKey("BelarusianLanguage") || person.Skills.ContainsKey("UkrainianLanguage")) > roster.People.Count / 2); Assert.All( roster.People, person => { Assert.True(person.Skills.ContainsKey("RussianLanguage")); if (person.Skills.TryGetValue("BelarusianLanguage", out var belarusian)) { Assert.InRange(belarusian, 0, max); } if (person.Skills.TryGetValue("UkrainianLanguage", out var ukrainian)) { Assert.InRange(ukrainian, 0, max); } }); } /// /// A school saved before native languages carries no pick, and the reload path asks for one /// with rollIfOmitted: false. The contract is that it takes the first listed language /// instead of rolling — a roll would change an existing school's tongue on the next start, /// and the intake after it would draw different people. /// [Fact] public void ReloadWithoutASavedPick_TakesTheFirstLanguageInsteadOfRolling() { var names = Fixtures.Catalog().Countries["Russia"].Names; Assert.True(names.Spoken.Count > 1, "this pins behaviour that only matters for a multi-language set"); foreach (var seed in new[] { 1, 2, 7, 20260818 }) { Assert.Equal(names.Spoken[0], NativeLanguages.Pick(names, seed, requested: null, rollIfOmitted: false)); } // A new school rolls instead, and the roll is stable for one seed. var rolled = NativeLanguages.Pick(names, 7, requested: null, rollIfOmitted: true); Assert.Contains(rolled, names.Spoken); Assert.Equal(rolled, NativeLanguages.Pick(names, 7, requested: null, rollIfOmitted: true)); // A saved pick is honoured whichever way it is asked for. Assert.Equal( names.Spoken[^1], NativeLanguages.Pick(names, 7, names.Spoken[^1], rollIfOmitted: false)); // Anything outside the set comes back null so the create path can answer 400. Assert.Null(NativeLanguages.Pick(names, 7, "Klingon", rollIfOmitted: true)); } [Fact] public void OmittedNativeLanguage_IsStableForTheSameSeed() { var map = Fixtures.Classrooms(2); var a = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Russia", Fixtures.AsOf); var b = RosterGenerator.Generate(Fixtures.Catalog(), map, 7, "Russia", Fixtures.AsOf); Assert.Equal( a.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal))), b.People.Select(person => string.Join(',', person.Skills.Keys.Order(StringComparer.Ordinal)))); } [Fact] public void Pupils_DoNotRollWorkSkills() { var catalog = Fixtures.Catalog(); var roster = Fixtures.Generate(Fixtures.Classrooms(11)); Assert.All( roster.People.Where(person => person.IsStudent), person => Assert.DoesNotContain(person.Skills.Keys, name => catalog.Skills[name].Work)); } [Fact] public void Intake_YearFourGainsEnglishWithoutRerollingMath() { var catalog = Fixtures.Catalog(); var before = Fixtures.Generate(Fixtures.Classrooms(5)); var year4 = before.Classes.Single(schoolClass => schoolClass.Year == 4); var pupil = before.People.First(person => person.Id == year4.PupilIds[0]); Assert.False(pupil.Skills.ContainsKey("English")); var math = pupil.Skills["Mathematics"]; var after = YearlyIntake.Apply( catalog, before, Fixtures.SchoolSeed, "Russia", new DateTime(2012, 9, 1, 0, 0, 0, DateTimeKind.Utc)); var grown = after.People.First(person => person.Id == pupil.Id); Assert.True(grown.ClassId is { } classId && after.Classes.Any(schoolClass => schoolClass.Id.Equals(classId, StringComparison.Ordinal) && schoolClass.Year == 5)); Assert.True(grown.Skills.ContainsKey("English")); Assert.Equal(math, grown.Skills["Mathematics"]); } private static IEnumerable PupilsIn(Roster roster, int year) { var classIds = roster.Classes .Where(schoolClass => schoolClass.Year == year) .Select(schoolClass => schoolClass.Id) .ToHashSet(StringComparer.Ordinal); return roster.People.Where(person => person.IsStudent && person.ClassId is { } classId && classIds.Contains(classId)); } }