using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using ScadaLink.Commons.Interfaces.Protocol;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.DataConnectionLayer.Adapters;
using ZB.MOM.WW.LmxProxy.Client.Domain;
using QualityCode = ScadaLink.Commons.Interfaces.Protocol.QualityCode;

namespace ScadaLink.DataConnectionLayer.Tests;

public class LmxProxyDataConnectionTests
{
    private readonly ILmxProxyClient _mockClient;
    private readonly ILmxProxyClientFactory _mockFactory;
    private readonly LmxProxyDataConnection _adapter;

    public LmxProxyDataConnectionTests()
    {
        _mockClient = Substitute.For<ILmxProxyClient>();
        _mockFactory = Substitute.For<ILmxProxyClientFactory>();
        _mockFactory.Create(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<string?>(), Arg.Any<bool>()).Returns(_mockClient);
        _adapter = new LmxProxyDataConnection(_mockFactory, NullLogger<LmxProxyDataConnection>.Instance);
    }

    private async Task ConnectAdapter(Dictionary<string, string>? details = null)
    {
        _mockClient.IsConnected.Returns(true);
        await _adapter.ConnectAsync(details ?? new Dictionary<string, string>());
    }

    // --- Connection ---

    [Fact]
    public async Task Connect_SetsStatusToConnected()
    {
        _mockClient.IsConnected.Returns(true);

        await _adapter.ConnectAsync(new Dictionary<string, string>
        {
            ["Host"] = "myhost",
            ["Port"] = "5001"
        });

        Assert.Equal(ConnectionHealth.Connected, _adapter.Status);
        _mockFactory.Received(1).Create("myhost", 5001, null, false);
        await _mockClient.Received(1).ConnectAsync(Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Connect_ExtractsApiKeyFromDetails()
    {
        _mockClient.IsConnected.Returns(true);

        await _adapter.ConnectAsync(new Dictionary<string, string>
        {
            ["Host"] = "server",
            ["Port"] = "50051",
            ["ApiKey"] = "my-secret-key"
        });

        _mockFactory.Received(1).Create("server", 50051, "my-secret-key", false);
    }

    [Fact]
    public async Task Connect_DefaultsHostAndPort()
    {
        _mockClient.IsConnected.Returns(true);

        await _adapter.ConnectAsync(new Dictionary<string, string>());

        _mockFactory.Received(1).Create("localhost", 50051, null, false);
    }

    [Fact]
    public async Task Disconnect_SetsStatusToDisconnected()
    {
        await ConnectAdapter();
        await _adapter.DisconnectAsync();

        Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
        await _mockClient.Received(1).DisconnectAsync();
    }

    // --- Read ---

    [Fact]
    public async Task Read_Good_ReturnsSuccessWithValue()
    {
        await ConnectAdapter();
        var now = DateTime.UtcNow;
        _mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
            .Returns(new Vtq(42.5, now, Quality.Good));

        var result = await _adapter.ReadAsync("Tag1");

        Assert.True(result.Success);
        Assert.Equal(42.5, result.Value!.Value);
        Assert.Equal(QualityCode.Good, result.Value.Quality);
    }

    [Fact]
    public async Task Read_Bad_ReturnsFailureWithValue()
    {
        await ConnectAdapter();
        _mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
            .Returns(new Vtq(null, DateTime.UtcNow, Quality.Bad));

        var result = await _adapter.ReadAsync("Tag1");

        Assert.False(result.Success);
        Assert.NotNull(result.Value);
        Assert.Equal(QualityCode.Bad, result.Value!.Quality);
    }

    [Fact]
    public async Task Read_Uncertain_MapsQuality()
    {
        await ConnectAdapter();
        _mockClient.ReadAsync("Tag1", Arg.Any<CancellationToken>())
            .Returns(new Vtq("maybe", DateTime.UtcNow, Quality.Uncertain));

        var result = await _adapter.ReadAsync("Tag1");

        Assert.True(result.Success);
        Assert.Equal(QualityCode.Uncertain, result.Value!.Quality);
    }

    [Fact]
    public async Task ReadBatch_ReturnsMappedResults()
    {
        await ConnectAdapter();
        var now = DateTime.UtcNow;
        _mockClient.ReadBatchAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<CancellationToken>())
            .Returns(new Dictionary<string, Vtq>
            {
                ["Tag1"] = new(10, now, Quality.Good),
                ["Tag2"] = new(null, now, Quality.Bad)
            });

        var results = await _adapter.ReadBatchAsync(["Tag1", "Tag2"]);

        Assert.True(results["Tag1"].Success);
        Assert.Equal(10, results["Tag1"].Value!.Value);
        Assert.False(results["Tag2"].Success);
    }

    // --- Write ---

    [Fact]
    public async Task Write_Success_ReturnsGoodResult()
    {
        await ConnectAdapter();

        var result = await _adapter.WriteAsync("Tag1", 42);

        Assert.True(result.Success);
        await _mockClient.Received(1).WriteAsync("Tag1", Arg.Any<TypedValue>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Write_Failure_ReturnsError()
    {
        await ConnectAdapter();
        _mockClient.WriteAsync("Tag1", Arg.Any<TypedValue>(), Arg.Any<CancellationToken>())
            .Throws(new InvalidOperationException("Write failed for tag"));

        var result = await _adapter.WriteAsync("Tag1", 42);

        Assert.False(result.Success);
        Assert.Contains("Write failed for tag", result.ErrorMessage);
    }

    [Fact]
    public async Task WriteBatch_Success_ReturnsAllGood()
    {
        await ConnectAdapter();

        var results = await _adapter.WriteBatchAsync(new Dictionary<string, object?> { ["T1"] = 1, ["T2"] = 2 });

        Assert.True(results["T1"].Success);
        Assert.True(results["T2"].Success);
    }

    [Fact]
    public async Task WriteBatch_Failure_ReturnsAllErrors()
    {
        await ConnectAdapter();
        _mockClient.WriteBatchAsync(Arg.Any<IDictionary<string, TypedValue>>(), Arg.Any<CancellationToken>())
            .Throws(new InvalidOperationException("Batch write failed"));

        var results = await _adapter.WriteBatchAsync(new Dictionary<string, object?> { ["T1"] = 1, ["T2"] = 2 });

        Assert.False(results["T1"].Success);
        Assert.False(results["T2"].Success);
        Assert.Contains("Batch write failed", results["T1"].ErrorMessage);
    }

    // --- Subscribe ---

    [Fact]
    public async Task Subscribe_CreatesSubscriptionAndReturnsId()
    {
        await ConnectAdapter();
        var mockSub = Substitute.For<ILmxSubscription>();
        _mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, Vtq>>(), Arg.Any<Action<Exception>?>(), Arg.Any<CancellationToken>())
            .Returns(mockSub);

        var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });

        Assert.NotNull(subId);
        Assert.NotEmpty(subId);
        await _mockClient.Received(1).SubscribeAsync(
            Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, Vtq>>(), Arg.Any<Action<Exception>?>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Unsubscribe_DisposesSubscription()
    {
        await ConnectAdapter();
        var mockSub = Substitute.For<ILmxSubscription>();
        _mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, Vtq>>(), Arg.Any<Action<Exception>?>(), Arg.Any<CancellationToken>())
            .Returns(mockSub);

        var subId = await _adapter.SubscribeAsync("Tag1", (_, _) => { });
        await _adapter.UnsubscribeAsync(subId);

        await mockSub.Received(1).DisposeAsync();
    }

    [Fact]
    public async Task Unsubscribe_UnknownId_DoesNotThrow()
    {
        await ConnectAdapter();
        await _adapter.UnsubscribeAsync("nonexistent-id");
    }

    // --- Dispose ---

    [Fact]
    public async Task Dispose_DisposesClientAndSubscriptions()
    {
        await ConnectAdapter();
        var mockSub = Substitute.For<ILmxSubscription>();
        _mockClient.SubscribeAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<Action<string, Vtq>>(), Arg.Any<Action<Exception>?>(), Arg.Any<CancellationToken>())
            .Returns(mockSub);
        await _adapter.SubscribeAsync("Tag1", (_, _) => { });

        await _adapter.DisposeAsync();

        await mockSub.Received(1).DisposeAsync();
        await _mockClient.Received(1).DisposeAsync();
        Assert.Equal(ConnectionHealth.Disconnected, _adapter.Status);
    }

