using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Server.Phase7;

namespace ZB.MOM.WW.OtOpcUa.Server.Tests.Phase7;

/// <summary>
///     Task #245 — covers the IReadable adapter that surfaces each scripted alarm's
///     live <c>ActiveState</c> so OPC UA variable reads on Source=ScriptedAlarm nodes
///     return the predicate truth instead of BadNotFound.
/// </summary>
[Trait("Category", "Unit")]
public sealed class ScriptedAlarmReadableTests
{
    private static (ScriptedAlarmEngine engine, CachedTagUpstreamSource upstream) BuildEngineWith(
        params (string alarmId, string predicateSource)[] alarms)
    {
        var upstream = new CachedTagUpstreamSource();
        var logger = new LoggerConfiguration().CreateLogger();
        var factory = new ScriptLoggerFactory(logger);
        var engine = new ScriptedAlarmEngine(upstream, new InMemoryAlarmStateStore(), factory, logger);
        var defs = alarms.Select(a => new ScriptedAlarmDefinition(
            AlarmId: a.alarmId,
            EquipmentPath: "/eq",
            AlarmName: a.alarmId,
            Kind: AlarmKind.LimitAlarm,
            Severity: AlarmSeverity.Medium,
            MessageTemplate: "x",
            PredicateScriptSource: a.predicateSource)).ToList();
        engine.LoadAsync(defs, CancellationToken.None).GetAwaiter().GetResult();
        return (engine, upstream);
    }

    [Fact]
    public async Task Reads_return_false_for_newly_loaded_alarm_with_inactive_predicate()
    {
        var (engine, _) = BuildEngineWith(("a1", "return false;"));
        using var _e = engine;
        var readable = new ScriptedAlarmReadable(engine);

        var result = await readable.ReadAsync(["a1"], CancellationToken.None);

        result.Count.ShouldBe(1);
        result[0].Value.ShouldBe(false);
        result[0].StatusCode.ShouldBe(0u, "Good quality when the engine has state");
    }

    [Fact]
    public async Task Reads_return_true_when_predicate_evaluates_to_active()
    {
        var (engine, upstream) = BuildEngineWith(
            ("tempAlarm", "return ctx.GetTag(\"/Site/Line/Cell/Temp\").Value is double d && d > 100;"));
        using var _e = engine;

        // Seed the upstream value + nudge the engine so the alarm transitions to Active.
        upstream.Push("/Site/Line/Cell/Temp",
            new DataValueSnapshot(150.0, 0u, DateTime.UtcNow, DateTime.UtcNow));

        // Allow the engine's change-driven cascade to run.
        await Task.Delay(50);

        var readable = new ScriptedAlarmReadable(engine);
        var result = await readable.ReadAsync(["tempAlarm"], CancellationToken.None);

        result[0].Value.ShouldBe(true);
    }

    [Fact]
    public async Task Reads_return_BadNodeIdUnknown_for_missing_alarm()
    {
        var (engine, _) = BuildEngineWith(("a1", "return false;"));
        using var _e = engine;
        var readable = new ScriptedAlarmReadable(engine);

        var result = await readable.ReadAsync(["a-not-loaded"], CancellationToken.None);

        result[0].Value.ShouldBeNull();
        result[0].StatusCode.ShouldBe(0x80340000u,
            "BadNodeIdUnknown surfaces a misconfiguration, not a silent false");
    }

    [Fact]
    public async Task Reads_batch_round_trip_preserves_order()
    {
        var (engine, _) = BuildEngineWith(
            ("a1", "return false;"),
            ("a2", "return false;"));
        using var _e = engine;
        var readable = new ScriptedAlarmReadable(engine);

        var result = await readable.ReadAsync(["a2", "missing", "a1"], CancellationToken.None);

        result.Count.ShouldBe(3);
        result[0].Value.ShouldBe(false);               // a2
        result[1].StatusCode.ShouldBe(0x80340000u);    // missing
        result[2].Value.ShouldBe(false);               // a1
    }

    [Fact]
    public void Null_engine_rejected()
    {
        Should.Throw<ArgumentNullException>(() => new ScriptedAlarmReadable(null!));
    }

    [Fact]
    public async Task Null_fullReferences_rejected()
    {
        var (engine, _) = BuildEngineWith(("a1", "return false;"));
        using var _e = engine;
        var readable = new ScriptedAlarmReadable(engine);

        await Should.ThrowAsync<ArgumentNullException>(
            () => readable.ReadAsync(null!, CancellationToken.None));
    }
}
