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

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

/// <summary>
///     Phase 7 follow-up (task #243) — verifies the composer that maps Config DB
///     rows to runtime engine definitions + wires up VirtualTagEngine +
///     ScriptedAlarmEngine + historian routing.
/// </summary>
[Trait("Category", "Unit")]
public sealed class Phase7EngineComposerTests
{
    private static Script ScriptRow(string id, string source) => new()
    {
        ScriptRowId = Guid.NewGuid(), GenerationId = 1,
        ScriptId = id, Name = id, SourceCode = source, SourceHash = "h",
    };

    private static VirtualTag VtRow(string id, string scriptId) => new()
    {
        VirtualTagRowId = Guid.NewGuid(), GenerationId = 1,
        VirtualTagId = id, EquipmentId = "eq-1", Name = id,
        DataType = "Float32", ScriptId = scriptId,
    };

    private static ScriptedAlarm AlarmRow(string id, string scriptId) => new()
    {
        ScriptedAlarmRowId = Guid.NewGuid(), GenerationId = 1,
        ScriptedAlarmId = id, EquipmentId = "eq-1", Name = id,
        AlarmType = "LimitAlarm", Severity = 500,
        MessageTemplate = "x", PredicateScriptId = scriptId,
    };

    [Fact]
    public void Compose_empty_rows_returns_Empty_sentinel()
    {
        var result = Phase7EngineComposer.Compose(
            scripts: [],
            virtualTags: [],
            scriptedAlarms: [],
            upstream: new CachedTagUpstreamSource(),
            alarmStateStore: new InMemoryAlarmStateStore(),
            historianSink: NullAlarmHistorianSink.Instance,
            rootScriptLogger: new LoggerConfiguration().CreateLogger(),
            loggerFactory: NullLoggerFactory.Instance);

        result.ShouldBeSameAs(Phase7ComposedSources.Empty);
        result.VirtualReadable.ShouldBeNull();
        result.ScriptedAlarmReadable.ShouldBeNull();
    }

    [Fact]
    public void Compose_VirtualTag_rows_returns_non_null_VirtualReadable()
    {
        var scripts = new[] { ScriptRow("scr-1", "return 1;") };
        var vtags = new[] { VtRow("vt-1", "scr-1") };

        var result = Phase7EngineComposer.Compose(
            scripts, vtags, [],
            upstream: new CachedTagUpstreamSource(),
            alarmStateStore: new InMemoryAlarmStateStore(),
            historianSink: NullAlarmHistorianSink.Instance,
            rootScriptLogger: new LoggerConfiguration().CreateLogger(),
            loggerFactory: NullLoggerFactory.Instance);

        result.VirtualReadable.ShouldNotBeNull();
        result.ScriptedAlarmReadable.ShouldBeNull("no alarms configured");
        result.Disposables.Count.ShouldBeGreaterThan(0);
    }

    [Fact]
    public void Compose_ScriptedAlarm_rows_returns_non_null_ScriptedAlarmReadable()
    {
        var scripts = new[] { ScriptRow("scr-1", "return false;") };
        var alarms = new[] { AlarmRow("al-1", "scr-1") };

        var result = Phase7EngineComposer.Compose(
            scripts, [], alarms,
            upstream: new CachedTagUpstreamSource(),
            alarmStateStore: new InMemoryAlarmStateStore(),
            historianSink: NullAlarmHistorianSink.Instance,
            rootScriptLogger: new LoggerConfiguration().CreateLogger(),
            loggerFactory: NullLoggerFactory.Instance);

        result.ScriptedAlarmReadable.ShouldNotBeNull("task #245 — alarm Active state readable");
        result.VirtualReadable.ShouldBeNull();
    }

    [Fact]
    public void Compose_missing_script_reference_throws_with_actionable_message()
    {
        var vtags = new[] { VtRow("vt-1", "scr-missing") };

        Should.Throw<InvalidOperationException>(() =>
            Phase7EngineComposer.Compose(
                scripts: [],
                vtags, [],
                upstream: new CachedTagUpstreamSource(),
                alarmStateStore: new InMemoryAlarmStateStore(),
                historianSink: NullAlarmHistorianSink.Instance,
                rootScriptLogger: new LoggerConfiguration().CreateLogger(),
                loggerFactory: NullLoggerFactory.Instance))
            .Message.ShouldContain("scr-missing");
    }

    [Fact]
    public void Compose_disabled_VirtualTag_is_skipped()
    {
        var scripts = new[] { ScriptRow("scr-1", "return 1;") };
        var disabled = VtRow("vt-1", "scr-1");
        disabled.Enabled = false;

        var defs = Phase7EngineComposer.ProjectVirtualTags(
            new[] { disabled },
            new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).ToList();

        defs.ShouldBeEmpty();
    }

    [Fact]
    public void ProjectVirtualTags_maps_timer_interval_milliseconds_to_TimeSpan()
    {
        var scripts = new[] { ScriptRow("scr-1", "return 1;") };
        var vt = VtRow("vt-1", "scr-1");
        vt.TimerIntervalMs = 2500;

        var def = Phase7EngineComposer.ProjectVirtualTags(
            new[] { vt },
            new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).Single();

        def.TimerInterval.ShouldBe(TimeSpan.FromMilliseconds(2500));
    }

    [Fact]
    public void ProjectScriptedAlarms_maps_Severity_numeric_to_AlarmSeverity_bucket()
    {
        var scripts = new[] { ScriptRow("scr-1", "return true;") };

        var buckets = new[] { (1, AlarmSeverity.Low), (250, AlarmSeverity.Low),
                               (251, AlarmSeverity.Medium), (500, AlarmSeverity.Medium),
                               (501, AlarmSeverity.High), (750, AlarmSeverity.High),
                               (751, AlarmSeverity.Critical), (1000, AlarmSeverity.Critical) };
        foreach (var (input, expected) in buckets)
        {
            var row = AlarmRow("a1", "scr-1");
            row.Severity = input;
            var def = Phase7EngineComposer.ProjectScriptedAlarms(
                new[] { row },
                new Dictionary<string, Script> { ["scr-1"] = scripts[0] }).Single();
            def.Severity.ShouldBe(expected, $"severity {input} should map to {expected}");
        }
    }
}

