using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters;

// Non-parallel with the rest of the DataConnectionManagerActor collection: these adapter
// tests pump a fire-and-forget event loop (ConnectAsync → Task.Run(RunEventLoopAsync)) whose
// thread-pool continuation must run before the WaitUntil attach-barrier observes
// fake.OnUpdate. Serializing within the assembly trims in-assembly thread-pool contention so
// that barrier (and the negative Unsubscribe assertion that follows it) is not starved.
[Collection("DataConnectionManagerActor")]
public class MxGatewayDataConnectionTests
{
    private static MxGatewayDataConnection NewAdapter(FakeMxGatewayClient fake) =>
        new(fake, NullLogger<MxGatewayDataConnection>.Instance);

    private static Dictionary<string, string> Details(int writeUserId = 0) => new()
    {
        ["Endpoint"] = "http://gw:5000",
        ["ApiKey"] = "key",
        ["ClientName"] = "client-a",
        ["WriteUserId"] = writeUserId.ToString(),
        ["ReadTimeoutMs"] = "2000",
    };

    // ── Task 6: connect / status / Disconnected ──

    [Fact]
    public async Task ConnectAsync_resolves_options_and_sets_status_connected()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);

        await adapter.ConnectAsync(Details(writeUserId: 7));

        Assert.Equal(ConnectionHealth.Connected, adapter.Status);
        Assert.NotNull(fake.ConnectedWith);
        Assert.Equal("http://gw:5000", fake.ConnectedWith!.Endpoint);
        Assert.Equal("client-a", fake.ConnectedWith.ClientName);
        Assert.Equal(7, fake.ConnectedWith.WriteUserId);
    }

    [Fact]
    public async Task ConnectAsync_blank_client_name_defaults_to_scadabridge()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);
        var details = Details();
        details["ClientName"] = "";

        await adapter.ConnectAsync(details);

        Assert.Equal("scadabridge", fake.ConnectedWith!.ClientName);
    }

    [Fact]
    public async Task Disconnected_fires_exactly_once_when_event_loop_faults()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);
        int raised = 0;
        adapter.Disconnected += () => Interlocked.Increment(ref raised);

        await adapter.ConnectAsync(Details());
        // Wait for the event loop to attach.
        await WaitUntil(() => fake.OnUpdate is not null);

        fake.FaultEventLoop();
        await WaitUntil(() => raised >= 1);

        Assert.Equal(1, raised);
        Assert.Equal(ConnectionHealth.Disconnected, adapter.Status);
    }

    // ── Task 7: subscribe / unsubscribe + event routing ──

    [Fact]
    public async Task Subscribed_tag_update_invokes_callback_with_mapped_TagValue()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());
        await WaitUntil(() => fake.OnUpdate is not null);

        TagValue? received = null;
        await adapter.SubscribeAsync("Area.Pump.Speed", (_, v) => received = v);
        Assert.Contains("Area.Pump.Speed", fake.Subscribed);

        var ts = DateTimeOffset.UtcNow;
        fake.OnUpdate!(new MxValueUpdate("Area.Pump.Speed", 42.0, QualityCode.Good, ts));

        Assert.NotNull(received);
        Assert.Equal(42.0, received!.Value);
        Assert.Equal(QualityCode.Good, received.Quality);
        Assert.Equal(ts, received.Timestamp);
    }

    [Fact]
    public async Task Unsubscribe_stops_routing_updates()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());
        await WaitUntil(() => fake.OnUpdate is not null);

        int hits = 0;
        var subId = await adapter.SubscribeAsync("T", (_, _) => hits++);
        await adapter.UnsubscribeAsync(subId);

        fake.OnUpdate!(new MxValueUpdate("T", 1, QualityCode.Good, DateTimeOffset.UtcNow));

        Assert.Equal(0, hits);
        Assert.Contains(subId, fake.Unsubscribed);
    }

    // ── Task 8: read / write + error classification ──

    [Fact]
    public async Task ReadAsync_maps_success_and_failure()
    {
        var fake = new FakeMxGatewayClient
        {
            ReadHandler = tags => tags.Select(t => t == "ok"
                ? new MxReadOutcome(t, true, 5, QualityCode.Good, DateTimeOffset.UtcNow, null)
                : new MxReadOutcome(t, false, null, QualityCode.Bad, default, "bad tag")).ToList()
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var ok = await adapter.ReadAsync("ok");
        Assert.True(ok.Success);
        Assert.Equal(5, ok.Value!.Value);

        var bad = await adapter.ReadAsync("nope");
        Assert.False(bad.Success);
        Assert.Equal("bad tag", bad.ErrorMessage);
    }

    [Fact]
    public async Task ReadBatchAsync_returns_dictionary_keyed_by_tag()
    {
        var fake = new FakeMxGatewayClient
        {
            ReadHandler = tags => tags.Select(t =>
                new MxReadOutcome(t, true, t.Length, QualityCode.Good, DateTimeOffset.UtcNow, null)).ToList()
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var results = await adapter.ReadBatchAsync(new[] { "aa", "bbb" });
        Assert.Equal(2, results["aa"].Value!.Value);
        Assert.Equal(3, results["bbb"].Value!.Value);
    }

    [Fact]
    public async Task WriteAsync_maps_failure_to_unsuccessful_WriteResult()
    {
        var fake = new FakeMxGatewayClient
        {
            WriteHandler = writes => writes.Select(w =>
                new MxWriteOutcome(w.TagPath, false, "rejected")).ToList()
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var r = await adapter.WriteAsync("T", 1);
        Assert.False(r.Success);
        Assert.Equal("rejected", r.ErrorMessage);
    }

    // ── Task 9: WriteBatchAndWait ──

    [Fact]
    public async Task WriteBatchAndWait_returns_true_when_response_matches()
    {
        var writeCalls = new List<string>();
        var fake = new FakeMxGatewayClient
        {
            WriteHandler = writes =>
            {
                writeCalls.AddRange(writes.Select(w => w.TagPath));
                return writes.Select(w => new MxWriteOutcome(w.TagPath, true, null)).ToList();
            },
            // Response path already reads the expected value.
            ReadHandler = tags => tags.Select(t =>
                new MxReadOutcome(t, true, "DONE", QualityCode.Good, DateTimeOffset.UtcNow, null)).ToList()
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var ok = await adapter.WriteBatchAndWaitAsync(
            new Dictionary<string, object?> { ["V"] = 1 }, "Flag", 1, "Resp", "DONE",
            TimeSpan.FromSeconds(5));

        Assert.True(ok);
        // Values written before the flag, and the flag itself.
        Assert.Contains("V", writeCalls);
        Assert.Contains("Flag", writeCalls);
    }

    [Fact]
    public async Task WriteBatchAndWait_returns_false_on_timeout()
    {
        var fake = new FakeMxGatewayClient
        {
            WriteHandler = writes => writes.Select(w => new MxWriteOutcome(w.TagPath, true, null)).ToList(),
            ReadHandler = tags => tags.Select(t =>
                new MxReadOutcome(t, true, "NEVER", QualityCode.Good, DateTimeOffset.UtcNow, null)).ToList()
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var ok = await adapter.WriteBatchAndWaitAsync(
            new Dictionary<string, object?> { ["V"] = 1 }, "Flag", 1, "Resp", "DONE",
            TimeSpan.FromMilliseconds(300));

        Assert.False(ok);
    }

    // ── Task 10: browse ──

    [Fact]
    public async Task BrowseChildrenAsync_maps_children_and_truncated()
    {
        var fake = new FakeMxGatewayClient
        {
            BrowseHandler = _ => (new List<MxBrowseChild>
            {
                new("Area1", "Area1", BrowseNodeClass.Object, true),
                new("Area1.Pump.Speed", "Speed", BrowseNodeClass.Variable, false),
            }, true)
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var result = await adapter.BrowseChildrenAsync(null);

        Assert.True(result.Truncated);
        Assert.Equal(2, result.Children.Count);
        Assert.Equal(BrowseNodeClass.Object, result.Children[0].NodeClass);
        Assert.True(result.Children[0].HasChildren);
        Assert.Equal("Area1.Pump.Speed", result.Children[1].NodeId);
        Assert.Equal(BrowseNodeClass.Variable, result.Children[1].NodeClass);
    }

    [Fact]
    public async Task BrowseChildrenAsync_surfaces_attribute_datatype()
    {
        var fake = new FakeMxGatewayClient
        {
            BrowseHandler = _ => (new List<MxBrowseChild>
            {
                new("Area1.Pump.Speed", "Speed", BrowseNodeClass.Variable, false, "Double"),
            }, false)
        };
        var adapter = NewAdapter(fake);
        await adapter.ConnectAsync(Details());

        var result = await adapter.BrowseChildrenAsync(null);

        Assert.Equal("Double", result.Children[0].DataType);
    }

    [Fact]
    public async Task BrowseChildrenAsync_throws_when_not_connected()
    {
        var fake = new FakeMxGatewayClient();
        var adapter = NewAdapter(fake);
        // Not connected.
        await Assert.ThrowsAsync<ConnectionNotConnectedException>(
            () => adapter.BrowseChildrenAsync(null));
    }

    // Generous ceiling (30 s, matching the DataConnectionManagerActorCollection rationale):
    // every caller polls a *monotonic, positive* barrier — fake.OnUpdate becoming non-null
    // (the fire-and-forget event-loop task has attached) or a Disconnected count reaching its
    // target. Both only ever transition once and never reset, so a longer ceiling merely
    // tolerates thread-pool starvation under full-solution CPU oversubscription; it cannot
    // produce a false pass. In Unsubscribe_stops_routing_updates this barrier establishes the
    // happens-before that the loop is attached BEFORE the test removes the subscription; the
    // negative assertion (hits == 0) runs afterwards and is independent of this timeout —
    // the update is delivered synchronously by the test thread, so widening here neither
    // weakens nor races the negative check.
    private static async Task WaitUntil(Func<bool> condition, int timeoutMs = 30_000)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        while (!condition() && sw.ElapsedMilliseconds < timeoutMs)
            await Task.Delay(10);
        Assert.True(condition(), "Condition not met within timeout.");
    }
}
