using Shouldly;
using Xunit;

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

[Trait("Category", "Unit")]
public sealed class S7AddressParserTests
{
    // --- Data blocks ---

    /// <summary>Verifies that data block addresses are parsed correctly.</summary>
    /// <param name="input">The address string to parse.</param>
    /// <param name="db">The expected data block number.</param>
    /// <param name="size">The expected data size.</param>
    /// <param name="byteOff">The expected byte offset.</param>
    /// <param name="bitOff">The expected bit offset.</param>
    [Theory]
    [InlineData("DB1.DBX0.0", 1, S7Size.Bit, 0, 0)]
    [InlineData("DB1.DBX0.7", 1, S7Size.Bit, 0, 7)]
    [InlineData("DB1.DBB0", 1, S7Size.Byte, 0, 0)]
    [InlineData("DB1.DBW0", 1, S7Size.Word, 0, 0)]
    [InlineData("DB1.DBD4", 1, S7Size.DWord, 4, 0)]
    [InlineData("DB10.DBW100", 10, S7Size.Word, 100, 0)]
    [InlineData("DB1.DBX15.3", 1, S7Size.Bit, 15, 3)]
    public void Parse_data_block_addresses(string input, int db, S7Size size, int byteOff, int bitOff)
    {
        var r = S7AddressParser.Parse(input);
        r.Area.ShouldBe(S7Area.DataBlock);
        r.DbNumber.ShouldBe(db);
        r.Size.ShouldBe(size);
        r.ByteOffset.ShouldBe(byteOff);
        r.BitOffset.ShouldBe(bitOff);
    }

    /// <summary>Verifies that parsing is case-insensitive and trims whitespace.</summary>
    /// <param name="input">The address string to parse.</param>
    /// <param name="db">The expected data block number.</param>
    /// <param name="size">The expected data size.</param>
    /// <param name="off">The expected byte offset.</param>
    [Theory]
    [InlineData("db1.dbw0", 1, S7Size.Word, 0)]
    [InlineData(" DB1.DBW0 ", 1, S7Size.Word, 0)]       // trim whitespace
    public void Parse_is_case_insensitive_and_trims(string input, int db, S7Size size, int off)
    {
        var r = S7AddressParser.Parse(input);
        r.Area.ShouldBe(S7Area.DataBlock);
        r.DbNumber.ShouldBe(db);
        r.Size.ShouldBe(size);
        r.ByteOffset.ShouldBe(off);
    }

    // --- M / I / Q ---

    /// <summary>Verifies that Memory (M), Input (I), and Output (Q) addresses are parsed correctly.</summary>
    /// <param name="input">The address string to parse.</param>
    /// <param name="area">The expected address area.</param>
    /// <param name="size">The expected data size.</param>
    /// <param name="byteOff">The expected byte offset.</param>
    /// <param name="bitOff">The expected bit offset.</param>
    [Theory]
    [InlineData("MB0", S7Area.Memory, S7Size.Byte, 0, 0)]
    [InlineData("MW10", S7Area.Memory, S7Size.Word, 10, 0)]
    [InlineData("MD4", S7Area.Memory, S7Size.DWord, 4, 0)]
    [InlineData("M0.0", S7Area.Memory, S7Size.Bit, 0, 0)]
    [InlineData("M255.7", S7Area.Memory, S7Size.Bit, 255, 7)]
    [InlineData("IB0", S7Area.Input, S7Size.Byte, 0, 0)]
    [InlineData("IW0", S7Area.Input, S7Size.Word, 0, 0)]
    [InlineData("I0.0", S7Area.Input, S7Size.Bit, 0, 0)]
    [InlineData("QB0", S7Area.Output, S7Size.Byte, 0, 0)]
    [InlineData("QW0", S7Area.Output, S7Size.Word, 0, 0)]
    [InlineData("Q0.0", S7Area.Output, S7Size.Bit, 0, 0)]
    [InlineData("QD4", S7Area.Output, S7Size.DWord, 4, 0)]
    public void Parse_MIQ_addresses(string input, S7Area area, S7Size size, int byteOff, int bitOff)
    {
        var r = S7AddressParser.Parse(input);
        r.Area.ShouldBe(area);
        r.DbNumber.ShouldBe(0);
        r.Size.ShouldBe(size);
        r.ByteOffset.ShouldBe(byteOff);
        r.BitOffset.ShouldBe(bitOff);
    }

    // --- Timers / counters ---

    /// <summary>Verifies that timer and counter addresses are parsed correctly.</summary>
    /// <param name="input">The address string to parse.</param>
    /// <param name="area">The expected address area.</param>
    /// <param name="number">The expected timer or counter number.</param>
    [Theory]
    [InlineData("T0", S7Area.Timer, 0)]
    [InlineData("T15", S7Area.Timer, 15)]
    [InlineData("C0", S7Area.Counter, 0)]
    [InlineData("C10", S7Area.Counter, 10)]
    public void Parse_timer_and_counter(string input, S7Area area, int number)
    {
        var r = S7AddressParser.Parse(input);
        r.Area.ShouldBe(area);
        r.ByteOffset.ShouldBe(number);
        r.Size.ShouldBe(S7Size.Word, "timers + counters are 16-bit opaque");
    }

    // --- Reject garbage ---

    /// <summary>Verifies that invalid addresses are rejected with FormatException.</summary>
    /// <param name="bad">The invalid address string to test.</param>
    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("Z0")]                 // unknown area
    [InlineData("DB")]                 // no number or tail
    [InlineData("DB1")]                // no tail
    [InlineData("DB1.")]               // empty tail
    [InlineData("DB1.DBX0")]           // bit size without .bit
    [InlineData("DB1.DBX0.8")]         // bit 8 out of range
    [InlineData("DB1.DBW0.0")]         // word with bit suffix
    [InlineData("DB0.DBW0")]           // db 0 invalid
    [InlineData("DBA.DBW0")]           // non-numeric db
    [InlineData("DB1.DBQ0")]           // invalid size letter
    [InlineData("M")]                  // no offset
    [InlineData("M0")]                 // bit access needs .bit
    [InlineData("M0.8")]               // bit 8
    [InlineData("MB-1")]               // negative offset
    [InlineData("MW")]                 // no offset digits
    [InlineData("TA")]                 // non-numeric timer
    public void Parse_rejects_invalid(string bad)
        => Should.Throw<FormatException>(() => S7AddressParser.Parse(bad));

    /// <summary>Verifies that TryParse returns false for invalid input without throwing.</summary>
    [Fact]
    public void TryParse_returns_false_for_garbage_without_throwing()
    {
        S7AddressParser.TryParse("not-an-address", out var r).ShouldBeFalse();
        r.ShouldBe(default);
    }

    /// <summary>Verifies that TryParse returns true for a valid address.</summary>
    [Fact]
    public void TryParse_returns_true_for_valid_address()
    {
        S7AddressParser.TryParse("DB1.DBW0", out var r).ShouldBeTrue();
        r.DbNumber.ShouldBe(1);
        r.Size.ShouldBe(S7Size.Word);
    }
}
