using System.Text.Json;
using Akka.Actor;
using Akka.TestKit.Xunit2;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.TestSupport;
using ZB.MOM.WW.ScadaBridge.TestSupport;
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors;
///
/// Review 2026-10-21 01/SR-P5: a deploy's flattened config is deserialized ONCE (in the
/// off-thread compile warm) and the parsed object is handed to the gate, the DCL-connection
/// ensure and the Instance Actor. These pin the two contracts that refactor must keep: a config
/// that does not parse is still a readable gate rejection with nothing applied, and an Instance
/// Actor built from the parsed object behaves like one built from the JSON.
///
public class DeploymentManagerParseOnceTests : TestKit, IDisposable
{
private readonly SiteStorageService _storage;
private readonly ScriptCompilationService _compilationService;
private readonly SharedScriptLibrary _sharedScriptLibrary;
private readonly TestLocalDb _localDb;
/// Creates a temporary LocalDb-backed site store per test.
public DeploymentManagerParseOnceTests()
{
_localDb = TestLocalDb.CreateTemp("dm-parse-once");
_storage = new SiteStorageService(_localDb.Db, NullLogger.Instance);
_storage.InitializeAsync().GetAwaiter().GetResult();
_compilationService = new ScriptCompilationService(NullLogger.Instance);
_sharedScriptLibrary = new SharedScriptLibrary(
_compilationService, NullLogger.Instance);
}
///
void IDisposable.Dispose()
{
Shutdown();
var path = _localDb.Path;
_localDb.Dispose();
TestLocalDb.DeleteFiles(path);
}
private IActorRef CreateBootedDeploymentManager()
{
var health = new DeploymentManagerStartupProbe();
var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
_storage, _compilationService, _sharedScriptLibrary, null,
new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 0 },
NullLogger.Instance,
dclManager: null, healthCollector: health, serviceProvider: null,
loggerFactory: null, configFetcher: null)));
AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20), TimeSpan.FromMilliseconds(25));
return dm;
}
/// Malformed JSON is rejected by the gate with the deserialization error; nothing is persisted.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Deploy_MalformedConfig_IsRejectedWithTheParseError()
{
var dm = CreateBootedDeploymentManager();
dm.Tell(new DeployInstanceCommand("dep-1", "Bad1", "r1", "{not json", "tester", DateTimeOffset.UtcNow));
var response = ExpectMsg(TimeSpan.FromSeconds(15));
Assert.Equal(DeploymentStatus.Failed, response.Status);
Assert.Contains("failed to deserialize", response.ErrorMessage);
Assert.Null(await _storage.GetDeployedConfigAsync("Bad1"));
}
/// A deploy built from the parsed object serves its configured attributes.
/// A task that represents the asynchronous operation.
[Fact]
public async Task Deploy_ParsedOncePath_CreatesAWorkingInstance()
{
var dm = CreateBootedDeploymentManager();
var json = JsonSerializer.Serialize(new FlattenedConfiguration
{
InstanceUniqueName = "Good1",
Attributes = [new ResolvedAttribute { CanonicalName = "A", Value = "7", DataType = "Int32" }]
});
dm.Tell(new DeployInstanceCommand("dep-2", "Good1", "r1", json, "tester", DateTimeOffset.UtcNow));
Assert.Equal(DeploymentStatus.Success, ExpectMsg(TimeSpan.FromSeconds(15)).Status);
var instance = await Sys.ActorSelection(dm.Path / "Good1").ResolveOne(TimeSpan.FromSeconds(5));
instance.Tell(new GetAttributeRequest("g", "Good1", "A", DateTimeOffset.UtcNow));
Assert.Equal("7", ExpectMsg().Value?.ToString());
}
}