using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;

namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Tests;

/// <summary>
///     Covers <see cref="WriteCommand.ParseValue"/> for the IEC 61131-3 atomic types
///     TwinCAT exposes. Wider matrix than AB CIP because IEC adds WSTRING + the four
///     TIME/DATE variants that all marshal as UDINT on the wire.
/// </summary>
[Trait("Category", "Unit")]
public sealed class WriteCommandParseValueTests
{
    /// <summary>Verifies that ParseValue Bool accepts common aliases.</summary>
    /// <param name="raw">The raw input string to parse.</param>
    /// <param name="expected">The expected boolean result.</param>
    [Theory]
    [InlineData("true", true)]
    [InlineData("0", false)]
    [InlineData("on", true)]
    [InlineData("NO", false)]
    public void ParseValue_Bool_accepts_common_aliases(string raw, bool expected)
    {
        WriteCommand.ParseValue(raw, TwinCATDataType.Bool).ShouldBe(expected);
    }

    /// <summary>Verifies that ParseValue Bool rejects garbage.</summary>
    [Fact]
    public void ParseValue_Bool_rejects_garbage()
    {
        Should.Throw<CliFx.Exceptions.CommandException>(
            () => WriteCommand.ParseValue("maybe", TwinCATDataType.Bool));
    }

    /// <summary>Verifies that ParseValue SInt parses signed byte.</summary>
    [Fact]
    public void ParseValue_SInt_signed_byte()
    {
        WriteCommand.ParseValue("-128", TwinCATDataType.SInt).ShouldBe((sbyte)-128);
    }

    /// <summary>Verifies that ParseValue USInt parses unsigned byte.</summary>
    [Fact]
    public void ParseValue_USInt_unsigned_byte()
    {
        WriteCommand.ParseValue("255", TwinCATDataType.USInt).ShouldBe((byte)255);
    }

    /// <summary>Verifies that ParseValue Int parses signed 16-bit value.</summary>
    [Fact]
    public void ParseValue_Int_signed_16bit()
    {
        WriteCommand.ParseValue("-32768", TwinCATDataType.Int).ShouldBe((short)-32768);
    }

    /// <summary>Verifies that ParseValue UInt parses unsigned 16-bit value.</summary>
    [Fact]
    public void ParseValue_UInt_unsigned_16bit()
    {
        WriteCommand.ParseValue("65535", TwinCATDataType.UInt).ShouldBe((ushort)65535);
    }

    /// <summary>Verifies that ParseValue DInt parses int32 bounds.</summary>
    [Fact]
    public void ParseValue_DInt_int32_bounds()
    {
        WriteCommand.ParseValue("-2147483648", TwinCATDataType.DInt).ShouldBe(int.MinValue);
    }

    /// <summary>Verifies that ParseValue UDInt parses uint32 maximum.</summary>
    [Fact]
    public void ParseValue_UDInt_uint32_max()
    {
        WriteCommand.ParseValue("4294967295", TwinCATDataType.UDInt).ShouldBe(uint.MaxValue);
    }

    /// <summary>Verifies that ParseValue LInt parses int64 minimum.</summary>
    [Fact]
    public void ParseValue_LInt_int64_min()
    {
        WriteCommand.ParseValue("-9223372036854775808", TwinCATDataType.LInt).ShouldBe(long.MinValue);
    }

    /// <summary>Verifies that ParseValue ULInt parses uint64 maximum.</summary>
    [Fact]
    public void ParseValue_ULInt_uint64_max()
    {
        WriteCommand.ParseValue("18446744073709551615", TwinCATDataType.ULInt).ShouldBe(ulong.MaxValue);
    }

    /// <summary>Verifies that ParseValue Real uses invariant culture.</summary>
    [Fact]
    public void ParseValue_Real_invariant_culture()
    {
        WriteCommand.ParseValue("3.14", TwinCATDataType.Real).ShouldBe(3.14f);
    }

    /// <summary>Verifies that ParseValue LReal has higher precision.</summary>
    [Fact]
    public void ParseValue_LReal_higher_precision()
    {
        WriteCommand.ParseValue("2.718281828", TwinCATDataType.LReal).ShouldBeOfType<double>();
    }

