using System.Buffers.Binary;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;

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

[Trait("Category", "Unit")]
public sealed class ModbusDataTypeTests
{
    /// <summary>
    ///     Register-count lookup is per-tag now (strings need StringLength; Int64/Float64 need 4).
    /// </summary>
    /// <param name="t">The Modbus data type to test.</param>
    /// <param name="expected">The expected register count for the data type.</param>
    [Theory]
    [InlineData(ModbusDataType.BitInRegister, 1)]
    [InlineData(ModbusDataType.Int16, 1)]
    [InlineData(ModbusDataType.UInt16, 1)]
    [InlineData(ModbusDataType.Int32, 2)]
    [InlineData(ModbusDataType.UInt32, 2)]
    [InlineData(ModbusDataType.Float32, 2)]
    [InlineData(ModbusDataType.Int64, 4)]
    [InlineData(ModbusDataType.UInt64, 4)]
    [InlineData(ModbusDataType.Float64, 4)]
    public void RegisterCount_returns_correct_register_count_per_type(ModbusDataType t, int expected)
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, t);
        ModbusDriver.RegisterCount(tag).ShouldBe((ushort)expected);
    }

    /// <summary>
    /// Verifies that string register count calculation rounds up character count to register pairs.
    /// </summary>
    /// <param name="chars">The number of characters in the string.</param>
    /// <param name="expectedRegs">The expected number of registers required.</param>
    [Theory]
    [InlineData(0, 1)]   // 0 chars → still 1 byte / 1 register (pathological but well-defined: length 0 is 0 bytes)
    [InlineData(1, 1)]
    [InlineData(2, 1)]
    [InlineData(3, 2)]
    [InlineData(10, 5)]
    public void RegisterCount_for_String_rounds_up_to_register_pair(ushort chars, int expectedRegs)
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String, StringLength: chars);
        // 0-char is encoded as 0 regs; the test case expects 1 for lengths 1-2, 2 for 3-4, etc.
        if (chars == 0) ModbusDriver.RegisterCount(tag).ShouldBe((ushort)0);
        else ModbusDriver.RegisterCount(tag).ShouldBe((ushort)expectedRegs);
    }

    // --- Int32 / UInt32 / Float32 with byte-order variants ---

    /// <summary>
    /// Verifies that Int32 with big-endian byte order decodes ABCD register layout correctly.
    /// </summary>
    [Fact]
    public void Int32_BigEndian_decodes_ABCD_layout()
    {
        // Value 0x12345678 → bytes [0x12, 0x34, 0x56, 0x78] as PLC wrote them.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int32,
            ByteOrder: ModbusByteOrder.BigEndian);
        var bytes = new byte[] { 0x12, 0x34, 0x56, 0x78 };
        ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(0x12345678);
    }

    /// <summary>
    /// Verifies that Int32 with word-swap byte order decodes CDAB register layout correctly.
    /// </summary>
    [Fact]
    public void Int32_WordSwap_decodes_CDAB_layout()
    {
        // Siemens/AB PLC stored 0x12345678 as register[0] = 0x5678, register[1] = 0x1234.
        // Wire bytes are [0x56, 0x78, 0x12, 0x34]; with ByteOrder=WordSwap we get 0x12345678 back.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int32,
            ByteOrder: ModbusByteOrder.WordSwap);
        var bytes = new byte[] { 0x56, 0x78, 0x12, 0x34 };
        ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(0x12345678);
    }

    /// <summary>
    /// Verifies that Float32 with word-swap byte order encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void Float32_WordSwap_encode_decode_roundtrips()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Float32,
            ByteOrder: ModbusByteOrder.WordSwap);
        var wire = ModbusDriver.EncodeRegister(25.5f, tag);
        wire.Length.ShouldBe(4);
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe(25.5f);
    }

    // --- Int64 / UInt64 / Float64 ---

    /// <summary>
    /// Verifies that Int64 with big-endian byte order encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void Int64_BigEndian_roundtrips()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Int64);
        var wire = ModbusDriver.EncodeRegister(0x0123456789ABCDEFL, tag);
        wire.Length.ShouldBe(8);
        BinaryPrimitives.ReadInt64BigEndian(wire).ShouldBe(0x0123456789ABCDEFL);
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe(0x0123456789ABCDEFL);
    }

    /// <summary>
    /// Verifies that UInt64 with word-swap byte order reverses four words correctly.
    /// </summary>
    [Fact]
    public void UInt64_WordSwap_reverses_four_words()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.UInt64,
            ByteOrder: ModbusByteOrder.WordSwap);
        var value = 0xAABBCCDDEEFF0011UL;

        var wireBE = new byte[8];
        BinaryPrimitives.WriteUInt64BigEndian(wireBE, value);

        // Word-swap layout: [word3, word2, word1, word0] where each word keeps its bytes big-endian.
        var wireWS = new byte[] { wireBE[6], wireBE[7], wireBE[4], wireBE[5], wireBE[2], wireBE[3], wireBE[0], wireBE[1] };
        ModbusDriver.DecodeRegister(wireWS, tag).ShouldBe(value);

        var roundtrip = ModbusDriver.EncodeRegister(value, tag);
        roundtrip.ShouldBe(wireWS);
    }

    /// <summary>
    /// Verifies that Float64 with word-swap byte order encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void Float64_roundtrips_under_word_swap()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Float64,
            ByteOrder: ModbusByteOrder.WordSwap);
        var wire = ModbusDriver.EncodeRegister(3.14159265358979d, tag);
        wire.Length.ShouldBe(8);
        ((double)ModbusDriver.DecodeRegister(wire, tag)!).ShouldBe(3.14159265358979d, tolerance: 1e-12);
    }

    // --- BitInRegister ---

    /// <summary>
    /// Verifies that BitInRegister correctly extracts a bit at the specified index.
    /// </summary>
    /// <param name="raw">The raw register value containing the bit.</param>
    /// <param name="bitIndex">The index of the bit to extract.</param>
    /// <param name="expected">The expected bit value.</param>
    [Theory]
    [InlineData(0b0000_0000_0000_0001, 0, true)]
    [InlineData(0b0000_0000_0000_0001, 1, false)]
    [InlineData(0b1000_0000_0000_0000, 15, true)]
    [InlineData(0b0100_0000_0100_0000, 6, true)]
    [InlineData(0b0100_0000_0100_0000, 14, true)]
    [InlineData(0b0100_0000_0100_0000, 7, false)]
    public void BitInRegister_extracts_bit_at_index(ushort raw, byte bitIndex, bool expected)
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.BitInRegister,
            BitIndex: bitIndex);
        var bytes = new byte[] { (byte)(raw >> 8), (byte)(raw & 0xFF) };
        ModbusDriver.DecodeRegister(bytes, tag).ShouldBe(expected);
    }

    /// <summary>
    /// Verifies that BitInRegister correctly rejects direct EncodeRegister calls.
    /// </summary>
    [Fact]
    public void BitInRegister_EncodeRegister_still_rejects_direct_calls()
    {
        // BitInRegister writes now go through WriteBitInRegisterAsync's RMW path (task #181).
        // EncodeRegister should never be reached for this type — if it is, throwing keeps an
        // unintended caller loud rather than silently clobbering the register.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.BitInRegister,
            BitIndex: 5);
        Should.Throw<InvalidOperationException>(() => ModbusDriver.EncodeRegister(true, tag))
            .Message.ShouldContain("WriteBitInRegisterAsync");
    }

    // --- String ---

    /// <summary>
    /// Verifies that String decodes ASCII characters packed two per register.
    /// </summary>
    [Fact]
    public void String_decodes_ASCII_packed_two_chars_per_register()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 6);
        // "HELLO!" = 0x48 0x45 0x4C 0x4C 0x4F 0x21 across 3 registers.
        var bytes = "HELLO!"u8.ToArray();
        ModbusDriver.DecodeRegister(bytes, tag).ShouldBe("HELLO!");
    }

    /// <summary>
    /// Verifies that String decode truncates at the first null terminator.
    /// </summary>
    [Fact]
    public void String_decode_truncates_at_first_nul()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 10);
        var bytes = new byte[] { 0x48, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
        ModbusDriver.DecodeRegister(bytes, tag).ShouldBe("Hi");
    }

    /// <summary>
    /// Verifies that String encode pads remaining bytes with null terminators.
    /// </summary>
    [Fact]
    public void String_encode_nul_pads_remaining_bytes()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 8);
        var wire = ModbusDriver.EncodeRegister("Hi", tag);
        wire.Length.ShouldBe(8);
        wire[0].ShouldBe((byte)'H');
        wire[1].ShouldBe((byte)'i');
        for (var i = 2; i < 8; i++) wire[i].ShouldBe((byte)0);
    }

    // --- DL205 low-byte-first strings (AutomationDirect DirectLOGIC quirk) ---

    /// <summary>
    /// Verifies that String with low-byte-first byte order decodes DL205-packed strings correctly.
    /// </summary>
    [Fact]
    public void String_LowByteFirst_decodes_DL205_packed_Hello()
    {
        // HR[1040] = 0x6548 (wire BE bytes [0x65, 0x48]) decodes first char from low byte = 'H',
        // second from high byte = 'e'. HR[1041] = 0x6C6C → 'l','l'. HR[1042] = 0x006F → 'o', nul.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 5, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
        var wire = new byte[] { 0x65, 0x48, 0x6C, 0x6C, 0x00, 0x6F };
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hello");
    }

    /// <summary>
    /// Verifies that String with low-byte-first byte order truncates at the first null terminator.
    /// </summary>
    [Fact]
    public void String_LowByteFirst_decode_truncates_at_first_nul()
    {
        // Low-byte-first with only 2 real chars in register 0 (lo='H', hi='i') and the rest nul.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 6, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
        var wire = new byte[] { 0x69, 0x48, 0x00, 0x00, 0x00, 0x00 };
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hi");
    }

    /// <summary>
    /// Verifies that String with low-byte-first byte order encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void String_LowByteFirst_encode_round_trips_with_decode()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 5, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
        var wire = ModbusDriver.EncodeRegister("Hello", tag);
        // Expect exactly the DL205-documented byte sequence.
        wire.ShouldBe(new byte[] { 0x65, 0x48, 0x6C, 0x6C, 0x00, 0x6F });
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe("Hello");
    }

    /// <summary>
    /// Verifies that String with high-byte-first and low-byte-first produce different results for the same wire data.
    /// </summary>
    [Fact]
    public void String_HighByteFirst_and_LowByteFirst_differ_on_same_wire()
    {
        // Same wire buffer, different byte order → first char switches 'H' vs 'e'.
        var wire = new byte[] { 0x48, 0x65 };
        var hi = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 2, StringByteOrder: ModbusStringByteOrder.HighByteFirst);
        var lo = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.String,
            StringLength: 2, StringByteOrder: ModbusStringByteOrder.LowByteFirst);
        ModbusDriver.DecodeRegister(wire, hi).ShouldBe("He");
        ModbusDriver.DecodeRegister(wire, lo).ShouldBe("eH");
    }

    // --- MapDataType node DataType advertisement ---

    /// <summary>
    /// Verifies that 64-bit Modbus types surface the correct DriverDataType (not the stale Int32 fallback).
    /// </summary>
    [Theory]
    [InlineData(ModbusDataType.Int64, DriverDataType.Int64)]
    [InlineData(ModbusDataType.UInt64, DriverDataType.UInt64)]
    public void MapDataType_64bit_surfaces_correct_DriverDataType(ModbusDataType wire, DriverDataType expected)
        => ModbusDriver.MapDataType(wire).ShouldBe(expected);

    // --- BCD (binary-coded decimal, DL205/DL260 default numeric encoding) ---

    /// <summary>
    /// Verifies that 16-bit BCD decoding produces the expected decimal value.
    /// </summary>
    /// <param name="raw">The raw 16-bit BCD value.</param>
    /// <param name="expected">The expected decoded decimal value.</param>
    [Theory]
    [InlineData(0x0000u, 0u)]
    [InlineData(0x0001u, 1u)]
    [InlineData(0x0009u, 9u)]
    [InlineData(0x0010u, 10u)]
    [InlineData(0x1234u, 1234u)]
    [InlineData(0x9999u, 9999u)]
    public void DecodeBcd_16_bit_decodes_expected_decimal(uint raw, uint expected)
        => ModbusDriver.DecodeBcd(raw, nibbles: 4).ShouldBe(expected);

    /// <summary>
    /// Verifies that BCD decoding rejects nibble values above nine.
    /// </summary>
    [Fact]
    public void DecodeBcd_rejects_nibbles_above_nine()
    {
        Should.Throw<InvalidDataException>(() => ModbusDriver.DecodeBcd(0x00A5u, nibbles: 4))
            .Message.ShouldContain("Non-BCD nibble");
    }

    /// <summary>
    /// Verifies that 16-bit BCD encoding produces the expected nibble pattern.
    /// </summary>
    /// <param name="value">The decimal value to encode.</param>
    /// <param name="expected">The expected 16-bit BCD encoding.</param>
    [Theory]
    [InlineData(0u, 0x0000u)]
    [InlineData(5u, 0x0005u)]
    [InlineData(42u, 0x0042u)]
    [InlineData(1234u, 0x1234u)]
    [InlineData(9999u, 0x9999u)]
    public void EncodeBcd_16_bit_encodes_expected_nibbles(uint value, uint expected)
        => ModbusDriver.EncodeBcd(value, nibbles: 4).ShouldBe(expected);

    /// <summary>
    /// Verifies that Bcd16 decodes DL205 register value 0x1234 as decimal 1234.
    /// </summary>
    [Fact]
    public void Bcd16_decodes_DL205_register_1234_as_decimal_1234()
    {
        // HR[1072] = 0x1234 on the DL205 profile represents decimal 1234. A plain Int16 decode
        // would return 0x04D2 = 4660 — proof the BCD path is different.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd16);
        ModbusDriver.DecodeRegister(new byte[] { 0x12, 0x34 }, tag).ShouldBe(1234);

        var int16Tag = tag with { DataType = ModbusDataType.Int16 };
        ModbusDriver.DecodeRegister(new byte[] { 0x12, 0x34 }, int16Tag).ShouldBe((short)0x1234);
    }

    /// <summary>
    /// Verifies that Bcd16 encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void Bcd16_encode_round_trips_with_decode()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd16);
        var wire = ModbusDriver.EncodeRegister(4321, tag);
        wire.ShouldBe(new byte[] { 0x43, 0x21 });
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe(4321);
    }

    /// <summary>
    /// Verifies that Bcd16 encode rejects values that exceed four decimal digits.
    /// </summary>
    [Fact]
    public void Bcd16_encode_rejects_out_of_range_values()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd16);
        Should.Throw<OverflowException>(() => ModbusDriver.EncodeRegister(10000, tag))
            .Message.ShouldContain("4 decimal digits");
    }

    /// <summary>
    /// Verifies that Bcd32 decodes eight decimal digits in big-endian byte order.
    /// </summary>
    [Fact]
    public void Bcd32_decodes_8_digits_big_endian()
    {
        // 0x12345678 as BCD = decimal 12_345_678.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd32);
        ModbusDriver.DecodeRegister(new byte[] { 0x12, 0x34, 0x56, 0x78 }, tag).ShouldBe(12_345_678);
    }

    /// <summary>
    /// Verifies that Bcd32 with word-swap byte order handles CDAB register layout correctly.
    /// </summary>
    [Fact]
    public void Bcd32_word_swap_handles_CDAB_layout()
    {
        // PLC stored 12_345_678 with word swap: low-word 0x5678 first, high-word 0x1234 second.
        // Wire bytes [0x56, 0x78, 0x12, 0x34] + WordSwap → decode to decimal 12_345_678.
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd32,
            ByteOrder: ModbusByteOrder.WordSwap);
        ModbusDriver.DecodeRegister(new byte[] { 0x56, 0x78, 0x12, 0x34 }, tag).ShouldBe(12_345_678);
    }

    /// <summary>
    /// Verifies that Bcd32 encodes and decodes consistently.
    /// </summary>
    [Fact]
    public void Bcd32_encode_round_trips_with_decode()
    {
        var tag = new ModbusTagDefinition("T", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd32);
        var wire = ModbusDriver.EncodeRegister(87_654_321u, tag);
        wire.ShouldBe(new byte[] { 0x87, 0x65, 0x43, 0x21 });
        ModbusDriver.DecodeRegister(wire, tag).ShouldBe(87_654_321);
    }

    /// <summary>
    /// Verifies that BCD register count matches the underlying data width.
    /// </summary>
    [Fact]
    public void Bcd_RegisterCount_matches_underlying_width()
    {
        var b16 = new ModbusTagDefinition("A", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd16);
        var b32 = new ModbusTagDefinition("B", ModbusRegion.HoldingRegisters, 0, ModbusDataType.Bcd32);
        ModbusDriver.RegisterCount(b16).ShouldBe((ushort)1);
        ModbusDriver.RegisterCount(b32).ShouldBe((ushort)2);
    }
}
