using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;

/// <summary>
///     Unit tests for <see cref="S7Driver.ReinterpretRawValue"/> and
///     <see cref="S7Driver.BoxValueForWrite"/> — the pure read/write type-reinterpret
///     helpers factored out of <c>ReadOneAsync</c> / <c>WriteOneAsync</c> so they can be
///     exercised without a live PLC (Driver.S7-014).
/// </summary>
[Trait("Category", "Unit")]
public sealed class S7TypeMappingTests
{
    // ── Helpers ──────────────────────────────────────────────────────────────────────────────

    private static S7TagDefinition Tag(string name, S7DataType dt) =>
        new(name, "DB1.DBW0", dt);

    private static S7ParsedAddress Addr(S7Size size) =>
        new(S7Area.DataBlock, DbNumber: 1, size, ByteOffset: 0, BitOffset: 0);

    // ── ReinterpretRawValue — implemented types ───────────────────────────────────────────

    /// <summary>Verifies that bool values are returned unchanged.</summary>
    [Fact]
    public void ReinterpretRawValue_Bool_returns_bool()
    {
        var tag = new S7TagDefinition("B", "DB1.DBX0.0", S7DataType.Bool);
        var addr = new S7ParsedAddress(S7Area.DataBlock, 1, S7Size.Bit, 0, 0);
        S7Driver.ReinterpretRawValue(tag, addr, true).ShouldBe(true);
        S7Driver.ReinterpretRawValue(tag, addr, false).ShouldBe(false);
    }

    /// <summary>Verifies that byte values are returned unchanged.</summary>
    [Fact]
    public void ReinterpretRawValue_Byte_returns_byte()
    {
        var tag = new S7TagDefinition("By", "DB1.DBB0", S7DataType.Byte);
        var addr = Addr(S7Size.Byte);
        S7Driver.ReinterpretRawValue(tag, addr, (byte)42).ShouldBe((byte)42);
    }

    /// <summary>Verifies that UInt16 values are returned as unsigned short.</summary>
    [Fact]
    public void ReinterpretRawValue_UInt16_returns_ushort()
    {
        S7Driver.ReinterpretRawValue(Tag("U16", S7DataType.UInt16), Addr(S7Size.Word), (ushort)1000)
            .ShouldBe((ushort)1000);
    }

    /// <summary>Verifies that unsigned short bits are reinterpreted as signed short.</summary>
    [Fact]
    public void ReinterpretRawValue_Int16_reinterprets_ushort_as_signed()
    {
        // 0xFFFF as ushort == -1 as short (two's complement reinterpret, not Convert).
        var result = S7Driver.ReinterpretRawValue(Tag("I16", S7DataType.Int16), Addr(S7Size.Word), (ushort)0xFFFF);
        result.ShouldBe((short)-1);
    }

    /// <summary>Verifies that UInt32 values are returned as unsigned integer.</summary>
    [Fact]
    public void ReinterpretRawValue_UInt32_returns_uint()
    {
        S7Driver.ReinterpretRawValue(Tag("U32", S7DataType.UInt32), Addr(S7Size.DWord), (uint)70_000u)
            .ShouldBe(70_000u);
    }

    /// <summary>Verifies that unsigned integer bits are reinterpreted as signed integer.</summary>
    [Fact]
    public void ReinterpretRawValue_Int32_reinterprets_uint_as_signed()
    {
        // 0xFFFFFFFF as uint == -1 as int.
        var result = S7Driver.ReinterpretRawValue(Tag("I32", S7DataType.Int32), Addr(S7Size.DWord), 0xFFFF_FFFFu);
        result.ShouldBe(-1);
    }

    /// <summary>Verifies that unsigned integer bits are converted to IEEE 754 float.</summary>
    [Fact]
    public void ReinterpretRawValue_Float32_converts_uint_bits_to_float()
    {
        float expected = 3.14f;
        uint bits = BitConverter.SingleToUInt32Bits(expected);
        var result = S7Driver.ReinterpretRawValue(Tag("F32", S7DataType.Float32), Addr(S7Size.DWord), bits);
        ((float)result).ShouldBe(expected, tolerance: 1e-6f);
    }

