using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;

/// <summary>
///     v3 RawPath identity: the server addresses Galaxy nodes by RawPath, while MXAccess dials the
///     Galaxy attributeRef (<c>tag_name.AttributeName</c>). These tests pin the driver-side boundary
///     translation built from <see cref="GalaxyDriverOptions.RawTags"/> — RawPath → attributeRef on
///     read / write / subscribe / ack (forward dial), and attributeRef → RawPath on the value fan-out
///     and alarm ConditionId (reverse), so the alarm resolves back to the same RawPath the value path
///     surfaces. Each entry's <c>TagConfig</c> carries the dialled address under the camelCase key
///     <c>attributeRef</c>.
/// </summary>
public sealed class GalaxyDriverRawPathKeyingTests
{
    private const string RawPath = "area/line/tank1/level";
    private const string AttributeRef = "Tank1.Level";

    private static GalaxyDriverOptions OptsWithRawTags(bool writeIdempotent = false) => new(
        new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
        new GalaxyMxAccessOptions("OtOpcUa-A"),
        new GalaxyRepositoryOptions(),
        new GalaxyReconnectOptions())
    {
        RawTags = [new RawTagEntry(RawPath, $"{{\"attributeRef\":\"{AttributeRef}\"}}", writeIdempotent)],
    };

    // ===== Read: RawPath in → attributeRef dialled =====

    /// <summary>ReadAsync translates the incoming RawPath to the Galaxy attributeRef before dialling.</summary>
    [Fact]
    public async Task ReadAsync_TranslatesRawPath_ToAttributeRef()
    {
        var reader = new RecordingReader();
        using var driver = new GalaxyDriver("g", OptsWithRawTags(), hierarchySource: null, dataReader: reader);

        await driver.ReadAsync([RawPath], CancellationToken.None);

        reader.LastRequest.ShouldBe([AttributeRef]);
    }

    /// <summary>An unmapped RawPath dials as-is (identity) — the gateway then rejects an unknown address.</summary>
    [Fact]
    public async Task ReadAsync_UnmappedRawPath_DialsIdentity()
    {
        var reader = new RecordingReader();
        using var driver = new GalaxyDriver("g", OptsWithRawTags(), hierarchySource: null, dataReader: reader);

        await driver.ReadAsync(["not/authored"], CancellationToken.None);

        reader.LastRequest.ShouldBe(["not/authored"]);
    }

    // ===== Write: RawPath in → attributeRef dialled, security resolved by RawPath =====

    /// <summary>
    ///     WriteAsync dials the attributeRef, and the security the writer resolves is the one captured at
    ///     discovery under the tag's RawPath (the discoverer emitted FullName = RawPath) — proving the
    ///     reverse mapping the resolver applies (attributeRef → RawPath) lands on the right classification.
    /// </summary>
    [Fact]
    public async Task WriteAsync_DialsAttributeRef_AndResolvesSecurityByRawPath()
    {
        var writer = new RecordingWriter();
        // Hierarchy: Tank1.Level classified Operate (sec 1). Discovery keys security by the RawPath.
        var src = new FakeHierarchySource([Obj("Tank1", Attr("Level", sec: 1))]);
        using var driver = new GalaxyDriver(
            "g", OptsWithRawTags(), hierarchySource: src, dataReader: null, dataWriter: writer);

        await driver.DiscoverAsync(new CapturingBuilder(), CancellationToken.None);
        await driver.WriteAsync([new WriteRequest(RawPath, 42.0)], CancellationToken.None);

        writer.Calls.ShouldHaveSingleItem();
        writer.Calls[0].FullRef.ShouldBe(AttributeRef);                       // dialled address
        writer.Calls[0].Resolved.ShouldBe(SecurityClassification.Operate);    // resolved by RawPath
    }

    // ===== Subscribe + value fan-out: RawPath in → attributeRef dialled → RawPath out =====