    /// <summary>Verifies that ParseValue String passes through input unchanged.</summary>
    [Fact]
    public void ParseValue_String_passthrough()
    {
        WriteCommand.ParseValue("hallo beckhoff", TwinCATDataType.String).ShouldBe("hallo beckhoff");
    }

    /// <summary>Verifies that ParseValue WString passes through input unchanged.</summary>
    [Fact]
    public void ParseValue_WString_passthrough()
    {
        // CLI layer doesn't distinguish UTF-8 input; the driver handles the WSTRING
        // encoding on the wire.
        WriteCommand.ParseValue("überstall", TwinCATDataType.WString).ShouldBe("überstall");
    }

    /// <summary>Verifies that ParseValue IEC date/time variants land on uint32.</summary>
    /// <param name="type">The IEC 61131-3 data type to parse.</param>
    [Theory]
    [InlineData(TwinCATDataType.Time)]
    [InlineData(TwinCATDataType.Date)]
    [InlineData(TwinCATDataType.DateTime)]
    [InlineData(TwinCATDataType.TimeOfDay)]
    public void ParseValue_IEC_date_time_variants_land_on_uint32(TwinCATDataType type)
    {
        // IEC 61131-3 TIME / DATE / DT / TOD all marshal as UDINT on the wire; the CLI
        // accepts a numeric raw value and lets the caller handle the encoding.
        WriteCommand.ParseValue("1234567", type).ShouldBeOfType<uint>();
    }

    /// <summary>Verifies that ParseValue Structure is refused.</summary>
    [Fact]
    public void ParseValue_Structure_refused()
    {
        Should.Throw<CliFx.Exceptions.CommandException>(
            () => WriteCommand.ParseValue("42", TwinCATDataType.Structure));
    }

    /// <summary>
    ///     Verifies that ParseValue wraps raw parse errors in a CommandException so the operator
    ///     sees a clean one-line message (Driver.TwinCAT.Cli-008).
    /// </summary>
    [Fact]
    public void ParseValue_non_numeric_for_numeric_types_throws_CommandException()
    {
        // Previously threw raw FormatException. After the fix, FormatException + OverflowException
        // are caught and re-thrown as CommandException so CliFx prints a single error line.
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(
            () => WriteCommand.ParseValue("xyz", TwinCATDataType.DInt));
        ex.Message.ShouldContain("xyz");
        ex.Message.ShouldContain("DInt");
    }

    /// <summary>
    ///     Verifies that ParseValue wraps OverflowException (value out of type range) in a
    ///     CommandException (Driver.TwinCAT.Cli-008).
    /// </summary>
    [Fact]
    public void ParseValue_overflow_for_numeric_types_throws_CommandException()
    {
        // 300 is out of range for SInt (sbyte: -128..127).
        var ex = Should.Throw<CliFx.Exceptions.CommandException>(
            () => WriteCommand.ParseValue("300", TwinCATDataType.SInt));
        ex.Message.ShouldContain("300");
        ex.Message.ShouldContain("SInt");
    }

    /// <summary>Verifies that SynthesiseTagName preserves symbolic path verbatim.</summary>
    /// <param name="symbol">The symbolic path to synthesise.</param>
    /// <param name="type">The TwinCAT data type.</param>
    /// <param name="expected">The expected synthesised tag name.</param>
    [Theory]
    [InlineData("MAIN.bStart", TwinCATDataType.Bool, "MAIN.bStart:Bool")]
    [InlineData("GVL.Counter", TwinCATDataType.DInt, "GVL.Counter:DInt")]
    [InlineData("Motor1.Status.Running", TwinCATDataType.Bool, "Motor1.Status.Running:Bool")]
    [InlineData("Recipe[3]", TwinCATDataType.Real, "Recipe[3]:Real")]
    public void SynthesiseTagName_preserves_symbolic_path_verbatim(
        string symbol, TwinCATDataType type, string expected)
    {
        ReadCommand.SynthesiseTagName(symbol, type).ShouldBe(expected);
    }
}