    // --- Guard ---

    [Fact]
    public async Task NotConnected_ThrowsOnRead()
    {
        _mockClient.IsConnected.Returns(false);

        await Assert.ThrowsAsync<InvalidOperationException>(() => _adapter.ReadAsync("tag1"));
    }

    [Fact]
    public async Task NotConnected_ThrowsOnWrite()
    {
        _mockClient.IsConnected.Returns(false);

        await Assert.ThrowsAsync<InvalidOperationException>(() => _adapter.WriteAsync("tag1", 1));
    }

    [Fact]
    public async Task NotConnected_ThrowsOnSubscribe()
    {
        _mockClient.IsConnected.Returns(false);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _adapter.SubscribeAsync("tag1", (_, _) => { }));
    }

    // --- Configuration Parsing ---

    [Fact]
    public async Task Connect_ParsesUseTls()
    {
        _mockClient.IsConnected.Returns(true);

        await _adapter.ConnectAsync(new Dictionary<string, string>
        {
            ["Host"] = "server",
            ["Port"] = "50051",
            ["UseTls"] = "true"
        });

        _mockFactory.Received(1).Create("server", 50051, null, true);
    }

    [Fact]
    public async Task Connect_DefaultsHostPortAndTls()
    {
        _mockClient.IsConnected.Returns(true);

        await _adapter.ConnectAsync(new Dictionary<string, string>());

        _mockFactory.Received(1).Create("localhost", 50051, null, false);
    }
}