    /// <summary>
    ///     SubscribeAsync dials the attributeRef; the resulting value fan-out surfaces on the public
    ///     OnDataChange keyed by the tag's RawPath — the same identity the alarm ConditionId uses.
    /// </summary>
    [Fact]
    public async Task Subscribe_DialsAttributeRef_AndValueFanOutSurfacesRawPath()
    {
        var subscriber = new GalaxyDriverSubscribeTests.FakeSubscriber();
        using var driver = new GalaxyDriver(
            "g", OptsWithRawTags(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber);

        var captured = new List<DataChangeEventArgs>();
        driver.OnDataChange += (_, args) => captured.Add(args);

        await driver.SubscribeAsync([RawPath], TimeSpan.FromSeconds(1), CancellationToken.None);

        // The gateway saw the dialled attributeRef, not the RawPath.
        subscriber.Map.ShouldContainKey(AttributeRef);
        subscriber.Map.ShouldNotContainKey(RawPath);

        await subscriber.EmitOnDataChangeAsync(subscriber.Map[AttributeRef], 42.0);
        await WaitForAsync(() => captured.Count >= 1);

        captured.ShouldHaveSingleItem();
        captured[0].FullReference.ShouldBe(RawPath); // surfaced under the RawPath identity
        ((double)captured[0].Snapshot.Value!).ShouldBe(42.0);
    }

    // ===== Alarm: ConditionId is the RawPath; ack round-trips back to the attributeRef =====

    /// <summary>
    ///     The alarm feed reports the Galaxy attributeRef; the driver surfaces the transition with
    ///     ConditionId = the tag's RawPath (matching the value path), and a subsequent ack keyed by that
    ///     RawPath dials back down to the attributeRef the gateway acknowledges.
    /// </summary>
    [Fact]
    public async Task Alarm_ConditionIdIsRawPath_AndAckRoundTripsToAttributeRef()
    {
        var feed = new FakeAlarmFeed();
        var ack = new RecordingAcknowledger();
        using var driver = new GalaxyDriver(
            "g", OptsWithRawTags(), hierarchySource: null, alarmAcknowledger: ack, alarmFeed: feed);

        var handle = await driver.SubscribeAlarmsAsync(["Tank1"], CancellationToken.None);
        var observed = new List<AlarmEventArgs>();
        driver.OnAlarmEvent += (_, args) => observed.Add(args);

        // Feed reports the dialled attributeRef.
        feed.Emit(NewTransition(AttributeRef, "Tank1"));

        observed.ShouldHaveSingleItem();
        observed[0].ConditionId.ShouldBe(RawPath);            // reverse-mapped to the RawPath identity
        observed[0].SubscriptionHandle.ShouldBe(handle);

        // Ack keyed by the RawPath the driver reported → dials back to the attributeRef.
        await driver.AcknowledgeAsync(
            [new AlarmAcknowledgeRequest("Tank1", RawPath, "ack it")], CancellationToken.None);

        ack.Calls.ShouldHaveSingleItem();
        ack.Calls[0].AlarmRef.ShouldBe(AttributeRef);
    }

    // ===== helpers / fakes =====

    private static async Task WaitForAsync(Func<bool> condition, int timeoutMs = 2000)
    {
        var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs);
        while (DateTime.UtcNow < deadline)
        {
            if (condition()) return;
            await Task.Delay(10);
        }
    }

    private static GalaxyAttribute Attr(string name, int sec)
        => new() { AttributeName = name, MxDataType = 2 /*Float32*/, SecurityClassification = sec };

    private static GalaxyObject Obj(string tag, params GalaxyAttribute[] attrs)
    {
        var o = new GalaxyObject { TagName = tag, ContainedName = tag };
        o.Attributes.AddRange(attrs);
        return o;
    }

    private static GalaxyAlarmTransition NewTransition(string alarmFullReference, string source)
        => new(
            AlarmFullReference: alarmFullReference,
            SourceObjectReference: source,
            AlarmTypeName: "AnalogLimitAlarm.HiHi",
            TransitionKind: GalaxyAlarmTransitionKind.Raise,
            SeverityBucket: AlarmSeverity.High,
            OpcUaSeverity: 800,
            RawMxAccessSeverity: 750,
            OriginalRaiseTimestampUtc: null,
            TransitionTimestampUtc: DateTime.UtcNow,
            OperatorUser: string.Empty,
            OperatorComment: string.Empty,
            Category: "Process",
            Description: "high level");

    private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
    {
        public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
            => Task.FromResult(objects);
    }

    /// <summary>Minimal builder — discovery only needs the SecurityCapturingBuilder wrapper to run.</summary>
    private sealed class CapturingBuilder : IAddressSpaceBuilder
    {
        public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
            => new Handle(attributeInfo.FullName);
        public void AddProperty(string browseName, DriverDataType dataType, object? value) { }

        private sealed class Handle(string fullRef) : IVariableHandle
        {
            public string FullReference { get; } = fullRef;
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new Sink();
            private sealed class Sink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } }
        }
    }

    private sealed class RecordingReader : IGalaxyDataReader
    {
        public IReadOnlyList<string>? LastRequest { get; private set; }

        public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
            IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
        {
            LastRequest = fullReferences;
            return Task.FromResult<IReadOnlyList<DataValueSnapshot>>(
                fullReferences.Select(_ => new DataValueSnapshot(0.0, StatusCodeMap.Good, null, DateTime.UtcNow)).ToArray());
        }
    }

    private sealed class RecordingWriter : IGalaxyDataWriter
    {
        public List<(string FullRef, object? Value, SecurityClassification Resolved)> Calls { get; } = [];

        public Task<IReadOnlyList<WriteResult>> WriteAsync(
            IReadOnlyList<WriteRequest> writes,
            Func<string, SecurityClassification> securityResolver,
            CancellationToken cancellationToken)
        {
            var results = new WriteResult[writes.Count];
            for (var i = 0; i < writes.Count; i++)
            {
                Calls.Add((writes[i].FullReference, writes[i].Value, securityResolver(writes[i].FullReference)));
                results[i] = new WriteResult(StatusCodeMap.Good);
            }
            return Task.FromResult<IReadOnlyList<WriteResult>>(results);
        }

        public void InvalidateHandleCaches() { }
    }

    private sealed class FakeAlarmFeed : IGalaxyAlarmFeed
    {
        public event EventHandler<GalaxyAlarmTransition>? OnAlarmTransition;
        public void Start() { }
        public void Emit(GalaxyAlarmTransition transition) => OnAlarmTransition?.Invoke(this, transition);
        public ValueTask DisposeAsync() => ValueTask.CompletedTask;
    }

    private sealed class RecordingAcknowledger : IGalaxyAlarmAcknowledger
    {
        public List<(string AlarmRef, string Comment, string Operator)> Calls { get; } = [];

        public Task AcknowledgeAsync(string alarmFullReference, string comment, string operatorUser, CancellationToken cancellationToken)
        {
            Calls.Add((alarmFullReference, comment, operatorUser));
            return Task.CompletedTask;
        }
    }
}