    // ── ReinterpretRawValue — unsupported types throw NotSupportedException ───────────────

    /// <summary>Verifies that unsupported data types throw NotSupportedException.</summary>
    /// <param name="dt">The unsupported S7 data type.</param>
    [Theory]
    [InlineData(S7DataType.Int64)]
    [InlineData(S7DataType.UInt64)]
    [InlineData(S7DataType.Float64)]
    [InlineData(S7DataType.String)]
    [InlineData(S7DataType.DateTime)]
    public void ReinterpretRawValue_unsupported_type_throws_NotSupportedException(S7DataType dt)
    {
        Should.Throw<NotSupportedException>(() =>
            S7Driver.ReinterpretRawValue(Tag("x", dt), Addr(S7Size.DWord), (uint)0));
    }

    // ── BoxValueForWrite — implemented types ─────────────────────────────────────────────

    /// <summary>Verifies that values are converted to bool for write.</summary>
    [Fact]
    public void BoxValueForWrite_Bool_converts_value_to_bool()
    {
        S7Driver.BoxValueForWrite(S7DataType.Bool, true).ShouldBe(true);
        S7Driver.BoxValueForWrite(S7DataType.Bool, false).ShouldBe(false);
        S7Driver.BoxValueForWrite(S7DataType.Bool, 1).ShouldBe(true);
    }

    /// <summary>Verifies that values are converted to byte for write.</summary>
    [Fact]
    public void BoxValueForWrite_Byte_converts_to_byte()
    {
        S7Driver.BoxValueForWrite(S7DataType.Byte, (byte)200).ShouldBe((byte)200);
    }

    /// <summary>Verifies that values are converted to unsigned short for write.</summary>
    [Fact]
    public void BoxValueForWrite_UInt16_converts_to_ushort()
    {
        S7Driver.BoxValueForWrite(S7DataType.UInt16, (ushort)1234).ShouldBe((ushort)1234);
    }

    /// <summary>Verifies that signed short is reinterpreted as unsigned short using two's complement.</summary>
    [Fact]
    public void BoxValueForWrite_Int16_reinterprets_as_ushort_two_complement()
    {
        // -1 as short → 0xFFFF as ushort; S7.Net writes the ushort to the wire.
        S7Driver.BoxValueForWrite(S7DataType.Int16, (short)-1).ShouldBe((ushort)0xFFFF);
    }

    /// <summary>Verifies that values are converted to unsigned integer for write.</summary>
    [Fact]
    public void BoxValueForWrite_UInt32_converts_to_uint()
    {
        S7Driver.BoxValueForWrite(S7DataType.UInt32, 70_000u).ShouldBe(70_000u);
    }

    /// <summary>Verifies that signed integer is reinterpreted as unsigned integer using two's complement.</summary>
    [Fact]
    public void BoxValueForWrite_Int32_reinterprets_as_uint_two_complement()
    {
        // -1 as int → 0xFFFFFFFF as uint.
        S7Driver.BoxValueForWrite(S7DataType.Int32, -1).ShouldBe(0xFFFF_FFFFu);
    }

    /// <summary>Verifies that float is encoded as IEEE 754 unsigned integer for write.</summary>
    [Fact]
    public void BoxValueForWrite_Float32_encodes_as_ieee754_uint()
    {
        float f = 3.14f;
        var expected = BitConverter.SingleToUInt32Bits(f);
        S7Driver.BoxValueForWrite(S7DataType.Float32, f).ShouldBe(expected);
    }

    // ── BoxValueForWrite — unsupported types throw NotSupportedException ─────────────────

