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 Constants private const int MaxLookbackLines = 20; private const int TopUnmatchedPatternsCount = 10; private const int SampleIssuesCount = 5; private const int SectionTestStartLineIndex = 63; // Line 64 (1-based) = index 63 (0-based) private const int SectionTestLineCount = 29; // Lines 64-92 inclusive #endregion #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) return false; // Look backwards to find the most recent section header for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - MaxLookbackLines; i--) { if (i >= fileLines.Count) continue; 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 = SampleIssuesCount) { return issues .Take(count) .Select(i => $" Line {i.LineNumber}: {i.LineContent}") .ToList(); } /// /// Writes special events summary to console. /// private static void WriteSpecialEventsSummary(IDictionary> occurrences) { Console.WriteLine($"\n--- Special Events Found ---"); if (occurrences.TryGetValue(EventDefinition.GeneralSchedule, out var gs)) Console.WriteLine($" GeneralSchedule: {gs.Count} occurrences"); if (occurrences.TryGetValue(EventDefinition.MeetTheCandidates, out var mtc)) Console.WriteLine($" MeetTheCandidates: {mtc.Count} occurrences"); if (occurrences.TryGetValue(EventDefinition.ChapterOfficerMeeting, out var com)) Console.WriteLine($" ChapterOfficerMeeting: {com.Count} occurrences"); if (occurrences.TryGetValue(EventDefinition.VotingDelegateMeeting, out var vdm)) Console.WriteLine($" VotingDelegateMeeting: {vdm.Count} occurrences"); } /// /// Writes issue breakdown by type to console. /// private static void WriteIssueBreakdown( Dictionary> issuesByType, Dictionary expectedByType, Dictionary fixableByType, List fixableIssues) { 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, SampleIssuesCount); Console.WriteLine($" Sample fixable issues:"); foreach (var sample in samples) { Console.WriteLine(sample); } } } } /// /// Writes unmatched line analysis to console. /// private static void WriteUnmatchedLineAnalysis(List fixableIssues) { var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList(); if (!unmatchedLines.Any()) return; Console.WriteLine($"\n--- Unmatched Line Analysis ---"); var unmatchedPatterns = unmatchedLines .GroupBy(i => i.LineContent.Trim()) .OrderByDescending(g => g.Count()) .Take(TopUnmatchedPatternsCount); Console.WriteLine($"Top unmatched line formats:"); foreach (var pattern in unmatchedPatterns) { Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)"); } } /// /// Analyzes and reports parsing results for a file. /// private static void AnalyzeParsingResults( EventOccurrenceParserResult result, FileInfo fileInfo, string title) { // 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=== {title} ==="); 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}"); WriteSpecialEventsSummary(result.Occurrences); WriteIssueBreakdown(issuesByType, expectedByType, fixableByType, fixableIssues); WriteUnmatchedLineAnalysis(fixableIssues); } /// /// 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); } /// /// Writes special events to console output. /// private static void WriteSpecialEvents(IDictionary> occurrences) { Console.WriteLine("General Schedule"); if (occurrences.TryGetValue(EventDefinition.GeneralSchedule, out var generalSchedule)) { foreach (var eo in generalSchedule.OrderBy(occurrence => occurrence.StartTime)) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Meet the Candidates"); if (occurrences.TryGetValue(EventDefinition.MeetTheCandidates, out var meetTheCandidates)) { foreach (var eo in meetTheCandidates) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Chapter Officer Meeting"); if (occurrences.TryGetValue(EventDefinition.ChapterOfficerMeeting, out var chapterOfficerMeeting)) { foreach (var eo in chapterOfficerMeeting) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } Console.WriteLine("Voting Delegate Meeting"); if (occurrences.TryGetValue(EventDefinition.VotingDelegateMeeting, out var votingDelegateMeeting)) { foreach (var eo in votingDelegateMeeting) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } } #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.TryGetValue(@event, out var eventOccurrences)) { Console.WriteLine($"!!! eventDefinition not found {@event.Name}"); continue; } foreach (var eo in eventOccurrences) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } WriteSpecialEvents(dictionary); 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.TryGetValue(@event, out var eventOccurrences)) { Console.WriteLine($"!!! eventDefinition not found {@event.Name}"); continue; } foreach (var eo in eventOccurrences) { Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); } } WriteSpecialEvents(dictionary); Assert.Pass(); } [Test] public void Analyze_2025Nationals_ParsingResults() { // Arrange var events = TestEntityHandler.GetEvents(); var fileInfo = TestEntityHandler.GetEventOccurrenceNationalsFileInfo(); var parser = new EventOccurrenceParser(fileInfo, events); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); AnalyzeParsingResults(result, fileInfo, "2025 TSA Nationals Competition Event Times Analysis"); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines); var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // 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 parser = new EventOccurrenceParser(fileInfo, events); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); AnalyzeParsingResults(result, fileInfo, "2025 TN TSA State Competition Event Times Analysis"); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines); var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // 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 parser = new EventOccurrenceParser(fileInfo, events); // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); AnalyzeParsingResults(result, fileInfo, "2024 TN TSA State Competition Event Times Analysis"); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines); var totalParsed = result.Occurrences.Values.Sum(list => list.Count); // Test passes if no exceptions were thrown Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); } [Test] public void Parse_Section_Lines64To92_ChildrensStoriesToConstructionChallenge() { // Arrange // Extract lines 64-92 from the test file - contains MS and HS events with various formats var allLines = File.ReadAllLines(TestEntityHandler.GetEventOccurrenceStateFileInfo().FullName); var sectionLines = allLines.Skip(SectionTestStartLineIndex).Take(SectionTestLineCount).ToArray(); // Lines 64-92 (0-indexed: 63-91) var sectionContent = string.Join("\n", sectionLines); var tempFile = EventOccurrenceParserTestHelpers.CreateTempFile(sectionContent); var events = TestEntityHandler.GetEvents(); var parser = new EventOccurrenceParser(tempFile, events); try { // Act var result = parser.Parse(); // Assert - Should parse without exceptions Assert.That(result, Is.Not.Null, "Parser should return a result"); // Count occurrences by event type var totalOccurrences = result.Occurrences.Values.Sum(list => list.Count); // Verify MS events are parsed var childrensStories = events.FirstOrDefault(e => e.Name.Contains("Children's Stories", StringComparison.OrdinalIgnoreCase)); var coding = events.FirstOrDefault(e => e.Name == "Coding"); var communityServiceVideo = events.FirstOrDefault(e => e.Name.Contains("Community Service Video", StringComparison.OrdinalIgnoreCase)); var constructionChallenge = events.FirstOrDefault(e => e.Name.Contains("Construction Challenge", StringComparison.OrdinalIgnoreCase)); // Count expected MS occurrences: // Children's Stories – MS: 5 occurrences (lines 65-69) // Coding – MS: 2 occurrences (lines 76-77) // Community Service Video – MS: 4 occurrences (lines 79-82) // Construction Challenge – MS: 5 occurrences (lines 88-92) // Total expected MS occurrences: 16 var msEventCount = 0; if (childrensStories != null && result.Occurrences.TryGetValue(childrensStories, out var csOccurrences)) msEventCount += csOccurrences.Count; if (coding != null && result.Occurrences.TryGetValue(coding, out var codingOccurrences)) msEventCount += codingOccurrences.Count; if (communityServiceVideo != null && result.Occurrences.TryGetValue(communityServiceVideo, out var csvOccurrences)) msEventCount += csvOccurrences.Count; if (constructionChallenge != null && result.Occurrences.TryGetValue(constructionChallenge, out var ccOccurrences)) msEventCount += ccOccurrences.Count; // When no school level is set, HS events should be processed (not skipped) // Verify HS events are processed or handled appropriately var hsIssues = result.Issues.Where(i => i.LineContent.Contains("Coding – HS") || i.LineContent.Contains("CAD") && i.LineContent.Contains("HS") || i.LineNumber >= 72 && i.LineNumber <= 86 && IsHighSchoolEvent(i.LineContent) ).ToList(); // Verify HS section headers are NOT tracked in SkippedSectionHeaders when no school level is set var skippedHeaders = result.SkippedSectionHeaders; // Verify continuation lines are skipped // Line 70 starts with "*The" - this enters continuation mode and both line 70 and 71 should be skipped var continuationLineIssues = result.Issues.Where(i => i.LineContent.Contains("books of semifinalist teams") || i.LineContent.Contains("be returned to teams") ).ToList(); // Verify specific time formats are parsed correctly var noonOccurrence = result.Occurrences.Values .SelectMany(list => list) .FirstOrDefault(eo => eo.Time.Contains("NOON", StringComparison.OrdinalIgnoreCase)); var lateTimeOccurrence = result.Occurrences.Values .SelectMany(list => list) .FirstOrDefault(eo => eo.Time.Contains("11:59", StringComparison.OrdinalIgnoreCase)); // Output detailed analysis Console.WriteLine($"\n=== Section Lines 64-92 Parsing Results ==="); Console.WriteLine($"Total occurrences parsed: {totalOccurrences}"); Console.WriteLine($"MS event occurrences: {msEventCount}"); Console.WriteLine($"Total issues: {result.Issues.Count}"); Console.WriteLine($"HS-related issues: {hsIssues.Count}"); Console.WriteLine($"Skipped section headers: {skippedHeaders.Count}"); Console.WriteLine($"Continuation line issues: {continuationLineIssues.Count}"); Console.WriteLine($"\n--- Issue Types ---"); foreach (var issueType in result.Issues.GroupBy(i => i.IssueType)) { Console.WriteLine($" {issueType.Key}: {issueType.Count()}"); } // Assertions Assert.That(totalOccurrences, Is.GreaterThan(0), "Should parse at least some occurrences"); Assert.That(msEventCount, Is.GreaterThanOrEqualTo(14), "Should parse most MS occurrences (at least 14 out of 16)"); // When no school level is set, HS events should be processed (not skipped) // HS events may create issues if they don't match event definitions, which is expected // HS section headers should NOT be tracked when no school level is set Assert.That(skippedHeaders, Has.Count.EqualTo(0), "Section headers should NOT be tracked when no school level is set"); // Line 70 (starts with "*The") enters continuation mode and both line 70 and 71 should be skipped without issues Assert.That(continuationLineIssues, Has.Count.EqualTo(0), "Continuation lines starting with '*' and subsequent lines should be skipped without issues"); Assert.That(noonOccurrence, Is.Not.Null, "Should parse NOON time format"); Assert.That(lateTimeOccurrence, Is.Not.Null, "Should parse 11:59 p.m. time format"); // Verify specific locations are parsed if (childrensStories != null && result.Occurrences.TryGetValue(childrensStories, out var childrensStoriesOccurrences)) { var locations = childrensStoriesOccurrences .Select(eo => eo.Location) .Where(loc => !string.IsNullOrWhiteSpace(loc)) .ToList(); Assert.That(locations, Has.Count.GreaterThan(0), "Children's Stories should have locations parsed"); } // Test passes with detailed information Assert.Pass($"Successfully parsed section: {totalOccurrences} occurrences, {result.Issues.Count} issues, {msEventCount} MS events"); } finally { EventOccurrenceParserTestHelpers.CleanupTempFile(tempFile); } } [Test] public void Parse_BiotechnologyMSAndHS_HSOccurrencesNotAssociatedWithMS() { // Arrange // This test verifies that HS events (like "Biotechnology Design – HS") are not incorrectly // associated with MS events (like "Biotechnology – MS") even if fuzzy matching finds a match var testContent = "Biotechnology – MS\n" + "Submit Entry April 3 8 a.m. – 9 a.m. Exhibit Hall C\n" + "Judging April 3 9 a.m. – 5 p.m. Exhibit Hall C\n" + "Biotechnology Design – HS\n" + "Submit Entry April 3 8 a.m. – 9:00 a.m. Exhibit Hall C\n" + "Judging April 3 9 a.m. – 5 p.m. Exhibit Hall C\n" + "Pick-up April 4 5 p.m. – 5:30 p.m. Exhibit Hall C"; var tempFile = EventOccurrenceParserTestHelpers.CreateTempFile(testContent); var events = new[] { EventOccurrenceParserTestHelpers.CreateTestEvent("Biotechnology") }; var parser = new EventOccurrenceParser(tempFile, events); try { // Act var result = parser.Parse(); // Assert var biotechnology = events.FirstOrDefault(e => e.Name == "Biotechnology"); Assert.That(biotechnology, Is.Not.Null, "Biotechnology event should exist"); // When no school level is set, all events (MS and HS) should be processed // HS section header should NOT be skipped (note: normalized to regular hyphen) Assert.That(result.SkippedSectionHeaders, Does.Not.Contain("Biotechnology Design - HS"), "HS section header should NOT be in SkippedSectionHeaders when no school level is set"); // With no school level filtering, both MS and HS events are processed if (result.Occurrences.TryGetValue(biotechnology, out var allOccurrences)) { // With no school level set, we process all occurrences (both MS and HS) // Expected: 2 MS occurrences (Submit Entry, Judging) + 3 HS occurrences (Submit Entry, Judging, Pick-up) = 5 total Assert.That(allOccurrences, Has.Count.EqualTo(5), "Should have all 5 occurrences (2 MS + 3 HS) when no school level is set. " + $"Found {allOccurrences.Count} occurrences total."); // Verify all expected occurrence names are present var occurrenceNames = allOccurrences.Select(o => o.Name).ToList(); Assert.That(occurrenceNames, Does.Contain("Submit Entry"), "Should have Submit Entry occurrences"); Assert.That(occurrenceNames, Does.Contain("Judging"), "Should have Judging occurrences"); Assert.That(occurrenceNames, Does.Contain("Pick-up"), "Should have Pick-up occurrence"); } Assert.Pass("All events processed when no school level is set"); } finally { EventOccurrenceParserTestHelpers.CleanupTempFile(tempFile); } } }