using Core.Entities; using Core.Models; using Core.Validation; using Tests.Builders; namespace Tests.Validation; [TestFixture] public class ValidationServiceTests { private ValidationConfiguration _defaultConfig; [SetUp] public void SetUp() { BuilderExtensions.ResetAllBuilders(); _defaultConfig = ValidationConfiguration.Default; } [Test] public void ValidationService_DiscoverRules_FindsAllRules() { // Arrange var service = new ValidationService(_defaultConfig); // Act - rules are discovered in constructor via reflection // Assert - verify rules were discovered by validating entities var student = StudentBuilder.Default() .WithRanking(EventDefinitionBuilder.Individual("Flight").Build(), 1) .Build(); var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Should find warnings since student has no regional event (if required by config) Assert.That(warnings, Is.Not.Null); } [Test] public void ValidateStudent_WithValidRankings_ReturnsNoWarnings() { // Arrange var config = new ValidationConfiguration { RequireRegionalEvent = true, RequireOnSiteActivity = true, RequireIndividualEvent = false, MinRecommendedEvents = 2, MaxRecommendedEvents = 4 }; var service = new ValidationService(config); var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(); var speech = EventDefinitionBuilder.Individual("Speech").AsRegionalEvent().AsOnSite().Build(); var coding = EventDefinitionBuilder.Individual("Coding").Build(); var student = StudentBuilder.Default() .WithRanking(flight, 1) .WithRanking(speech, 2) .WithRanking(coding, 3) .Build(); // Act var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(warnings, Is.Empty, "Student with valid rankings should have no warnings"); } [Test] public void ValidateStudent_MissingRegionalEvent_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { RequireRegionalEvent = true, RequireOnSiteActivity = false, RequireIndividualEvent = false, NoRegionalEventSeverity = ValidationSeverity.Warning }; var service = new ValidationService(config); var coding = EventDefinitionBuilder.Individual("Coding").Build(); // Not regional var student = StudentBuilder.Default() .WithRanking(coding, 1) .Build(); // Act var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(warnings, Has.Count.EqualTo(1)); Assert.That(warnings[0].Code, Is.EqualTo("NO_REGIONAL_EVENT")); Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning)); Assert.That(warnings[0].Context, Is.EqualTo(ValidationContext.StudentRanking)); } [Test] public void ValidateStudent_MissingOnSiteActivity_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { RequireOnSiteActivity = true, RequireRegionalEvent = false, NoOnSiteActivitySeverity = ValidationSeverity.Warning }; var service = new ValidationService(config); var flight = EventDefinitionBuilder.Individual("Flight").Build(); // Not on-site var student = StudentBuilder.Default() .WithRanking(flight, 1) .Build(); // Act var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(warnings, Has.Count.EqualTo(1)); Assert.That(warnings[0].Code, Is.EqualTo("NO_ONSITE_ACTIVITY")); Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning)); } [Test] public void ValidateStudent_TooManyRegionalEvents_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { MaxRegionalEvents = 2, TooManyRegionalEventsSeverity = ValidationSeverity.Warning, RequireRegionalEvent = false, RequireOnSiteActivity = false }; var service = new ValidationService(config); var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(); var coding = EventDefinitionBuilder.Individual("Coding").AsRegionalEvent().Build(); var essays = EventDefinitionBuilder.Individual("Essays").AsRegionalEvent().Build(); var student = StudentBuilder.Default() .WithRanking(flight, 1) .WithRanking(coding, 2) .WithRanking(essays, 3) .Build(); // Act var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(warnings, Has.Count.EqualTo(1)); Assert.That(warnings[0].Code, Is.EqualTo("TOO_MANY_REGIONAL_EVENTS")); Assert.That(warnings[0].Message, Does.Contain("3 regional events")); Assert.That(warnings[0].Message, Does.Contain("max recommended: 2")); } [Test] public void ValidateStudentAssignment_ValidAssignment_ReturnsNoWarnings() { // Arrange var config = new ValidationConfiguration { RequireRegionalEvent = true, RequireOnSiteActivity = true, MinRecommendedEvents = 2, MaxRecommendedEvents = 4 }; var service = new ValidationService(config); var flight = EventDefinitionBuilder.Individual("Flight").AsRegionalEvent().Build(); var speech = EventDefinitionBuilder.Individual("Speech").AsOnSite().Build(); var coding = EventDefinitionBuilder.Individual("Coding").Build(); var student = StudentBuilder.Default().Build(); var stats = new StudentEventStatistics { Student = student, Events = new List { flight, speech, coding } }; // Act var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment); // Assert Assert.That(warnings, Is.Empty); } [Test] public void ValidateStudentAssignment_TooFewEvents_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { MinRecommendedEvents = 3, EventCountSeverity = ValidationSeverity.Warning, RequireRegionalEvent = false, RequireOnSiteActivity = false }; var service = new ValidationService(config); var flight = EventDefinitionBuilder.Individual("Flight").Build(); var student = StudentBuilder.Default().Build(); var stats = new StudentEventStatistics { Student = student, Events = new List { flight } }; // Act var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment); // Assert Assert.That(warnings, Has.Count.GreaterThan(0)); var warning = warnings.FirstOrDefault(w => w.Code == "TOO_FEW_EVENTS"); Assert.That(warning, Is.Not.Null); Assert.That(warning!.Severity, Is.EqualTo(ValidationSeverity.Warning)); } [Test] public void ValidateStudentAssignment_TooManyEvents_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { MaxRecommendedEvents = 3, EventCountSeverity = ValidationSeverity.Warning, RequireRegionalEvent = false, RequireOnSiteActivity = false }; var service = new ValidationService(config); var student = StudentBuilder.Default().Build(); var events = new List(); for (int i = 0; i < 5; i++) { events.Add(EventDefinitionBuilder.Individual($"Event{i}").Build()); } var stats = new StudentEventStatistics { Student = student, Events = events }; // Act var warnings = service.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment); // Assert var warning = warnings.FirstOrDefault(w => w.Code == "TOO_MANY_EVENTS"); Assert.That(warning, Is.Not.Null); Assert.That(warning!.Message, Does.Contain("5 events")); } [Test] public void ValidateTeam_ValidTeam_ReturnsNoWarnings() { // Arrange var config = new ValidationConfiguration { RequireTeamCaptain = true }; var service = new ValidationService(config); var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build(); var team = TeamBuilder.Default() .ForEvent(robotics) .WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build(), isCaptain: true) .WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build()) .Build(); // Act var warnings = service.ValidateTeam(team); // Assert Assert.That(warnings, Is.Empty); } [Test] public void ValidateTeam_MissingCaptain_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { RequireTeamCaptain = true, MissingCaptainSeverity = ValidationSeverity.Warning }; var service = new ValidationService(config); var robotics = EventDefinitionBuilder.Team("Robotics", 2, 4).Build(); var team = TeamBuilder.Default() .ForEvent(robotics) .WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build()) .WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build()) .Build(); // Act var warnings = service.ValidateTeam(team); // Assert Assert.That(warnings, Has.Count.EqualTo(1)); Assert.That(warnings[0].Code, Is.EqualTo("MISSING_CAPTAIN")); Assert.That(warnings[0].Severity, Is.EqualTo(ValidationSeverity.Warning)); } [Test] public void ValidateTeam_TeamTooSmall_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning, RequireTeamCaptain = false }; var service = new ValidationService(config); var robotics = EventDefinitionBuilder.Team("Robotics", 3, 5).Build(); var team = TeamBuilder.Default() .ForEvent(robotics) .WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build()) .Build(); // Act var warnings = service.ValidateTeam(team); // Assert var warning = warnings.FirstOrDefault(w => w.Code == "TEAM_SIZE_TOO_SMALL"); Assert.That(warning, Is.Not.Null); Assert.That(warning!.Message, Does.Contain("1 members")); Assert.That(warning.Message, Does.Contain("min: 3")); } [Test] public void ValidateTeam_TeamTooLarge_ReturnsWarning() { // Arrange var config = new ValidationConfiguration { TeamSizeSeverity = ValidationSeverity.Warning, RequireTeamCaptain = false }; var service = new ValidationService(config); var robotics = EventDefinitionBuilder.Team("Robotics", 2, 3).Build(); var team = TeamBuilder.Default() .ForEvent(robotics) .WithStudent(StudentBuilder.Default().WithName("Alice", "A").Build()) .WithStudent(StudentBuilder.Default().WithName("Bob", "B").Build()) .WithStudent(StudentBuilder.Default().WithName("Carol", "C").Build()) .WithStudent(StudentBuilder.Default().WithName("David", "D").Build()) .Build(); // Act var warnings = service.ValidateTeam(team); // Assert var warning = warnings.FirstOrDefault(w => w.Code == "TEAM_SIZE_TOO_LARGE"); Assert.That(warning, Is.Not.Null); Assert.That(warning!.Message, Does.Contain("4 members")); Assert.That(warning.Message, Does.Contain("max: 3")); } [Test] public void ValidationService_ConfigurationChanges_AffectResults() { // Arrange var student = StudentBuilder.Default() .WithRanking(EventDefinitionBuilder.Individual("Flight").Build(), 1) .Build(); // Strict config var strictConfig = new ValidationConfiguration { RequireRegionalEvent = true, RequireOnSiteActivity = false, RequireIndividualEvent = false, NoRegionalEventSeverity = ValidationSeverity.Error }; var strictService = new ValidationService(strictConfig); // Lenient config var lenientConfig = new ValidationConfiguration { RequireRegionalEvent = false, RequireOnSiteActivity = false, RequireIndividualEvent = false }; var lenientService = new ValidationService(lenientConfig); // Act var strictWarnings = strictService.ValidateStudentRankings(student, ValidationContext.StudentRanking); var lenientWarnings = lenientService.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(strictWarnings, Has.Count.EqualTo(1)); Assert.That(strictWarnings[0].Severity, Is.EqualTo(ValidationSeverity.Error)); Assert.That(lenientWarnings, Is.Empty, "Lenient config should not require regional event"); } [Test] public void ValidationWarning_ContainsMetadata() { // Arrange var config = new ValidationConfiguration { RequireRegionalEvent = true, RequireOnSiteActivity = false }; var service = new ValidationService(config); var student = StudentBuilder.Default() .WithName("Alice", "Anderson") .WithRanking(EventDefinitionBuilder.Individual("Coding").Build(), 1) .Build(); // Act var warnings = service.ValidateStudentRankings(student, ValidationContext.StudentRanking); // Assert Assert.That(warnings, Has.Count.EqualTo(1)); var warning = warnings[0]; Assert.That(warning.Metadata, Contains.Key("StudentId")); Assert.That(warning.Metadata, Contains.Key("StudentName")); Assert.That(warning.Metadata["StudentName"], Is.EqualTo("Alice Anderson")); } }