using Core.Entities; using Core.Models; using Core.Parsers; namespace Tests.Parsers; /// /// Integration tests for EventOccurrenceParser using real test data files. /// public class EventOccurrenceParser_Tests { #region Helper Methods /// /// Checks if a line contains a High School event marker. /// private static bool IsHighSchoolEvent(string line) { return line.Contains("– HS", StringComparison.OrdinalIgnoreCase) || line.Contains(" - HS", StringComparison.OrdinalIgnoreCase) || line.Contains("- HS", StringComparison.OrdinalIgnoreCase); } /// /// Checks if a line contains a Middle School event marker. /// private static bool IsMiddleSchoolEvent(string line) { return line.Contains("– MS", StringComparison.OrdinalIgnoreCase) || line.Contains(" - MS", StringComparison.OrdinalIgnoreCase) || line.Contains("- MS", StringComparison.OrdinalIgnoreCase); } /// /// Determines if an issue is expected (HS-related) or fixable. /// private static bool IsExpectedIssue(ParsingIssue issue, List fileLines, int currentLineIndex) { // Check if the issue line itself is an HS event header if (IsHighSchoolEvent(issue.LineContent)) return true; // For MissingEventDefinition issues, check if we're in an HS section if (issue.IssueType == ParsingIssueType.MissingEventDefinition) { // Look backwards to find the most recent section header for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - 20; i--) { if (i < fileLines.Count) { var line = fileLines[i].Trim(); if (IsHighSchoolEvent(line)) return true; if (IsMiddleSchoolEvent(line)) return false; // Found MS section, so this is fixable } } } return false; } /// /// Categorizes issues into expected (HS-related) and fixable. /// private static (List Expected, List Fixable) CategorizeIssues( List issues, List fileLines) { var expected = new List(); var fixable = new List(); foreach (var issue in issues) { var lineIndex = issue.LineNumber - 1; // Convert to 0-based index if (IsExpectedIssue(issue, fileLines, lineIndex)) { expected.Add(issue); } else { fixable.Add(issue); } } return (expected, fixable); } /// /// Gets sample lines for a list of issues. /// private static List GetSampleLines(List issues, int count = 5) { return issues .Take(count) .Select(i => $" Line {i.LineNumber}: {i.LineContent}") .ToList(); } /// /// Analyzes location parsing failures and extracts common patterns. /// private static Dictionary AnalyzeLocationFailures( List locationIssues, List fileLines) { var locationPatterns = new Dictionary(); foreach (var issue in locationIssues) { // Try to extract the location part from the line // The format is typically: "EventName Month Day Time Location" var parts = issue.LineContent.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Look for location-like strings (usually after time) // This is a heuristic - we'll look for parts that don't match date/time patterns var timeRegex = new System.Text.RegularExpressions.Regex( @"\d{1,2}:?\d{0,2}\s*[AaPp]\.?[Mm]\.?|NOON"); bool foundTime = false; var locationParts = new List(); foreach (var part in parts) { if (timeRegex.IsMatch(part) || part == "NOON") { foundTime = true; continue; } if (foundTime && !string.IsNullOrWhiteSpace(part)) { locationParts.Add(part); } } if (locationParts.Any()) { var location = string.Join(" ", locationParts).Trim(); if (!string.IsNullOrWhiteSpace(location)) { locationPatterns.TryGetValue(location, out var count); locationPatterns[location] = count + 1; } } } return locationPatterns; } /// /// Counts HS vs MS event sections in the file. /// private static (int HighSchool, int MiddleSchool) CountEventSections(List fileLines) { int hsCount = 0; int msCount = 0; foreach (var line in fileLines) { var trimmed = line.Trim(); if (IsHighSchoolEvent(trimmed)) hsCount++; else if (IsMiddleSchoolEvent(trimmed)) msCount++; } return (hsCount, msCount); } #endregion [Test] public void ParseNationalsTest() { var events = TestEntityHandler.GetEvents(); var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceNationalsFileInfo(), events); var result = parser.Parse(); var dictionary = result.Occurrences; Console.WriteLine($"Occurrence, Month, Date, Time, Location"); foreach (var @event in events) { Console.WriteLine($"{@event.Name}"); if (!dictionary.ContainsKey(@event)) { Console.WriteLine("!!! eventDefinition not found " + @event.Name); continue; } var eventOccurrences = dictionary[@event]; foreach (var eo in eventOccurrences) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("General Schedule"); if (dictionary.ContainsKey(EventDefinition.GeneralSchedule)) { foreach (var eo in dictionary[EventDefinition.GeneralSchedule].OrderBy(occurrence => occurrence.StartTime)) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Meet the Candidates"); if (dictionary.ContainsKey(EventDefinition.MeetTheCandidates)) { foreach (var eo in dictionary[EventDefinition.MeetTheCandidates]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Chapter Officer Meeting"); if (dictionary.ContainsKey(EventDefinition.ChapterOfficerMeeting)) { foreach (var eo in dictionary[EventDefinition.ChapterOfficerMeeting]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Voting Delegate Meeting"); if (dictionary.ContainsKey(EventDefinition.VotingDelegateMeeting)) { foreach (var eo in dictionary[EventDefinition.VotingDelegateMeeting]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Assert.Pass(); } [Test] public void ParseStatesTest() { var events = TestEntityHandler.GetEvents(); var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceStateFileInfo(), events); var result = parser.Parse(); var dictionary = result.Occurrences; Console.WriteLine($"Occurrence, Month, Date, Time, Location"); foreach (var @event in events) { Console.WriteLine($"{@event.Name}"); if (!dictionary.ContainsKey(@event)) { Console.WriteLine("!!! eventDefinition not found " + @event.Name); continue; } var eventOccurrences = dictionary[@event]; foreach (var eo in eventOccurrences) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("General Schedule"); if (dictionary.ContainsKey(EventDefinition.GeneralSchedule)) { foreach (var eo in dictionary[EventDefinition.GeneralSchedule].OrderBy(occurrence => occurrence.StartTime)) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Meet the Candidates"); if (dictionary.ContainsKey(EventDefinition.MeetTheCandidates)) { foreach (var eo in dictionary[EventDefinition.MeetTheCandidates]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Chapter Officer Meeting"); if (dictionary.ContainsKey(EventDefinition.ChapterOfficerMeeting)) { foreach (var eo in dictionary[EventDefinition.ChapterOfficerMeeting]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Voting Delegate Meeting"); if (dictionary.ContainsKey(EventDefinition.VotingDelegateMeeting)) { foreach (var eo in dictionary[EventDefinition.VotingDelegateMeeting]) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Assert.Pass(); } [Test] public void Analyze_2025Nationals_ParsingResults() { // Arrange var events = TestEntityHandler.GetEvents(); var fileInfo = TestEntityHandler.GetEventOccurrenceNationalsFileInfo(); var locationConfig = LocationParsingConfiguration.Default; var parser = new EventOccurrenceParser(fileInfo, events, locationConfig); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); // Load file lines for analysis var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var totalLines = fileLines.Count; var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // Categorize issues var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines); // Count event sections var (hsSections, msSections) = CountEventSections(fileLines); // Group issues by type var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList()); var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); // Output analysis Console.WriteLine($"\n=== 2025 TSA Nationals Competition Event Times Analysis ==="); Console.WriteLine($"\n--- Summary Statistics ---"); Console.WriteLine($"Total lines in file: {totalLines}"); Console.WriteLine($"Total occurrences parsed: {totalParsed}"); Console.WriteLine($"Total issues found: {result.Issues.Count}"); Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}"); Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}"); Console.WriteLine($"\n--- Special Events Found ---"); if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule)) Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates)) Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting)) Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting)) Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences"); Console.WriteLine($"\n--- Issue Breakdown by Type ---"); foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count)) { var issueType = kvp.Key; var allIssues = kvp.Value; expectedByType.TryGetValue(issueType, out var expectedCount); fixableByType.TryGetValue(issueType, out var fixableCount); Console.WriteLine($"\n{issueType}:"); Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})"); if (fixableCount > 0) { var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList(); var samples = GetSampleLines(fixableOfType, 5); Console.WriteLine($" Sample fixable issues:"); foreach (var sample in samples) { Console.WriteLine(sample); } } } // Pattern Analysis var locationFailures = fixableIssues.Where(i => i.IssueType == ParsingIssueType.LocationParseFailure).ToList(); if (locationFailures.Any()) { Console.WriteLine($"\n--- Location Parse Failure Analysis ---"); var locationPatterns = AnalyzeLocationFailures(locationFailures, fileLines); var topLocations = locationPatterns.OrderByDescending(x => x.Value).Take(10); Console.WriteLine($"Top unmatched location strings:"); foreach (var loc in topLocations) { Console.WriteLine($" \"{loc.Key}\" (appears {loc.Value} times)"); } } var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList(); if (unmatchedLines.Any()) { Console.WriteLine($"\n--- Unmatched Line Analysis ---"); var unmatchedPatterns = unmatchedLines .GroupBy(i => i.LineContent.Trim()) .OrderByDescending(g => g.Count()) .Take(10); Console.WriteLine($"Top unmatched line formats:"); foreach (var pattern in unmatchedPatterns) { Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)"); } } // Test passes if no exceptions were thrown Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); } [Test] public void Analyze_2025State_ParsingResults() { // Arrange var events = TestEntityHandler.GetEvents(); var fileInfo = TestEntityHandler.GetEventOccurrenceStateFileInfo(); var locationConfig = LocationParsingConfiguration.Default; var parser = new EventOccurrenceParser(fileInfo, events, locationConfig); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); // Load file lines for analysis var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var totalLines = fileLines.Count; var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // Categorize issues var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines); // Count event sections var (hsSections, msSections) = CountEventSections(fileLines); // Group issues by type var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList()); var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); // Output analysis Console.WriteLine($"\n=== 2025 TN TSA State Competition Event Times Analysis ==="); Console.WriteLine($"\n--- Summary Statistics ---"); Console.WriteLine($"Total lines in file: {totalLines}"); Console.WriteLine($"Total occurrences parsed: {totalParsed}"); Console.WriteLine($"Total issues found: {result.Issues.Count}"); Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}"); Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}"); Console.WriteLine($"\n--- Special Events Found ---"); if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule)) Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates)) Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting)) Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting)) Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences"); Console.WriteLine($"\n--- Issue Breakdown by Type ---"); foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count)) { var issueType = kvp.Key; var allIssues = kvp.Value; expectedByType.TryGetValue(issueType, out var expectedCount); fixableByType.TryGetValue(issueType, out var fixableCount); Console.WriteLine($"\n{issueType}:"); Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})"); if (fixableCount > 0) { var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList(); var samples = GetSampleLines(fixableOfType, 5); Console.WriteLine($" Sample fixable issues:"); foreach (var sample in samples) { Console.WriteLine(sample); } } } // Pattern Analysis var locationFailures = fixableIssues.Where(i => i.IssueType == ParsingIssueType.LocationParseFailure).ToList(); if (locationFailures.Any()) { Console.WriteLine($"\n--- Location Parse Failure Analysis ---"); var locationPatterns = AnalyzeLocationFailures(locationFailures, fileLines); var topLocations = locationPatterns.OrderByDescending(x => x.Value).Take(10); Console.WriteLine($"Top unmatched location strings:"); foreach (var loc in topLocations) { Console.WriteLine($" \"{loc.Key}\" (appears {loc.Value} times)"); } } var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList(); if (unmatchedLines.Any()) { Console.WriteLine($"\n--- Unmatched Line Analysis ---"); var unmatchedPatterns = unmatchedLines .GroupBy(i => i.LineContent.Trim()) .OrderByDescending(g => g.Count()) .Take(10); Console.WriteLine($"Top unmatched line formats:"); foreach (var pattern in unmatchedPatterns) { Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)"); } } // Test passes if no exceptions were thrown Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); } [Test] public void Analyze_2024State_ParsingResults() { // Arrange var events = TestEntityHandler.GetEvents(); var fileInfo = TestEntityHandler.GetEventOccurrenceState2024FileInfo(); var locationConfig = LocationParsingConfiguration.Default; var parser = new EventOccurrenceParser(fileInfo, events, locationConfig); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); // Load file lines for analysis var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var totalLines = fileLines.Count; var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // Categorize issues var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines); // Count event sections var (hsSections, msSections) = CountEventSections(fileLines); // Group issues by type var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList()); var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count()); // Output analysis Console.WriteLine($"\n=== 2024 TN TSA State Competition Event Times Analysis ==="); Console.WriteLine($"\n--- Summary Statistics ---"); Console.WriteLine($"Total lines in file: {totalLines}"); Console.WriteLine($"Total occurrences parsed: {totalParsed}"); Console.WriteLine($"Total issues found: {result.Issues.Count}"); Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)"); Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}"); Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}"); Console.WriteLine($"\n--- Special Events Found ---"); if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule)) Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates)) Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting)) Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences"); if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting)) Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences"); Console.WriteLine($"\n--- Issue Breakdown by Type ---"); foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count)) { var issueType = kvp.Key; var allIssues = kvp.Value; expectedByType.TryGetValue(issueType, out var expectedCount); fixableByType.TryGetValue(issueType, out var fixableCount); Console.WriteLine($"\n{issueType}:"); Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})"); if (fixableCount > 0) { var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList(); var samples = GetSampleLines(fixableOfType, 5); Console.WriteLine($" Sample fixable issues:"); foreach (var sample in samples) { Console.WriteLine(sample); } } } // Pattern Analysis var locationFailures = fixableIssues.Where(i => i.IssueType == ParsingIssueType.LocationParseFailure).ToList(); if (locationFailures.Any()) { Console.WriteLine($"\n--- Location Parse Failure Analysis ---"); var locationPatterns = AnalyzeLocationFailures(locationFailures, fileLines); var topLocations = locationPatterns.OrderByDescending(x => x.Value).Take(10); Console.WriteLine($"Top unmatched location strings:"); foreach (var loc in topLocations) { Console.WriteLine($" \"{loc.Key}\" (appears {loc.Value} times)"); } } var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList(); if (unmatchedLines.Any()) { Console.WriteLine($"\n--- Unmatched Line Analysis ---"); var unmatchedPatterns = unmatchedLines .GroupBy(i => i.LineContent.Trim()) .OrderByDescending(g => g.Count()) .Take(10); Console.WriteLine($"Top unmatched line formats:"); foreach (var pattern in unmatchedPatterns) { Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)"); } } // Test passes if no exceptions were thrown Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); } }