using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;

/// <summary>
///     Guards the failure-visibility surface added for archreview 01/S-1: the applier must report a
///     failed rebuild (<see cref="AddressSpaceApplyOutcome.RebuildFailed"/>) and tally swallowed per-node
///     sink failures (<see cref="AddressSpaceApplyOutcome.FailedNodes"/> for Apply's own passes; the
///     <c>Materialise*</c> passes return their own failed-node counts) instead of reporting optimistic
///     success while the running address space is stale or partial.
/// </summary>
public sealed class AddressSpaceApplierFailureSurfaceTests
{
    // ---------------- T1: RebuildFailed ----------------

    /// <summary>A rebuild whose sink call throws is reported as attempted-but-failed, not optimistic success.</summary>
    [Fact]
    public void Apply_WhenRebuildThrows_ReportsRebuildFailed()
    {
        var sink = new ConfigurableThrowingSink { ThrowOnRebuild = true };
        var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);

        var outcome = applier.Apply(RebuildingPlan("eq-1"));

        outcome.RebuildCalled.ShouldBeTrue();
        outcome.RebuildFailed.ShouldBeTrue();
    }

    /// <summary>A clean apply reports no failure on either channel — the regression guard for the new fields.</summary>
    [Fact]
    public void Apply_HappyPath_ReportsNoFailure()
    {
        var sink = new ConfigurableThrowingSink();
        var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);

        var outcome = applier.Apply(RebuildingPlan("eq-1"));

        outcome.RebuildCalled.ShouldBeTrue();
        outcome.RebuildFailed.ShouldBeFalse();
        outcome.FailedNodes.ShouldBe(0);
    }

    // ---------------- T2: FailedNodes (removal-pass tally) ----------------

    /// <summary>A removal-pass alarm-condition write that throws is counted into FailedNodes (swallowed today).</summary>
    [Fact]
    public void Apply_WhenRemovalConditionWriteThrows_CountsFailedNodes()
    {
        var sink = new ConfigurableThrowingSink { ThrowOnAlarmWrite = true };
        var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);

        var outcome = applier.Apply(EquipmentRemovalPlan("eq-1"));

        outcome.RemovedNodes.ShouldBe(1);
        outcome.FailedNodes.ShouldBe(1);
    }

    // ---------------- T3: Materialise* passes return failed-node counts ----------------

    /// <summary>Every EnsureVariable throwing in MaterialiseEquipmentTags is tallied into the pass's int return.</summary>
    [Fact]
    public void MaterialiseEquipmentTags_WhenEnsureVariableThrows_ReturnsFailedCount()
    {
        var sink = new ConfigurableThrowingSink { ThrowOnEnsureVariable = true };
        var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);

        var composition = CompositionWithTags(
            new EquipmentTagPlan("t1", "eq-1", "drv", FolderPath: "", Name: "A", DataType: "Float", FullName: "40001", Writable: false, Alarm: null),
            new EquipmentTagPlan("t2", "eq-1", "drv", FolderPath: "", Name: "B", DataType: "Float", FullName: "40002", Writable: false, Alarm: null),
            new EquipmentTagPlan("t3", "eq-1", "drv", FolderPath: "", Name: "C", DataType: "Float", FullName: "40003", Writable: false, Alarm: null));

        applier.MaterialiseEquipmentTags(composition).ShouldBe(3);
    }

    /// <summary>All five Materialise* passes return 0 failures on a clean sink — the happy-path regression.</summary>
    [Fact]
    public void Materialise_HappyPath_AllPassesReturnZero()
    {
        var sink = new ConfigurableThrowingSink();
        var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);

        var tags = CompositionWithTags(
            new EquipmentTagPlan("t1", "eq-1", "drv", FolderPath: "", Name: "A", DataType: "Float", FullName: "40001", Writable: false, Alarm: null));
        var vtags = CompositionWithVirtualTags(
            new EquipmentVirtualTagPlan("v1", "eq-1", FolderPath: "", Name: "VA", DataType: "Float", Expression: "1", DependencyRefs: Array.Empty<string>(), Historize: false));
        var alarms = CompositionWithScriptedAlarms(
            new EquipmentScriptedAlarmPlan("a1", "eq-1", "Alarm", "OffNormalAlarm", 500, "msg", "pred", "true",
                Array.Empty<string>(), HistorizeToAveva: false, Retain: true, Enabled: true));

        applier.MaterialiseHierarchy(tags).ShouldBe(0);
        applier.MaterialiseEquipmentTags(tags).ShouldBe(0);
        applier.MaterialiseEquipmentVirtualTags(vtags).ShouldBe(0);
        applier.MaterialiseScriptedAlarms(alarms).ShouldBe(0);
    }

    // ---------------- fixtures ----------------

    private static AddressSpaceComposition CompositionWithTags(params EquipmentTagPlan[] tags) =>
        new(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
        {
            EquipmentTags = tags,
        };

    private static AddressSpaceComposition CompositionWithVirtualTags(params EquipmentVirtualTagPlan[] vtags) =>
        new(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
        {
            EquipmentVirtualTags = vtags,
        };

    private static AddressSpaceComposition CompositionWithScriptedAlarms(params EquipmentScriptedAlarmPlan[] alarms) =>
        new(Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
        {
            EquipmentScriptedAlarms = alarms,
        };

    // A rebuild-forcing plan: a node-affecting ChangedEquipment delta classifies as Rebuild (R2-07), so
    // these tests exercise the SafeRebuild path (RebuildCalled/RebuildFailed) regardless of the pure-add
    // routing. A pure-add plan would no longer rebuild, so the failure-surface tests must use a change.
    private static AddressSpacePlan RebuildingPlan(string id) => new(
        AddedEquipment: Array.Empty<EquipmentNode>(),
        RemovedEquipment: Array.Empty<EquipmentNode>(),
        ChangedEquipment: new[]
        {
            new AddressSpacePlan.EquipmentDelta(
                new EquipmentNode(id, id, "line-1"),
                new EquipmentNode(id, id + "-renamed", "line-1")),
        },
        AddedDrivers: Array.Empty<DriverInstancePlan>(),
        RemovedDrivers: Array.Empty<DriverInstancePlan>(),
        ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
        AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
        RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
        ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());

    private static AddressSpacePlan EquipmentRemovalPlan(params string[] ids) => new(
        AddedEquipment: Array.Empty<EquipmentNode>(),
        RemovedEquipment: ids.Select(id => new EquipmentNode(id, id, "line-1")).ToArray(),
        ChangedEquipment: Array.Empty<AddressSpacePlan.EquipmentDelta>(),
        AddedDrivers: Array.Empty<DriverInstancePlan>(),
        RemovedDrivers: Array.Empty<DriverInstancePlan>(),
        ChangedDrivers: Array.Empty<AddressSpacePlan.DriverDelta>(),
        AddedAlarms: Array.Empty<ScriptedAlarmPlan>(),
        RemovedAlarms: Array.Empty<ScriptedAlarmPlan>(),
        ChangedAlarms: Array.Empty<AddressSpacePlan.AlarmDelta>());

    /// <summary>An <see cref="IOpcUaAddressSpaceSink"/> whose every sink call can be independently made to
    /// throw, so tests can drive each Safe* helper's catch branch. Non-throwing calls are no-ops.</summary>
    private sealed class ConfigurableThrowingSink : IOpcUaAddressSpaceSink
    {
        public bool ThrowOnRebuild { get; init; }
        public bool ThrowOnEnsureVariable { get; init; }
        public bool ThrowOnEnsureFolder { get; init; }
        public bool ThrowOnAlarmWrite { get; init; }
        public bool ThrowOnMaterialiseAlarm { get; init; }

        public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }

        public void WriteAlarmQuality(string alarmNodeId, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm) { }

        public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
        {
            if (ThrowOnAlarmWrite) throw new InvalidOperationException("simulated WriteAlarmCondition fault");
        }

        public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false)
        {
            if (ThrowOnMaterialiseAlarm) throw new InvalidOperationException("simulated MaterialiseAlarmCondition fault");
        }

        public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
        {
            if (ThrowOnEnsureFolder) throw new InvalidOperationException("simulated EnsureFolder fault");
        }

        public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
        {
            if (ThrowOnEnsureVariable) throw new InvalidOperationException("simulated EnsureVariable fault");
        }

        public void RebuildAddressSpace()
        {
            if (ThrowOnRebuild) throw new InvalidOperationException("simulated RebuildAddressSpace fault");
        }

        public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { }
        public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }
        public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
    }
}
