< Summary

Information
Class: FirebaseAdapter.FirebaseContextRepository
Assembly: FirebaseAdapter
File(s): /home/runner/work/KicktippAi/KicktippAi/src/FirebaseAdapter/FirebaseContextRepository.cs
Line coverage
79%
Covered lines: 118
Uncovered lines: 31
Coverable lines: 149
Total lines: 280
Line coverage: 79.1%
Branch coverage
100%
Covered branches: 30
Total branches: 30
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%66100%
SaveContextDocumentAsync()100%6685.19%
GetLatestContextDocumentAsync()100%2275%
GetContextDocumentAsync()100%2266.67%
GetContextDocumentByTimestampAsync()100%2263.64%
GetContextDocumentNamesAsync()100%4478.57%
GetContextDocumentVersionsAsync()100%4475%
UpdateContextDocumentVersionAsync()100%2278.95%
ConvertToContextDocument(...)100%11100%
BuildDocumentId(...)100%22100%

File(s)

/home/runner/work/KicktippAi/KicktippAi/src/FirebaseAdapter/FirebaseContextRepository.cs

#LineLine coverage
 1using EHonda.KicktippAi.Core;
 2using FirebaseAdapter.Models;
 3using Google.Cloud.Firestore;
 4using Microsoft.Extensions.Logging;
 5using NodaTime;
 6
 7namespace FirebaseAdapter;
 8
 9/// <summary>
 10/// Firebase Firestore implementation of the context repository.
 11/// </summary>
 12public class FirebaseContextRepository : IContextRepository
 13{
 14    private readonly FirestoreDb _firestoreDb;
 15    private readonly ILogger<FirebaseContextRepository> _logger;
 16    private readonly string _contextDocumentsCollection;
 17    private readonly string _competition;
 18
 119    public FirebaseContextRepository(
 120        FirestoreDb firestoreDb,
 121        ILogger<FirebaseContextRepository> logger,
 122        string? competition = null)
 23    {
 124        _firestoreDb = firestoreDb ?? throw new ArgumentNullException(nameof(firestoreDb));
 125        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 26
 127        _contextDocumentsCollection = "context-documents";
 128        _competition = string.IsNullOrWhiteSpace(competition)
 129            ? CompetitionIds.Bundesliga2025_26
 130            : competition.Trim();
 31
 132        _logger.LogInformation("Firebase context repository initialized");
 133    }
 34
 35    public async Task<int?> SaveContextDocumentAsync(string documentName, string content, string communityContext, Cance
 36    {
 37        try
 38        {
 39            // Get the latest version to check if content differs
 140            var latestDocument = await GetLatestContextDocumentAsync(documentName, communityContext, cancellationToken);
 41
 42            // If content is the same, don't save a new version
 143            if (latestDocument != null && latestDocument.Content == content)
 44            {
 145                _logger.LogInformation("Context document {DocumentName} content unchanged, skipping save", documentName)
 146                return null;
 47            }
 48
 49            // Determine the next version number
 150            var nextVersion = latestDocument?.Version + 1 ?? 0;
 51
 152            var now = Timestamp.GetCurrentTimestamp();
 153            var documentId = BuildDocumentId(documentName, communityContext, nextVersion);
 54
 155            var firestoreDocument = new FirestoreContextDocument
 156            {
 157                Id = documentId,
 158                DocumentName = documentName,
 159                Content = content,
 160                Version = nextVersion,
 161                CreatedAt = now,
 162                Competition = _competition,
 163                CommunityContext = communityContext
 164            };
 65
 166            var docRef = _firestoreDb.Collection(_contextDocumentsCollection).Document(documentId);
 167            await docRef.SetAsync(firestoreDocument, cancellationToken: cancellationToken);
 68
 169            _logger.LogInformation("Saved context document {DocumentName} version {Version} for community {CommunityCont
 170                documentName, nextVersion, communityContext);
 71
 172            return nextVersion;
 73        }
 074        catch (Exception ex)
 75        {
 076            _logger.LogError(ex, "Failed to save context document {DocumentName} for community {CommunityContext}",
 077                documentName, communityContext);
 078            throw;
 79        }
 180    }
 81
 82    public async Task<ContextDocument?> GetLatestContextDocumentAsync(string documentName, string communityContext, Canc
 83    {
 84        try
 85        {
 186            var query = _firestoreDb.Collection(_contextDocumentsCollection)
 187                .WhereEqualTo("documentName", documentName)
 188                .WhereEqualTo("communityContext", communityContext)
 189                .WhereEqualTo("competition", _competition)
 190                .OrderByDescending("version")
 191                .Limit(1);
 92
 193            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 94
 195            if (!snapshot.Documents.Any())
 96            {
 197                return null;
 98            }
 99
 1100            var firestoreDoc = snapshot.Documents.First().ConvertTo<FirestoreContextDocument>();
 1101            return ConvertToContextDocument(firestoreDoc);
 102        }
 0103        catch (Exception ex)
 104        {
 0105            _logger.LogError(ex, "Failed to retrieve latest context document {DocumentName} for community {CommunityCont
 0106                documentName, communityContext);
 0107            throw;
 108        }
 1109    }
 110
 111    public async Task<ContextDocument?> GetContextDocumentAsync(string documentName, int version, string communityContex
 112    {
 113        try
 114        {
 1115            var documentId = BuildDocumentId(documentName, communityContext, version);
 1116            var docRef = _firestoreDb.Collection(_contextDocumentsCollection).Document(documentId);
 1117            var snapshot = await docRef.GetSnapshotAsync(cancellationToken);
 118
 1119            if (!snapshot.Exists)
 120            {
 1121                return null;
 122            }
 123
 1124            var firestoreDoc = snapshot.ConvertTo<FirestoreContextDocument>();
 1125            return ConvertToContextDocument(firestoreDoc);
 126        }
 0127        catch (Exception ex)
 128        {
 0129            _logger.LogError(ex, "Failed to retrieve context document {DocumentName} version {Version} for community {Co
 0130                documentName, version, communityContext);
 0131            throw;
 132        }
 1133    }
 134
 135    public async Task<ContextDocument?> GetContextDocumentByTimestampAsync(
 136        string documentName,
 137        DateTimeOffset createdAtOrEarlier,
 138        string communityContext,
 139        CancellationToken cancellationToken = default)
 140    {
 141        try
 142        {
 1143            var query = _firestoreDb.Collection(_contextDocumentsCollection)
 1144                .WhereEqualTo("documentName", documentName)
 1145                .WhereEqualTo("communityContext", communityContext)
 1146                .WhereEqualTo("competition", _competition)
 1147                .WhereLessThanOrEqualTo("createdAt", Timestamp.FromDateTime(createdAtOrEarlier.UtcDateTime))
 1148                .OrderByDescending("createdAt")
 1149                .OrderByDescending("version")
 1150                .Limit(1);
 151
 1152            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 153
 1154            if (!snapshot.Documents.Any())
 155            {
 1156                return null;
 157            }
 158
 1159            var firestoreDoc = snapshot.Documents.First().ConvertTo<FirestoreContextDocument>();
 1160            return ConvertToContextDocument(firestoreDoc);
 161        }
 0162        catch (Exception ex)
 163        {
 0164            _logger.LogError(
 0165                ex,
 0166                "Failed to retrieve context document {DocumentName} at timestamp {CreatedAt} for community {CommunityCon
 0167                documentName,
 0168                createdAtOrEarlier,
 0169                communityContext);
 0170            throw;
 171        }
 1172    }
 173
 174    public async Task<IReadOnlyList<string>> GetContextDocumentNamesAsync(string communityContext, CancellationToken can
 175    {
 176        try
 177        {
 1178            var query = _firestoreDb.Collection(_contextDocumentsCollection)
 1179                .WhereEqualTo("communityContext", communityContext)
 1180                .WhereEqualTo("competition", _competition)
 1181                .Select("documentName");
 182
 1183            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 184
 1185            var documentNames = snapshot.Documents
 1186                .Select(doc => doc.GetValue<string>("documentName"))
 1187                .Distinct()
 1188                .OrderBy(name => name, StringComparer.Ordinal)
 1189                .ToList()
 1190                .AsReadOnly();
 191
 1192            return documentNames;
 193        }
 0194        catch (Exception ex)
 195        {
 0196            _logger.LogError(ex, "Failed to retrieve context document names for community {CommunityContext}", community
 0197            throw;
 198        }
 1199    }
 200
 201    public async Task<IReadOnlyList<ContextDocument>> GetContextDocumentVersionsAsync(string documentName, string commun
 202    {
 203        try
 204        {
 1205            var query = _firestoreDb.Collection(_contextDocumentsCollection)
 1206                .WhereEqualTo("documentName", documentName)
 1207                .WhereEqualTo("communityContext", communityContext)
 1208                .WhereEqualTo("competition", _competition)
 1209                .OrderBy("version");
 210
 1211            var snapshot = await query.GetSnapshotAsync(cancellationToken);
 212
 1213            var documents = snapshot.Documents
 1214                .Select(doc => doc.ConvertTo<FirestoreContextDocument>())
 1215                .Select(ConvertToContextDocument)
 1216                .ToList()
 1217                .AsReadOnly();
 218
 1219            return documents;
 220        }
 0221        catch (Exception ex)
 222        {
 0223            _logger.LogError(ex, "Failed to retrieve context document versions for {DocumentName} in community {Communit
 0224                documentName, communityContext);
 0225            throw;
 226        }
 1227    }
 228
 229    public async Task<bool> UpdateContextDocumentVersionAsync(string documentName, int version, string newContent, strin
 230    {
 231        try
 232        {
 1233            var documentId = BuildDocumentId(documentName, communityContext, version);
 1234            var docRef = _firestoreDb.Collection(_contextDocumentsCollection).Document(documentId);
 235
 236            // Check if document exists
 1237            var snapshot = await docRef.GetSnapshotAsync(cancellationToken);
 1238            if (!snapshot.Exists)
 239            {
 1240                _logger.LogWarning("Cannot update non-existent document {DocumentId}", documentId);
 1241                return false;
 242            }
 243
 244            // Update only the content field
 1245            var updates = new Dictionary<string, object>
 1246            {
 1247                ["content"] = newContent
 1248            };
 249
 1250            await docRef.UpdateAsync(updates, cancellationToken: cancellationToken);
 251
 1252            _logger.LogInformation("Updated content for context document {DocumentName} version {Version} in community {
 1253                documentName, version, communityContext);
 254
 1255            return true;
 256        }
 0257        catch (Exception ex)
 258        {
 0259            _logger.LogError(ex, "Failed to update context document {DocumentName} version {Version} in community {Commu
 0260                documentName, version, communityContext);
 0261            throw;
 262        }
 1263    }
 264
 265    private static ContextDocument ConvertToContextDocument(FirestoreContextDocument firestoreDoc)
 266    {
 1267        return new ContextDocument(
 1268            firestoreDoc.DocumentName,
 1269            firestoreDoc.Content,
 1270            firestoreDoc.Version,
 1271            firestoreDoc.CreatedAt.ToDateTimeOffset());
 272    }
 273
 274    private string BuildDocumentId(string documentName, string communityContext, int version)
 275    {
 1276        return string.Equals(_competition, CompetitionIds.Bundesliga2025_26, StringComparison.OrdinalIgnoreCase)
 1277            ? $"{documentName}_{communityContext}_{version}"
 1278            : $"{_competition}_{documentName}_{communityContext}_{version}";
 279    }
 280}