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>
///     Tests for <see cref="GalaxyDriver"/>'s <c>IWritable</c> wiring. Verifies the
///     SecurityClassification per-tag map gets populated during Discovery and routes the
///     subsequent WriteAsync calls to the right gateway command (Write vs WriteSecured).
///     The actual Write / WriteSecured invocation is tested separately at the
///     <see cref="GatewayGalaxyDataWriter"/> level — this test class focuses on the
///     driver-side wiring.
/// </summary>
public sealed class GalaxyDriverWriteTests
{
    private static GalaxyDriverOptions Opts() => new(
        new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
        new GalaxyMxAccessOptions("OtOpcUa-A"),
        new GalaxyRepositoryOptions(),
        new GalaxyReconnectOptions());

    private sealed class FakeHierarchySource(IReadOnlyList<GalaxyObject> objects) : IGalaxyHierarchySource
    {
        /// <summary>Returns the fake Galaxy object hierarchy.</summary>
        /// <param name="cancellationToken">Token to cancel the operation.</param>
        public Task<IReadOnlyList<GalaxyObject>> GetHierarchyAsync(CancellationToken cancellationToken)
            => Task.FromResult(objects);
    }

    private sealed class FakeBuilder : IAddressSpaceBuilder
    {
        /// <summary>Gets the list of variables added to this builder.</summary>
        public List<DriverAttributeInfo> Variables { get; } = [];

        /// <summary>Adds a folder and returns this builder for chaining.</summary>
        /// <param name="browseName">The browse name of the folder.</param>
        /// <param name="displayName">The display name of the folder.</param>
        public IAddressSpaceBuilder Folder(string browseName, string displayName) => this;
        /// <summary>Adds a variable to the variables list and returns a handle.</summary>
        /// <param name="browseName">The browse name of the variable.</param>
        /// <param name="displayName">The display name of the variable.</param>
        /// <param name="attributeInfo">The attribute information for the variable.</param>
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
        {
            Variables.Add(attributeInfo);
            return new FakeHandle(attributeInfo.FullName);
        }
        /// <summary>No-op property adding operation for test compatibility.</summary>
        /// <param name="browseName">The browse name of the property.</param>
        /// <param name="dataType">The data type of the property.</param>
        /// <param name="value">The value of the property.</param>
        public void AddProperty(string browseName, DriverDataType dataType, object? value) { }

        private sealed class FakeHandle(string fullRef) : IVariableHandle
        {
            /// <summary>Gets the full reference for this variable handle.</summary>
            public string FullReference { get; } = fullRef;
            /// <summary>Marks this variable as an alarm condition and returns a noop sink.</summary>
            /// <param name="info">The alarm condition information.</param>
            public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink();
            /// <summary>No-op alarm transition handler.</summary>
            private sealed class NoopSink : IAlarmConditionSink {
                /// <summary>Handles alarm state transition events.</summary>
                /// <param name="args">The alarm event arguments.</param>
                public void OnTransition(AlarmEventArgs args) { }
            }
        }
    }

    private sealed class FakeWriter : IGalaxyDataWriter
    {
        /// <summary>Gets the list of write calls received by this writer.</summary>
        public List<(string FullRef, object? Value, SecurityClassification Resolved)> Calls { get; } = [];

