< Summary

Information
Class: ContextProviders.Kicktipp.KicktippContextProvider
Assembly: ContextProviders.Kicktipp
File(s): /home/runner/work/KicktippAi/KicktippAi/src/ContextProviders.Kicktipp/KicktippContextProvider.cs
Line coverage
100%
Covered lines: 133
Uncovered lines: 0
Coverable lines: 133
Total lines: 299
Line coverage: 100%
Branch coverage
82%
Covered branches: 33
Total branches: 40
Branch coverage: 82.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%88100%
LoadTeamHistoryAsync()100%22100%
LoadHomeAwayHistoryAsync()100%22100%
LoadDetailedHeadToHeadHistoryAsync()100%22100%
GetContextAsync()50%22100%
GetMatchContextAsync()50%22100%
GetBonusQuestionContextAsync()50%22100%
CurrentBundesligaStandings()100%11100%
RecentHistory()100%22100%
HomeHistory()50%22100%
AwayHistory()50%22100%
HeadToHeadHistory()50%22100%
CommunityScoringRules()100%22100%
ReadFileContentAsync()100%11100%
GetTeamAbbreviation(...)90%1010100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/ContextProviders.Kicktipp/KicktippContextProvider.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2using System.Text;
 3using ContextProviders.Kicktipp.Csv;
 4using EHonda.KicktippAi.Core;
 5using KicktippIntegration;
 6using Microsoft.Extensions.FileProviders;
 7
 8namespace ContextProviders.Kicktipp;
 9
 10public class KicktippContextProvider : IKicktippContextProvider
 11{
 12    private readonly IKicktippClient _kicktippClient;
 13    private readonly IFileProvider _communityRulesFileProvider;
 14    private readonly string _community;
 15    private readonly string _communityContext;
 16    private readonly Lazy<Task<IReadOnlyDictionary<string, List<MatchResult>>>> _teamHistoryLazy;
 17    private readonly Lazy<Task<IReadOnlyDictionary<string, (List<MatchResult> homeHistory, List<MatchResult> awayHistory
 18    private readonly Lazy<Task<IReadOnlyDictionary<string, List<HeadToHeadResult>>>> _detailedHeadToHeadHistoryLazy;
 19
 120    public KicktippContextProvider(
 121        IKicktippClient kicktippClient,
 122        IFileProvider communityRulesFileProvider,
 123        string community,
 124        string? communityContext = null)
 25    {
 126        _kicktippClient = kicktippClient ?? throw new ArgumentNullException(nameof(kicktippClient));
 127        _communityRulesFileProvider = communityRulesFileProvider ?? throw new ArgumentNullException(nameof(communityRule
 128        _community = community ?? throw new ArgumentNullException(nameof(community));
 129        _communityContext = communityContext ?? community;
 130        _teamHistoryLazy = new Lazy<Task<IReadOnlyDictionary<string, List<MatchResult>>>>(LoadTeamHistoryAsync);
 131        _homeAwayHistoryLazy = new Lazy<Task<IReadOnlyDictionary<string, (List<MatchResult> homeHistory, List<MatchResul
 132        _detailedHeadToHeadHistoryLazy = new Lazy<Task<IReadOnlyDictionary<string, List<HeadToHeadResult>>>>(LoadDetaile
 133    }
 34
 35    private async Task<IReadOnlyDictionary<string, List<MatchResult>>> LoadTeamHistoryAsync()
 36    {
 137        var matchesWithHistory = await _kicktippClient.GetMatchesWithHistoryAsync(_community);
 138        var teamHistory = new Dictionary<string, List<MatchResult>>();
 39
 140        foreach (var matchWithHistory in matchesWithHistory)
 41        {
 142            teamHistory[matchWithHistory.Match.HomeTeam] = matchWithHistory.HomeTeamHistory;
 143            teamHistory[matchWithHistory.Match.AwayTeam] = matchWithHistory.AwayTeamHistory;
 44        }
 45
 146        return teamHistory;
 147    }
 48
 49    private async Task<IReadOnlyDictionary<string, (List<MatchResult> homeHistory, List<MatchResult> awayHistory)>> Load
 50    {
 151        var matchesWithHistory = await _kicktippClient.GetMatchesWithHistoryAsync(_community);
 152        var homeAwayHistory = new Dictionary<string, (List<MatchResult> homeHistory, List<MatchResult> awayHistory)>();
 53
 154        foreach (var matchWithHistory in matchesWithHistory)
 55        {
 156            var homeTeam = matchWithHistory.Match.HomeTeam;
 157            var awayTeam = matchWithHistory.Match.AwayTeam;
 58
 159            var (homeTeamHistory, awayTeamHistory) = await _kicktippClient.GetHomeAwayHistoryAsync(_community, homeTeam,
 160            var cacheKey = $"{homeTeam}|{awayTeam}";
 161            homeAwayHistory[cacheKey] = (homeTeamHistory, awayTeamHistory);
 162        }
 63
 164        return homeAwayHistory;
 165    }
 66
 67    private async Task<IReadOnlyDictionary<string, List<HeadToHeadResult>>> LoadDetailedHeadToHeadHistoryAsync()
 68    {
 169        var matchesWithHistory = await _kicktippClient.GetMatchesWithHistoryAsync(_community);
 170        var detailedHeadToHeadHistory = new Dictionary<string, List<HeadToHeadResult>>();
 71
 172        foreach (var matchWithHistory in matchesWithHistory)
 73        {
 174            var homeTeam = matchWithHistory.Match.HomeTeam;
 175            var awayTeam = matchWithHistory.Match.AwayTeam;
 76
 177            var history = await _kicktippClient.GetHeadToHeadDetailedHistoryAsync(_community, homeTeam, awayTeam);
 178            var cacheKey = $"{homeTeam}|{awayTeam}";
 179            detailedHeadToHeadHistory[cacheKey] = history;
 180        }
 81
 182        return detailedHeadToHeadHistory;
 183    }
 84
 85    public async IAsyncEnumerable<DocumentContext> GetContextAsync([EnumeratorCancellation] CancellationToken cancellati
 86    {
 87        // Provide current Bundesliga standings
 188        yield return await CurrentBundesligaStandings();
 89
 90        // Provide community scoring rules
 191        yield return await CommunityScoringRules();
 192    }
 93
 94    /// <summary>
 95    /// Gets context for the two teams in a match.
 96    /// </summary>
 97    /// <param name="homeTeam">The home team name.</param>
 98    /// <param name="awayTeam">The away team name.</param>
 99    /// <returns>An enumerable of context documents for both teams.</returns>
 100    public async IAsyncEnumerable<DocumentContext> GetMatchContextAsync(string homeTeam, string awayTeam, [EnumeratorCan
 101    {
 102        // Provide current Bundesliga standings
 1103        yield return await CurrentBundesligaStandings();
 104
 105        // Provide community scoring rules
 1106        yield return await CommunityScoringRules();
 107
 108        // Provide recent history for both teams (Position 1 - already implemented)
 1109        yield return await RecentHistory(homeTeam);
 1110        yield return await RecentHistory(awayTeam);
 111
 112        // Provide home/away specific history for both teams (Position 2)
 1113        yield return await HomeHistory(homeTeam, awayTeam);
 1114        yield return await AwayHistory(homeTeam, awayTeam);
 115
 116        // Provide head-to-head history between the teams (Position 3)
 1117        yield return await HeadToHeadHistory(homeTeam, awayTeam);
 1118    }
 119
 120    /// <summary>
 121    /// Gets context for bonus questions.
 122    /// </summary>
 123    /// <returns>An enumerable of context documents relevant for bonus questions.</returns>
 124    public async IAsyncEnumerable<DocumentContext> GetBonusQuestionContextAsync([EnumeratorCancellation] CancellationTok
 125    {
 126        // Provide current Bundesliga standings
 1127        yield return await CurrentBundesligaStandings();
 128
 129        // Provide community scoring rules
 1130        yield return await CommunityScoringRules();
 131
 132        // For bonus questions, we could add historical season data, transfer information, etc.
 133        // For now, we'll use the standings as the primary context
 1134    }
 135
 136    /// <summary>
 137    /// Gets the current Bundesliga standings as context.
 138    /// </summary>
 139    /// <returns>A document context containing the current standings.</returns>
 140    public async Task<DocumentContext> CurrentBundesligaStandings()
 141    {
 1142        var standings = await _kicktippClient.GetStandingsAsync(_community);
 143
 1144        return standings.ToCsvDocumentContext<TeamStanding, TeamStandingCsvMap>("bundesliga-standings");
 1145    }
 146
 147    /// <summary>
 148    /// Gets recent match history for a specific team.
 149    /// </summary>
 150    /// <param name="teamName">The name of the team to get history for.</param>
 151    /// <returns>A document context containing the team's recent match history.</returns>
 152    public async Task<DocumentContext> RecentHistory(string teamName)
 153    {
 1154        var teamHistory = await _teamHistoryLazy.Value;
 1155        var matchResults = teamHistory.TryGetValue(teamName, out var results) ? results : new List<MatchResult>();
 156
 1157        var teamAbbreviation = GetTeamAbbreviation(teamName);
 158
 1159        return matchResults.ToCsvDocumentContext<MatchResult, MatchResultCsvMap>($"recent-history-{teamAbbreviation}");
 1160    }
 161
 162    /// <summary>
 163    /// Gets home/away specific history for both teams in a match.
 164    /// </summary>
 165    /// <param name="homeTeam">The home team name.</param>
 166    /// <param name="awayTeam">The away team name.</param>
 167    /// <returns>A document context containing home team's home history and away team's away history.</returns>
 168    public async Task<DocumentContext> HomeHistory(string homeTeam, string awayTeam)
 169    {
 1170        var homeAwayHistory = await _homeAwayHistoryLazy.Value;
 1171        var cacheKey = $"{homeTeam}|{awayTeam}";
 172
 1173        var (homeTeamHistory, _) = homeAwayHistory.TryGetValue(cacheKey, out var history)
 1174            ? history
 1175            : (new List<MatchResult>(), new List<MatchResult>());
 176
 1177        var homeAbbreviation = GetTeamAbbreviation(homeTeam);
 178
 1179        return homeTeamHistory.ToCsvDocumentContext<MatchResult, MatchResultCsvMap>($"home-history-{homeAbbreviation}");
 1180    }
 181
 182    public async Task<DocumentContext> AwayHistory(string homeTeam, string awayTeam)
 183    {
 1184        var homeAwayHistory = await _homeAwayHistoryLazy.Value;
 1185        var cacheKey = $"{homeTeam}|{awayTeam}";
 186
 1187        var (_, awayTeamHistory) = homeAwayHistory.TryGetValue(cacheKey, out var history)
 1188            ? history
 1189            : (new List<MatchResult>(), new List<MatchResult>());
 190
 1191        var awayAbbreviation = GetTeamAbbreviation(awayTeam);
 192
 1193        return awayTeamHistory.ToCsvDocumentContext<MatchResult, MatchResultCsvMap>($"away-history-{awayAbbreviation}");
 1194    }
 195
 196    /// <summary>
 197    /// Gets head-to-head history between two teams.
 198    /// </summary>
 199    /// <param name="homeTeam">The home team name.</param>
 200    /// <param name="awayTeam">The away team name.</param>
 201    /// <returns>A document context containing head-to-head match history.</returns>
 202    public async Task<DocumentContext> HeadToHeadHistory(string homeTeam, string awayTeam)
 203    {
 1204        var detailedHeadToHeadHistory = await _detailedHeadToHeadHistoryLazy.Value;
 1205        var cacheKey = $"{homeTeam}|{awayTeam}";
 206
 1207        var history = detailedHeadToHeadHistory.TryGetValue(cacheKey, out var cachedHistory)
 1208            ? cachedHistory
 1209            : new List<HeadToHeadResult>();
 210
 1211        var homeAbbreviation = GetTeamAbbreviation(homeTeam);
 1212        var awayAbbreviation = GetTeamAbbreviation(awayTeam);
 213
 1214        return history.ToCsvDocumentContext<HeadToHeadResult, HeadToHeadResultCsvMap>(
 1215            $"head-to-head-{homeAbbreviation}-vs-{awayAbbreviation}");
 1216    }
 217
 218    /// <summary>
 219    /// Gets the community scoring rules as context.
 220    /// </summary>
 221    /// <returns>A document context containing the scoring rules.</returns>
 222    /// <exception cref="FileNotFoundException">Thrown when the community-specific scoring rules file is not found.</exc
 223    public async Task<DocumentContext> CommunityScoringRules()
 224    {
 1225        var filePath = $"{_communityContext}.md";
 1226        var fileInfo = _communityRulesFileProvider.GetFileInfo(filePath);
 227
 1228        if (!fileInfo.Exists)
 229        {
 1230            throw new FileNotFoundException(
 1231                $"Community scoring rules file not found: {filePath}. Expected file for community context '{_communityCo
 232        }
 233
 1234        var content = await ReadFileContentAsync(fileInfo);
 1235        return new DocumentContext(
 1236            Name: $"community-rules-{_communityContext}.md",
 1237            Content: content);
 1238    }
 239
 240    /// <summary>
 241    /// Reads the content from a file info asynchronously.
 242    /// </summary>
 243    /// <param name="fileInfo">The file info to read from.</param>
 244    /// <returns>The file content as a string.</returns>
 245    private static async Task<string> ReadFileContentAsync(IFileInfo fileInfo)
 246    {
 1247        await using var stream = fileInfo.CreateReadStream();
 1248        using var reader = new StreamReader(stream);
 1249        return await reader.ReadToEndAsync();
 1250    }
 251
 252    /// <summary>
 253    /// Gets a team abbreviation for file naming.
 254    /// </summary>
 255    private string GetTeamAbbreviation(string teamName)
 256    {
 257        // Current season team abbreviations (2025-26 Bundesliga participants)
 1258        var abbreviations = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 1259        {
 1260            { "1. FC Heidenheim 1846", "fch" },
 1261            { "1. FC Köln", "fck" },
 1262            { "1. FC Union Berlin", "fcu" },
 1263            { "1899 Hoffenheim", "tsg" },
 1264            { "Bayer 04 Leverkusen", "b04" },
 1265            { "Bor. Mönchengladbach", "bmg" },
 1266            { "Borussia Dortmund", "bvb" },
 1267            { "Eintracht Frankfurt", "sge" },
 1268            { "FC Augsburg", "fca" },
 1269            { "FC Bayern München", "fcb" },
 1270            { "FC St. Pauli", "fcs" },
 1271            { "FSV Mainz 05", "m05" },
 1272            { "Hamburger SV", "hsv" },
 1273            { "RB Leipzig", "rbl" },
 1274            { "SC Freiburg", "scf" },
 1275            { "VfB Stuttgart", "vfb" },
 1276            { "VfL Wolfsburg", "wob" },
 1277            { "Werder Bremen", "svw" }
 1278        };
 279
 1280        if (abbreviations.TryGetValue(teamName, out var abbreviation))
 281        {
 1282            return abbreviation;
 283        }
 284
 285        // Fallback: create abbreviation from team name
 1286        var words = teamName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
 1287        var abbr = new StringBuilder();
 288
 1289        foreach (var word in words.Take(3)) // Take up to 3 words
 290        {
 1291            if (word.Length > 0 && char.IsLetter(word[0]))
 292            {
 1293                abbr.Append(char.ToLowerInvariant(word[0]));
 294            }
 295        }
 296
 1297        return abbr.Length > 0 ? abbr.ToString() : "unknown";
 298    }
 299}