using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using AutopilotMonitor.Shared.DataAccess; using AutopilotMonitor.Shared.Models.Graph; namespace AutopilotMonitor.Functions.Tests.GraphResolution; /// /// In-memory fake. Reusable across PR-B (cleanup /// function tests) and PR-C (resolver integration tests). /// internal sealed class FakeScriptNameCacheRepository : IScriptNameCacheRepository { public Dictionary<(string Tenant, ScriptKind Kind, string Id), ScriptDisplayNameEntry> Entries { get; } = new(); public Dictionary<(string Tenant, ScriptKind Kind), ScriptNameCacheMeta> Meta { get; } = new(); public List<(string Tenant, ScriptKind Kind, string Id)> DeleteCalls { get; } = new(); /// If non-null, the next Delete call throws this exception (one-shot). public Exception? NextDeleteThrows { get; set; } public Task> GetManyAsync( string tenantId, IReadOnlyCollection refs, CancellationToken ct = default) { var result = new Dictionary(); foreach (var r in refs) { if (Entries.TryGetValue((tenantId, r.Kind, r.Id), out var entry)) { result[r] = entry; } } return Task.FromResult>(result); } public Task UpsertManyAsync(string tenantId, IReadOnlyCollection entries, CancellationToken ct = default) { foreach (var e in entries) { Entries[(tenantId, e.Kind, e.ScriptId)] = e; } return Task.CompletedTask; } public Task TryGetMetaAsync(string tenantId, ScriptKind kind, CancellationToken ct = default) { Meta.TryGetValue((tenantId, kind), out var meta); return Task.FromResult(meta); } public Task UpsertMetaAsync(ScriptNameCacheMeta meta, CancellationToken ct = default) { Meta[(meta.TenantId, meta.Kind)] = meta; return Task.CompletedTask; } public async IAsyncEnumerable QueryExpiredAsync( DateTimeOffset positiveCutoff, DateTimeOffset notFoundCutoff, [EnumeratorCancellation] CancellationToken ct = default) { // Snapshot to allow mutation-during-enumeration in tests. foreach (var entry in Entries.Values.ToList()) { ct.ThrowIfCancellationRequested(); var cutoff = entry.IsNotFound ? notFoundCutoff : positiveCutoff; if (entry.FetchedAt < cutoff) { yield return entry; } await Task.Yield(); } } public Task DeleteAsync(string tenantId, ScriptKind kind, string scriptId, CancellationToken ct = default) { DeleteCalls.Add((tenantId, kind, scriptId)); if (NextDeleteThrows != null) { var ex = NextDeleteThrows; NextDeleteThrows = null; throw ex; } Entries.Remove((tenantId, kind, scriptId)); return Task.CompletedTask; } }