        /// <summary>Records write requests with their resolved security classifications.</summary>
        /// <param name="writes">The list of write requests to process.</param>
        /// <param name="securityResolver">Function to resolve security classification for each request.</param>
        /// <param name="cancellationToken">Token to cancel the operation.</param>
        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);
        }

        /// <inheritdoc />
        public void InvalidateHandleCaches() { /* no-op — this fake has no handle caches */ }
    }

    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;
    }

    /// <summary>Verifies that WriteAsync routes through the injected writer and propagates values correctly.</summary>
    [Fact]
    public async Task WriteAsync_RoutesThroughInjectedWriter_AndPropagatesValues()
    {
        var src = new FakeHierarchySource([
            Obj("Tank1_Level", Attr("PV", sec: 0 /*FreeAccess*/), Attr("SP", sec: 1 /*Operate*/)),
        ]);
        var writer = new FakeWriter();
        var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: src, dataReader: null, dataWriter: writer);

        var builder = new FakeBuilder();
        await driver.DiscoverAsync(builder, CancellationToken.None);

        await driver.WriteAsync([
            new WriteRequest("Tank1_Level.PV", 42.0),
            new WriteRequest("Tank1_Level.SP", 50.0),
        ], CancellationToken.None);

        writer.Calls.Count.ShouldBe(2);
        writer.Calls[0].Resolved.ShouldBe(SecurityClassification.FreeAccess);
        writer.Calls[1].Resolved.ShouldBe(SecurityClassification.Operate);
    }

    /// <summary>Verifies that WriteAsync resolves every security classification from discovery data.</summary>
    /// <param name="mxSec">The raw MXAccess security integer from the discovery attribute.</param>
    /// <param name="expected">The expected resolved security classification.</param>
    [Theory]
    [InlineData(0, SecurityClassification.FreeAccess)]
    [InlineData(1, SecurityClassification.Operate)]
    [InlineData(2, SecurityClassification.SecuredWrite)]
    [InlineData(3, SecurityClassification.VerifiedWrite)]
    [InlineData(4, SecurityClassification.Tune)]
    [InlineData(5, SecurityClassification.Configure)]
    [InlineData(6, SecurityClassification.ViewOnly)]
    public async Task WriteAsync_ResolvesEverySecurityClassification_FromDiscovery(int mxSec, SecurityClassification expected)
    {
        var src = new FakeHierarchySource([
            Obj("Tank", Attr("PV", sec: mxSec)),
        ]);
        var writer = new FakeWriter();
        var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: src, dataReader: null, dataWriter: writer);

        await driver.DiscoverAsync(new FakeBuilder(), CancellationToken.None);
        await driver.WriteAsync([new WriteRequest("Tank.PV", 1.0)], CancellationToken.None);

        writer.Calls[0].Resolved.ShouldBe(expected);
    }

    /// <summary>Verifies that unknown tags resolve to FreeAccess classification and writes proceed.</summary>
    [Fact]
    public async Task WriteAsync_UnknownTag_ResolvesToFreeAccess_DefaultsToWrite()
    {
        var writer = new FakeWriter();
        var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);

        // No DiscoverAsync call → classification map is empty → resolver returns FreeAccess
        // for any tag the gateway might attempt. WriteAsync must not throw on unknown tags.
        await driver.WriteAsync([new WriteRequest("Random.Tag", 1.0)], CancellationToken.None);

        writer.Calls[0].Resolved.ShouldBe(SecurityClassification.FreeAccess);
    }

    /// <summary>Verifies that an empty write request returns empty without calling the writer.</summary>
    [Fact]
    public async Task WriteAsync_EmptyRequest_ReturnsEmpty_WithoutCallingWriter()
    {
        var writer = new FakeWriter();
        var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);

        var result = await driver.WriteAsync([], CancellationToken.None);

        result.ShouldBeEmpty();
        writer.Calls.ShouldBeEmpty();
    }

    /// <summary>Verifies that WriteAsync throws a clear guard error when no writer is configured.</summary>
    [Fact]
    public async Task WriteAsync_NoWriter_Throws()
    {
        var driver = new GalaxyDriver("g", Opts(), hierarchySource: null);

        var ex = await Should.ThrowAsync<NotSupportedException>(() =>
            driver.WriteAsync([new WriteRequest("x", 1)], CancellationToken.None));
        ex.Message.ShouldContain("GalaxyDriver.WriteAsync requires");
    }

    /// <summary>Verifies that WriteAsync throws ObjectDisposedException after the driver is disposed.</summary>
    [Fact]
    public async Task WriteAsync_AfterDispose_Throws()
    {
        var writer = new FakeWriter();
        var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: writer);
        driver.Dispose();
        await Should.ThrowAsync<ObjectDisposedException>(() =>
            driver.WriteAsync([new WriteRequest("x", 1)], CancellationToken.None));
    }
}