    /// <summary>Verifies that unsupported data types throw NotSupportedException on write.</summary>
    /// <param name="dt">The unsupported S7 data type.</param>
    [Theory]
    [InlineData(S7DataType.Int64)]
    [InlineData(S7DataType.UInt64)]
    [InlineData(S7DataType.Float64)]
    [InlineData(S7DataType.String)]
    [InlineData(S7DataType.DateTime)]
    public void BoxValueForWrite_unsupported_type_throws_NotSupportedException(S7DataType dt)
    {
        Should.Throw<NotSupportedException>(() =>
            S7Driver.BoxValueForWrite(dt, 0));
    }

    // ── BoxValueForWrite — overflow paths ────────────────────────────────────────────────

    /// <summary>Verifies that out-of-range values throw OverflowException for byte write.</summary>
    [Fact]
    public void BoxValueForWrite_Byte_overflows_for_out_of_range_value()
    {
        // Convert.ToByte(256) throws OverflowException.
        Should.Throw<OverflowException>(() => S7Driver.BoxValueForWrite(S7DataType.Byte, 256));
    }

    /// <summary>Verifies that out-of-range values throw OverflowException for unsigned short write.</summary>
    [Fact]
    public void BoxValueForWrite_UInt16_overflows_for_out_of_range_value()
    {
        Should.Throw<OverflowException>(() => S7Driver.BoxValueForWrite(S7DataType.UInt16, 65_536));
    }

    // ── MapDataType (via DiscoverAsync) — Int64/UInt64 now map to their own members ───────

    // MapDataType is private; reach it through DiscoverAsync with the shared
    // RecordingAddressSpaceBuilder (S7TestBuilders.cs) — the same indirection
    // S7DiscoveryAndSubscribeTests uses. T1 split the formerly-lossy Int64/UInt64 → Int32 line
    // so 64-bit tags surface as the matching DriverDataType.

    /// <summary>Verifies that an Int64 tag discovers a node with DriverDataType.Int64 (no longer lossily mapped to Int32).</summary>
    [Fact]
    public async Task DiscoverAsync_Int64_tag_maps_to_DriverDataType_Int64()
    {
        var opts = new S7DriverOptions
        {
            Host = "192.0.2.1",
            Probe = new S7ProbeOptions { Enabled = false },
            RawTags = S7RawTags.Entries(new S7TagDefinition("Counter64", "DB1.DBB0", S7DataType.Int64)),
        };
        // v3: DiscoverAsync projects the table built at Initialize — initialize with a fake PLC first.
        using var drv = new S7Driver(opts, "s7-i64-map", new ConnectingFakeS7PlcFactory());
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var builder = new RecordingAddressSpaceBuilder();
        await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);

        builder.Variables.Single(v => v.Name == "Counter64").Attr.DriverDataType
            .ShouldBe(DriverDataType.Int64, "Int64 now maps to its own DriverDataType member, not lossy Int32");
    }

    /// <summary>Verifies that a UInt64 tag discovers a node with DriverDataType.UInt64.</summary>
    [Fact]
    public async Task DiscoverAsync_UInt64_tag_maps_to_DriverDataType_UInt64()
    {
        var opts = new S7DriverOptions
        {
            Host = "192.0.2.1",
            Probe = new S7ProbeOptions { Enabled = false },
            RawTags = S7RawTags.Entries(new S7TagDefinition("Total64", "DB1.DBB0", S7DataType.UInt64)),
        };
        // v3: DiscoverAsync projects the table built at Initialize — initialize with a fake PLC first.
        using var drv = new S7Driver(opts, "s7-u64-map", new ConnectingFakeS7PlcFactory());
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var builder = new RecordingAddressSpaceBuilder();
        await drv.DiscoverAsync(builder, TestContext.Current.CancellationToken);

        builder.Variables.Single(v => v.Name == "Total64").Attr.DriverDataType
            .ShouldBe(DriverDataType.UInt64, "UInt64 now maps to its own DriverDataType member, not lossy Int32");
    }
}
