using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
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>IReadable</c> wiring. PR 4.2 ships the
///     abstraction (<see cref="IGalaxyDataReader"/>) and the wiring; PR 4.4 supplies the
///     production gateway-backed reader. These tests verify the wiring against a fake
///     reader plus the explicit "no reader → NotSupportedException" fallback that protects
///     deployments running on this PR from silently producing wrong reads.
/// </summary>
public sealed class GalaxyDriverReadTests
{
    private static GalaxyDriverOptions Opts() => new(
        new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
        new GalaxyMxAccessOptions("OtOpcUa-A"),
        new GalaxyRepositoryOptions(),
        new GalaxyReconnectOptions());

    private sealed class FakeReader : IGalaxyDataReader
    {
        /// <summary>Gets the last read request.</summary>
        public IReadOnlyList<string>? LastRequest { get; private set; }
        /// <summary>Gets or sets the function that decides the result for a given tag list.</summary>
        public Func<IReadOnlyList<string>, IReadOnlyList<DataValueSnapshot>> Decide { get; set; } =
            tags => tags.Select(t => new DataValueSnapshot(
                Value: t,
                StatusCode: StatusCodeMap.Good,
                SourceTimestampUtc: DateTime.UtcNow,
                ServerTimestampUtc: DateTime.UtcNow)).ToArray();

        /// <inheritdoc />
        public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
            IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
        {
            LastRequest = fullReferences;
            return Task.FromResult(Decide(fullReferences));
        }
    }

    /// <summary>Verifies that ReadAsync routes through the injected reader.</summary>
    [Fact]
    public async Task ReadAsync_RoutesThroughInjectedReader()
    {
        var reader = new FakeReader();
        var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);

        var result = await driver.ReadAsync(["Tank1.Level", "Tank2.Level"], CancellationToken.None);

        reader.LastRequest.ShouldBe(new[] { "Tank1.Level", "Tank2.Level" });
        result.Count.ShouldBe(2);
        result[0].Value.ShouldBe("Tank1.Level");
        result[0].StatusCode.ShouldBe(StatusCodeMap.Good);
    }

    /// <summary>Verifies that ReadAsync returns empty without calling the reader for an empty request.</summary>
    [Fact]
    public async Task ReadAsync_EmptyRequest_ReturnsEmpty_WithoutCallingReader()
    {
        var reader = new FakeReader();
        var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);

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

        result.ShouldBeEmpty();
        reader.LastRequest.ShouldBeNull();
    }

    /// <summary>Verifies that ReadAsync throws when seams and production runtime are not built.</summary>
    [Fact]
    public async Task ReadAsync_NoSeams_AndNoProductionRuntime_Throws()
    {
        // Construction without seams + without InitializeAsync gives a driver where
        // _dataReader and _subscriber are both null. The follow-up read path can't
        // synthesise a Read without one, so it surfaces a NotSupportedException
        // pointing at the misuse rather than NullRef'ing inside the pump path.
        var driver = new GalaxyDriver("g", Opts(), hierarchySource: null);

        var ex = await Should.ThrowAsync<NotSupportedException>(() =>
            driver.ReadAsync(["x"], CancellationToken.None));
        ex.Message.ShouldContain("production runtime not built");
    }

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

    /// <summary>Verifies that ReadAsync resolves from the first OnDataChange event on the subscribe-once path.</summary>
    [Fact]
    public async Task ReadAsync_SubscribeOncePath_ResolvesFromFirstOnDataChange()
    {
        // Follow-up #1: when no test reader is injected but a subscriber IS, the driver
        // synthesises a Read by subscribing, waiting for the first OnDataChange event
        // per item handle (gw pushes initial value), then unsubscribing.
        var subscriber = new GalaxyDriverSubscribeTests.FakeSubscriber();
        using var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber);

        var readTask = driver.ReadAsync(["Tank.Level"], CancellationToken.None);
        // Push the "initial value" event the gw would emit immediately after SubscribeBulk.
        await Task.Delay(50); // give SubscribeBulk a beat to register + handler to attach
        var itemHandle = subscriber.Map["Tank.Level"];
        await subscriber.EmitOnDataChangeAsync(itemHandle, 42.0);

        var result = await readTask;
        result.Count.ShouldBe(1);
        result[0].Value.ShouldBe(42.0);
        // Cleanup unsubscribed the live handle.
        subscriber.UnsubscribedHandles.ShouldContain(itemHandle);
    }

    /// <summary>Verifies that ReadAsync surfaces rejected tags as bad status on the subscribe-once path.</summary>
    [Fact]
    public async Task ReadAsync_SubscribeOncePath_RejectedTagSurfacesAsBadStatus()
    {
        // gw rejects "Bad" at SubscribeBulk; the read path completes that slot with a
        // Bad-status snapshot rather than waiting forever for an event that won't come.
        var subscriber = new GalaxyDriverSubscribeTests.FakeSubscriber { Decide = tag => tag != "Bad" };
        using var driver = new GalaxyDriver(
            "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber);

        var readTask = driver.ReadAsync(["Good", "Bad"], CancellationToken.None);
        await Task.Delay(50);
        await subscriber.EmitOnDataChangeAsync(subscriber.Map["Good"], 1.0);

        var result = await readTask;
        result.Count.ShouldBe(2);
        result[0].Value.ShouldBe(1.0);
        result[1].StatusCode.ShouldBe(0x80000000u); // Bad
    }

    /// <summary>Verifies that ReadAsync preserves reader status codes.</summary>
    [Fact]
    public async Task ReadAsync_PreservesReaderStatusCodes()
    {
        var reader = new FakeReader
        {
            Decide = tags => new DataValueSnapshot[]
            {
                new(42.0, StatusCodeMap.Good, DateTime.UtcNow, DateTime.UtcNow),
                new(null, StatusCodeMap.BadNotConnected, null, DateTime.UtcNow),
            },
        };
        var driver = new GalaxyDriver("g", Opts(), hierarchySource: null, dataReader: reader);

        var result = await driver.ReadAsync(["a", "b"], CancellationToken.None);

        result[0].StatusCode.ShouldBe(StatusCodeMap.Good);
        result[1].StatusCode.ShouldBe(StatusCodeMap.BadNotConnected);
    }
}
