< Summary

Information
Class: Orchestrator.Commands.Operations.CollectContext.CollectContextKicktippCommand
Assembly: Orchestrator
File(s): /home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/CollectContext/CollectContextKicktippCommand.cs
Line coverage
87%
Covered lines: 139
Uncovered lines: 19
Coverable lines: 158
Total lines: 324
Line coverage: 87.9%
Branch coverage
84%
Covered branches: 59
Total branches: 70
Branch coverage: 84.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ExecuteAsync()100%11100%
ExecuteWithSettingsAsync()87.5%8894.12%
ExecuteKicktippContextCollection()92.86%424294.68%
IsHistoryDocument(...)100%44100%
ParseMatchdays(...)80%101088.89%
PrintOutcomeCollectionSummary(...)16.67%21625%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/Orchestrator/Commands/Operations/CollectContext/CollectContextKicktippCommand.cs

#LineLine coverage
 1using System.Globalization;
 2using Microsoft.Extensions.Logging;
 3using Spectre.Console.Cli;
 4using Spectre.Console;
 5using EHonda.KicktippAi.Core;
 6using Orchestrator.Infrastructure;
 7using Orchestrator.Infrastructure.Factories;
 8using Orchestrator.Services;
 9
 10namespace Orchestrator.Commands.Operations.CollectContext;
 11
 12/// <summary>
 13/// Command for collecting Kicktipp context documents and storing them in the database.
 14/// </summary>
 15public class CollectContextKicktippCommand : AsyncCommand<CollectContextKicktippSettings>
 16{
 17    private readonly IAnsiConsole _console;
 18    private readonly IFirebaseServiceFactory _firebaseServiceFactory;
 19    private readonly IKicktippClientFactory _kicktippClientFactory;
 20    private readonly IContextProviderFactory _contextProviderFactory;
 21    private readonly MatchOutcomeCollectionService _matchOutcomeCollectionService;
 22    private readonly TimeProvider _timeProvider;
 23    private readonly ILogger<CollectContextKicktippCommand> _logger;
 24
 125    public CollectContextKicktippCommand(
 126        IAnsiConsole console,
 127        IFirebaseServiceFactory firebaseServiceFactory,
 128        IKicktippClientFactory kicktippClientFactory,
 129        IContextProviderFactory contextProviderFactory,
 130        MatchOutcomeCollectionService matchOutcomeCollectionService,
 131        TimeProvider timeProvider,
 132        ILogger<CollectContextKicktippCommand> logger)
 33    {
 134        _console = console;
 135        _firebaseServiceFactory = firebaseServiceFactory;
 136        _kicktippClientFactory = kicktippClientFactory;
 137        _contextProviderFactory = contextProviderFactory;
 138        _matchOutcomeCollectionService = matchOutcomeCollectionService;
 139        _timeProvider = timeProvider;
 140        _logger = logger;
 141    }
 42
 43    protected override async Task<int> ExecuteAsync(CommandContext context, CollectContextKicktippSettings settings, Can
 44    {
 145        return await ExecuteWithSettingsAsync(settings, cancellationToken);
 146    }
 47
 48    internal async Task<int> ExecuteWithSettingsAsync(CollectContextKicktippSettings settings, CancellationToken cancell
 49    {
 50        try
 51        {
 52            // Validate settings
 153            if (string.IsNullOrWhiteSpace(settings.CommunityContext))
 54            {
 155                _console.MarkupLine("[red]Error: Community context is required[/]");
 156                return 1;
 57            }
 58
 159            _console.MarkupLine($"[green]Collect-context kicktipp command initialized[/]");
 60
 161            if (settings.Verbose)
 62            {
 163                _console.MarkupLine("[dim]Verbose mode enabled[/]");
 64            }
 65
 166            if (settings.DryRun)
 67            {
 168                _console.MarkupLine("[magenta]Dry run mode enabled - no changes will be made to database[/]");
 69            }
 70
 171            if (settings.MatchOutcomesOnly)
 72            {
 073                _console.MarkupLine("[blue]Match outcomes only mode enabled - context documents will not be updated[/]")
 74            }
 75
 76            // Execute the context collection workflow
 177            await ExecuteKicktippContextCollection(settings, cancellationToken);
 78
 179            return 0;
 80        }
 181        catch (Exception ex)
 82        {
 183            _logger.LogError(ex, "Error executing collect-context kicktipp command");
 184            _console.MarkupLine($"[red]Error:[/] {ex.Message}");
 185            return 1;
 86        }
 187    }
 88
 89    private async Task ExecuteKicktippContextCollection(CollectContextKicktippSettings settings, CancellationToken cance
 90    {
 191        var competition = CompetitionResolver.ResolveCompetition(settings.Competition, settings.CommunityContext, settin
 192        var repositoryCompetition = CompetitionResolver.ToRepositoryCompetitionArgument(competition);
 93
 194        var outcomeCollectionResult = await _matchOutcomeCollectionService.CollectAsync(
 195            settings.CommunityContext,
 196            settings.DryRun,
 197            repositoryCompetition,
 198            cancellationToken);
 99
 1100        PrintOutcomeCollectionSummary(outcomeCollectionResult, settings);
 101
 1102        if (settings.MatchOutcomesOnly)
 103        {
 0104            var completionMessage = settings.DryRun
 0105                ? "[magenta]✓ Match outcome dry run completed[/]"
 0106                : "[green]✓ Match outcome collection completed![/]";
 0107            _console.MarkupLine(completionMessage);
 0108            return;
 109        }
 110
 111        // Create services using factories (factories handle env var loading)
 1112        var kicktippClient = _kicktippClientFactory.CreateClient();
 1113        var contextRepository = _firebaseServiceFactory.CreateContextRepository(repositoryCompetition);
 114
 1115        _console.MarkupLine($"[blue]Using community context:[/] [yellow]{settings.CommunityContext}[/]");
 1116        _console.MarkupLine($"[blue]Using competition:[/] [yellow]{competition}[/]");
 117
 1118        var requestedMatchdays = ParseMatchdays(settings.Matchdays);
 1119        var targetMatchdays = requestedMatchdays.Count > 0
 1120            ? requestedMatchdays.Select<int, int?>(matchday => matchday).ToList()
 1121            : new List<int?> { null };
 122
 123        // Collect all unique context documents for all matches
 1124        var allContextDocuments = new Dictionary<string, string>(); // documentName -> content
 125
 1126        foreach (var targetMatchday in targetMatchdays)
 127        {
 1128            var matchdayLabel = targetMatchday.HasValue ? $"matchday {targetMatchday.Value}" : "current matchday";
 129
 130            // Create context provider using factory
 1131            var contextProvider = _contextProviderFactory.CreateKicktippContextProvider(
 1132                kicktippClient,
 1133                settings.CommunityContext,
 1134                settings.CommunityContext,
 1135                repositoryCompetition,
 1136                targetMatchday);
 137
 1138            _console.MarkupLine($"[blue]Getting {matchdayLabel} matches...[/]");
 139
 140            // Step 1: Get target matchday matches
 1141            var matchesWithHistory = targetMatchday.HasValue
 1142                ? await kicktippClient.GetMatchesWithHistoryAsync(settings.CommunityContext, targetMatchday.Value)
 1143                : await kicktippClient.GetMatchesWithHistoryAsync(settings.CommunityContext);
 144
 1145            if (!matchesWithHistory.Any())
 146            {
 1147                _console.MarkupLine($"[yellow]No matches found for {matchdayLabel}[/]");
 1148                continue;
 149            }
 150
 1151            _console.MarkupLine($"[green]Found {matchesWithHistory.Count} matches for {matchdayLabel}[/]");
 152
 153            // Step 2: Collect all unique context documents for all matches
 1154            foreach (var matchWithHistory in matchesWithHistory)
 155            {
 1156                var match = matchWithHistory.Match;
 1157                _console.MarkupLine($"[cyan]Collecting context for:[/] {match.HomeTeam} vs {match.AwayTeam}");
 158
 159                try
 160                {
 161                    // Get context for this specific match
 1162                    await foreach (var contextDoc in contextProvider.GetMatchContextAsync(match.HomeTeam, match.AwayTeam
 163                    {
 164                        // Use the document name as key to avoid duplicates
 1165                        if (!allContextDocuments.ContainsKey(contextDoc.Name))
 166                        {
 1167                            allContextDocuments[contextDoc.Name] = contextDoc.Content;
 168
 1169                            if (settings.Verbose)
 170                            {
 1171                                _console.MarkupLine($"[dim]  Collected context document: {contextDoc.Name}[/]");
 172                            }
 173                        }
 174                    }
 1175                }
 1176                catch (Exception ex)
 177                {
 1178                    _logger.LogError(ex, "Failed to collect context for match {HomeTeam} vs {AwayTeam}", match.HomeTeam,
 1179                    _console.MarkupLine($"[red]  ✗ Failed to collect context: {ex.Message}[/]");
 1180                }
 1181            }
 1182        }
 183
 1184        if (!allContextDocuments.Any())
 185        {
 1186            return;
 187        }
 188
 1189        _console.MarkupLine($"[green]Collected {allContextDocuments.Count} unique context documents[/]");
 190
 191        // Step 3: Save context documents to database
 1192        var savedCount = 0;
 1193        var skippedCount = 0;
 1194        var currentDate = DateOnly.FromDateTime(_timeProvider.GetLocalNow().DateTime)
 1195            .ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
 196
 1197        foreach (var (documentName, content) in allContextDocuments)
 198        {
 199            try
 200            {
 1201                if (settings.DryRun)
 202                {
 1203                    _console.MarkupLine($"[magenta]  Dry run - would save:[/] {documentName}");
 1204                    continue;
 205                }
 206
 207                // Check if this is a history document that needs Data_Collected_At column
 1208                string finalContent = content;
 1209                if (IsHistoryDocument(documentName))
 210                {
 211                    // Get the previous version to compare against
 1212                    var previousDocument = await contextRepository.GetLatestContextDocumentAsync(documentName, settings.
 1213                    var previousContent = previousDocument?.Content;
 214
 215                    // Add Data_Collected_At column with current date for new matches
 1216                    finalContent = HistoryCsvUtility.AddDataCollectedAtColumn(content, previousContent, currentDate);
 217
 1218                    if (settings.Verbose)
 219                    {
 1220                        _console.MarkupLine($"[dim]  Added Data_Collected_At column to {documentName}[/]");
 221                    }
 222                }
 223
 1224                var savedVersion = await contextRepository.SaveContextDocumentAsync(
 1225                    documentName,
 1226                    finalContent,
 1227                    settings.CommunityContext);
 228
 1229                if (savedVersion.HasValue)
 230                {
 1231                    savedCount++;
 1232                    if (settings.Verbose)
 233                    {
 1234                        _console.MarkupLine($"[green]  ✓ Saved {documentName} as version {savedVersion.Value}[/]");
 235                    }
 236                }
 237                else
 238                {
 1239                    skippedCount++;
 1240                    if (settings.Verbose)
 241                    {
 1242                        _console.MarkupLine($"[dim]  - Skipped {documentName} (content unchanged)[/]");
 243                    }
 244                }
 1245            }
 1246            catch (Exception ex)
 247            {
 1248                _logger.LogError(ex, "Failed to save context document {DocumentName}", documentName);
 1249                _console.MarkupLine($"[red]  ✗ Failed to save {documentName}: {ex.Message}[/]");
 1250            }
 1251        }
 252
 1253        if (settings.DryRun)
 254        {
 1255            _console.MarkupLine($"[magenta]✓ Dry run completed - would have processed {allContextDocuments.Count} docume
 256        }
 257        else
 258        {
 1259            _console.MarkupLine($"[green]✓ Context collection completed![/]");
 1260            _console.MarkupLine($"[green]  Saved: {savedCount} documents[/]");
 1261            _console.MarkupLine($"[dim]  Skipped: {skippedCount} documents (unchanged)[/]");
 262        }
 1263    }
 264
 265    private static bool IsHistoryDocument(string documentName)
 266    {
 1267        return documentName.StartsWith("recent-history-", StringComparison.OrdinalIgnoreCase) ||
 1268               documentName.StartsWith("home-history-", StringComparison.OrdinalIgnoreCase) ||
 1269               documentName.StartsWith("away-history-", StringComparison.OrdinalIgnoreCase);
 270    }
 271
 272    private static IReadOnlyList<int> ParseMatchdays(string? matchdays)
 273    {
 1274        if (string.IsNullOrWhiteSpace(matchdays))
 275        {
 1276            return [];
 277        }
 278
 1279        var result = new List<int>();
 1280        foreach (var token in matchdays.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntrie
 281        {
 1282            if (!int.TryParse(token, out var matchday) || matchday <= 0)
 283            {
 0284                throw new ArgumentException($"Invalid matchday '{token}'. Use positive integers separated by commas.");
 285            }
 286
 1287            if (!result.Contains(matchday))
 288            {
 1289                result.Add(matchday);
 290            }
 291        }
 292
 1293        return result;
 294    }
 295
 296    private void PrintOutcomeCollectionSummary(MatchOutcomeCollectionResult result, CollectContextKicktippSettings setti
 297    {
 1298        _console.MarkupLine($"[blue]Current tippuebersicht matchday:[/] [yellow]{result.CurrentMatchday}[/]");
 299
 1300        if (!result.IncompleteMatchdays.Any())
 301        {
 1302            _console.MarkupLine("[green]All persisted matchdays up to the current matchday are already complete[/]");
 1303            return;
 304        }
 305
 0306        _console.MarkupLine($"[blue]Incomplete matchdays to check:[/] [yellow]{string.Join(", ", result.IncompleteMatchd
 307
 0308        foreach (var summary in result.MatchdaySummaries)
 309        {
 0310            if (settings.DryRun)
 311            {
 0312                _console.MarkupLine(
 0313                    $"[magenta]  Dry run - would evaluate matchday {summary.Matchday}[/] " +
 0314                    $"({summary.FetchedMatches} matches, {summary.CompletedMatches} completed, {summary.PendingMatches} 
 0315                continue;
 316            }
 317
 0318            _console.MarkupLine(
 0319                $"[green]  Matchday {summary.Matchday}:[/] {summary.FetchedMatches} matches, " +
 0320                $"created {summary.CreatedCount}, updated {summary.UpdatedCount}, unchanged {summary.UnchangedCount}, " 
 0321                $"pending {summary.PendingMatches}");
 322        }
 0323    }
 324}