using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
///
/// Gitea #66: reads only a
/// template's inheritance chain + composition closure — one projection query per closure depth —
/// instead of the whole library that
/// materialises. Asserted against real SQL on SQLite with the reader commands recorded.
///
public class TemplateAnalysisClosureRepositoryTests : IDisposable
{
private const int UnrelatedTemplates = 40;
private const string BigBody = "/* script body that must never be read by analysis */";
private readonly SqlReaderCommandRecorder _recorder = new();
private readonly ScadaBridgeDbContext _context;
private readonly TemplateEngineRepository _repository;
/// Initializes the test class with an in-memory context and repository.
public TemplateAnalysisClosureRepositoryTests()
{
_context = SqliteTestHelper.CreateInMemoryContext(_recorder);
_repository = new TemplateEngineRepository(_context, new TemplateGraphWatermark());
}
/// Releases the resources held by this test class.
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
private async Task AddAsync(string name, int? parentId = null)
{
var t = new Template(name) { ParentTemplateId = parentId };
t.Attributes.Add(new TemplateAttribute($"{name}Attr1"));
t.Attributes.Add(new TemplateAttribute($"{name}Attr2"));
t.Scripts.Add(new TemplateScript($"{name}Script", BigBody));
_context.Templates.Add(t);
await _context.SaveChangesAsync();
return t;
}
private async Task ComposeAsync(Template owner, Template module, string slot)
{
_context.Set().Add(
new TemplateComposition(slot) { TemplateId = owner.Id, ComposedTemplateId = module.Id });
await _context.SaveChangesAsync();
}
private sealed record Seeded(int Grand, int Parent, int Child, int ModA, int ModB, int ModBBase, int Inner);
private async Task SeedAsync()
{
var grand = await AddAsync("Grand");
var parent = await AddAsync("Parent", grand.Id);
var child = await AddAsync("Child", parent.Id);
var modBBase = await AddAsync("ModBBase");
var modB = await AddAsync("ModB", modBBase.Id);
var modA = await AddAsync("ModA");
var inner = await AddAsync("Inner");
await ComposeAsync(parent, modA, "m1");
await ComposeAsync(child, modB, "m2");
await ComposeAsync(modA, inner, "inner");
for (var i = 0; i < UnrelatedTemplates; i++)
await AddAsync($"Unrelated{i}");
_context.ChangeTracker.Clear();
_recorder.Reset();
return new Seeded(grand.Id, parent.Id, child.Id, modA.Id, modB.Id, modBBase.Id, inner.Id);
}
///
/// The closure is exactly the chain + composition closure, loaded untracked without script
/// bodies, in one query per depth — against 47 templates for the whole-graph load.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task Closure_ReadsOnlyChainAndCompositionClosure_OneQueryPerDepth()
{
var ids = await SeedAsync();
var whole = await _repository.GetAllTemplatesForAnalysisAsync();
var wholeQueries = _recorder.Commands.Count;
_recorder.Reset();
var closure = await _repository.GetTemplateAnalysisClosureAsync(ids.Child);
var closureQueries = _recorder.Commands.Count;
// Entity count: 6 templates / 12 attributes instead of 47 / 94.
Assert.Equal(
new[] { ids.Grand, ids.Parent, ids.Child, ids.ModA, ids.ModB, ids.Inner }.OrderBy(i => i),
closure.Select(t => t.Id).OrderBy(i => i));
Assert.Equal(7 + UnrelatedTemplates, whole.Count);
Assert.Equal(12, closure.Sum(t => t.Attributes.Count));
Assert.Equal(2 * (7 + UnrelatedTemplates), whole.Sum(t => t.Attributes.Count));
// Query count: one projection query per depth (Child; Parent+ModB; Grand+ModA; Inner).
Assert.Equal(4 * wholeQueries, closureQueries);
// Same analysis shape as the whole-graph load: untracked, no script bodies, edges present.
Assert.Empty(_context.ChangeTracker.Entries());
Assert.All(closure.SelectMany(t => t.Scripts), s => Assert.Equal(string.Empty, s.Code));
Assert.False(_recorder.AnySelectReads("\"Code\""));
Assert.Single(closure.Single(t => t.Id == ids.Child).Compositions);
// Positive control: the body column IS visible to the recorder when a load reads it.
_recorder.Reset();
await _repository.GetAllTemplatesAsync();
Assert.True(_recorder.AnySelectReads("\"Code\""));
}
/// An unknown id loads nothing beyond the single probe query.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Closure_UnknownTemplate_IsEmpty()
{
await SeedAsync();
Assert.Empty(await _repository.GetTemplateAnalysisClosureAsync(987654));
}
}