using System.Text.Json;
using Moq;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
using static ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests.TemplateReconcileClosureTests;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests;
///
/// Gitea #89: the last two whole-library template reads — the operator resync
/// ( ) and the read-only resolved-members
/// query — now read only what the inheritance resolver walks: the resync reuses #86's reconcile
/// closure (subtree + chain), falling back to the template's own chain when nothing derives from
/// it, and resolved-members reads the chain alone. Pinned against the #86 fixture (deep subtree,
/// ancestors above the edited template, a composed module, look-alike unrelated trees, a parent
/// cycle, a dangling parent).
///
public class TemplateChainReadTests
{
private static Task> Chain(
IReadOnlyList graph,
int id,
List? calls = null) =>
TemplateReconcileClosure.CollectChainAsync(
id,
(ids, _) =>
{
calls?.Add("ids:" + string.Join(",", ids.OrderBy(i => i)));
return Task.FromResult>(graph.Where(t => ids.Contains(t.Id)).ToList());
});
/// Every fixture template id, plus one that does not exist.
/// One row per id.
public static IEnumerable EveryTemplateAndUnknown() =>
BuildGraph().Select(t => new object[] { t.Id }).Append(new object[] { 424242 });
///
/// The resolved-members query answers identically from the chain alone and from the whole
/// library, for every template (resolved set + staleness, serialized).
///
/// The template resolved.
/// A task that represents the asynchronous operation.
[Theory]
[MemberData(nameof(EveryTemplateAndUnknown))]
public async Task ResolvedMembers_AnswerIdentically_FromChainAndWholeGraph(int templateId)
{
var graph = BuildGraph();
var chain = await Chain(graph, templateId);
Assert.Equal(
JsonSerializer.Serialize(TemplateInheritanceResolver.Resolve(templateId, graph), Json),
JsonSerializer.Serialize(TemplateInheritanceResolver.Resolve(templateId, chain), Json));
}
/// The chain is the template and its ancestors — one load per level, nothing else.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Chain_IsTemplateAndAncestorsOnly_OneLoadPerLevel()
{
var calls = new List();
var chain = await Chain(BuildGraph(), GreatLeaf, calls);
Assert.Equal(new[] { GreatLeaf, Leaf1, Mid, Base, Root }, chain.Select(t => t.Id));
Assert.Equal(new[] { "ids:7", "ids:4", "ids:3", "ids:2", "ids:1" }, calls);
Assert.DoesNotContain(chain, t => t.Id == Mod || t.Id == U1);
}
/// A parent cycle and a dangling parent both end the chain; an unknown id loads nothing.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Chain_TerminatesOnCyclesDanglingParentsAndUnknownIds()
{
var graph = BuildGraph();
Assert.Equal(new[] { CycC, CycA, CycB }, (await Chain(graph, CycC)).Select(t => t.Id));
Assert.Equal(new[] { DangKid, Dang }, (await Chain(graph, DangKid)).Select(t => t.Id));
Assert.Empty(await Chain(graph, 424242));
}
///
/// End to end through : resyncing from
/// the closure (with the whole-library load wired to throw) issues exactly the writes, audit and
/// result of the old whole-graph resync, for every template and for an unknown id.
///
/// The template resynced.
/// A task that represents the asynchronous operation.
[Theory]
[MemberData(nameof(EveryTemplateAndUnknown))]
public async Task Resync_IssuesIdenticalWritesAndResult_FromClosureAndWholeGraph(int templateId)
{
var expected = await RecordResync(templateId, useClosure: false);
var actual = await RecordResync(templateId, useClosure: true);
Assert.Equal(expected, actual);
}
///
/// The equivalence is not vacuous: resyncing the leaf GreatLeaf (empty reconcile closure,
/// so the chain fallback is load-bearing) materializes the members it inherits from four levels
/// up, resyncing Leaf1 removes its orphan, and resyncing Base repairs the subtree.
///
/// A task that represents the asynchronous operation.
[Fact]
public async Task Fixture_ResyncWritesRepairs_ForLeafAndSubtree()
{
var leaf = await RecordResync(GreatLeaf, useClosure: true);
Assert.Contains(leaf, w => w.StartsWith("AddTemplateAttributeAsync", StringComparison.Ordinal));
Assert.Contains(leaf, w => w.StartsWith("Result:Success", StringComparison.Ordinal));
var leaf1 = await RecordResync(Leaf1, useClosure: true);
Assert.Contains(leaf1, w => w.StartsWith("DeleteTemplateAttributeAsync", StringComparison.Ordinal));
var subtree = await RecordResync(Base, useClosure: true);
Assert.Contains(subtree, w => w.StartsWith("UpdateTemplateScriptAsync", StringComparison.Ordinal));
Assert.Contains(subtree, w => w.StartsWith("UpdateTemplateAlarmAsync", StringComparison.Ordinal));
}
///
/// Runs the resync on a fresh copy of the graph and returns every write the service issued, the
/// audit row and the result, rendered as sorted strings.
///
private static async Task> RecordResync(int templateId, bool useClosure)
{
var graph = BuildGraph();
var repo = new Mock();
var audit = new Mock();
repo.Setup(r => r.SaveChangesAsync(It.IsAny())).ReturnsAsync(1);
if (useClosure)
{
repo.Setup(r => r.GetAllTemplatesAsync(It.IsAny()))
.ThrowsAsync(new InvalidOperationException("resync loaded the whole tracked graph"));
repo.Setup(r => r.GetTemplateReconcileClosureAsync(templateId, It.IsAny()))
.Returns((int id, CancellationToken _) => Closure(graph, id));
// The resync writes through what it loads: the chain fallback must be the tracked read.
repo.Setup(r => r.GetTemplateChainAsync(templateId, true, It.IsAny()))
.Returns((int id, bool _, CancellationToken _) => Chain(graph, id));
}
else
{
repo.Setup(r => r.GetAllTemplatesAsync(It.IsAny())).ReturnsAsync(graph);
repo.Setup(r => r.GetTemplateReconcileClosureAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(graph);
repo.Setup(r => r.GetTemplateChainAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(graph);
}
var service = new TemplateService(repo.Object, audit.Object);
var result = await service.ResyncInheritedMembersAsync(templateId, "tester");
static string Render(Moq.IInvocation i) =>
i.Method.Name + "(" + string.Join(", ", i.Arguments
.Where(a => a is not CancellationToken)
.Select(a => JsonSerializer.Serialize(a, Json))) + ")";
return repo.Invocations
.Where(i => i.Method.Name.StartsWith("Add", StringComparison.Ordinal)
|| i.Method.Name.StartsWith("Update", StringComparison.Ordinal)
|| i.Method.Name.StartsWith("Delete", StringComparison.Ordinal)
|| i.Method.Name == nameof(ITemplateEngineRepository.SaveChangesAsync))
.Select(Render)
.Concat(audit.Invocations.Select(Render))
.Append(result.IsSuccess
? "Result:Success " + JsonSerializer.Serialize(result.Value, Json)
: "Result:Failure " + result.Error)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
}
}