using System.Globalization;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Types;

/// <summary>
/// Tests for <see cref="AttributeValueCodec"/> — the canonical, round-trippable
/// codec used wherever an attribute value is stored or transmitted. Scalars must
/// encode to their historical invariant-culture string; List attributes encode to
/// a JSON array and decode back to a typed <c>List&lt;T&gt;</c>.
/// </summary>
public class AttributeValueCodecTests
{
    [Fact]
    public void Encode_StringList_ProducesJsonArray() =>
        Assert.Equal("[\"WO-1\",\"WO-2\"]",
            AttributeValueCodec.Encode(new List<string> { "WO-1", "WO-2" }));

    [Fact]
    public void Encode_Scalar_String_ReturnedAsIs() =>
        Assert.Equal("hello", AttributeValueCodec.Encode("hello"));

    [Fact]
    public void Encode_Scalar_Double_IsInvariant()
    {
        var original = CultureInfo.CurrentCulture;
        try
        {
            CultureInfo.CurrentCulture = new CultureInfo("de-DE");
            Assert.Equal("1.5", AttributeValueCodec.Encode(1.5));
        }
        finally
        {
            CultureInfo.CurrentCulture = original;
        }
    }

    [Fact]
    public void Encode_Null_ReturnsNull() =>
        Assert.Null(AttributeValueCodec.Encode(null));

    [Fact]
    public void Encode_EmptyList_IsBracketPair() =>
        Assert.Equal("[]", AttributeValueCodec.Encode(new List<string>()));

    [Fact]
    public void Encode_StringWithComma_IsEscaped() =>
        Assert.Equal("[\"ACME, Inc.\"]",
            AttributeValueCodec.Encode(new List<string> { "ACME, Inc." }));

    [Fact]
    public void RoundTrip_FloatList()
    {
        var json = AttributeValueCodec.Encode(new List<float> { 1.5f, 2.25f, -3.75f });
        var back = (IList<float>)AttributeValueCodec.Decode(json, DataType.List, DataType.Float)!;
        Assert.Equal(new[] { 1.5f, 2.25f, -3.75f }, back);
    }

    [Fact]
    public void Encode_DoubleList_IsInvariant()
    {
        var original = CultureInfo.CurrentCulture;
        try
        {
            CultureInfo.CurrentCulture = new CultureInfo("de-DE");
            Assert.Equal("[1.5,2.5]",
                AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 }));
        }
        finally
        {
            CultureInfo.CurrentCulture = original;
        }
    }

    [Fact]
    public void Encode_Int32List_ProducesNativeNumbers() =>
        Assert.Equal("[10,20,30]",
            AttributeValueCodec.Encode(new List<int> { 10, 20, 30 }));

    [Fact]
    public void Encode_BoolList_ProducesNativeBooleans() =>
        Assert.Equal("[true,false]",
            AttributeValueCodec.Encode(new List<bool> { true, false }));

    [Fact]
    public void Encode_StringList_StaysQuoted() =>
        Assert.Equal("[\"a\",\"b\"]",
            AttributeValueCodec.Encode(new List<string> { "a", "b" }));

    [Fact]
    public void Encode_DateTimeList_IsIso8601()
    {
        var json = AttributeValueCodec.Encode(
            new List<DateTime> { new(2026, 6, 16, 0, 0, 0, DateTimeKind.Utc) });
        Assert.StartsWith("[\"", json);   // DateTime list must be an array of quoted strings
        Assert.Contains("2026-06-16T00:00:00", json);
        Assert.DoesNotContain("06/16/2026", json);
    }

    [Fact]
    public void Decode_OldStringFloatForm_BackwardCompatible()
    {
        var back = (IList<float>)AttributeValueCodec.Decode("[\"1.5\",\"2.25\"]", DataType.List, DataType.Float)!;
        Assert.Equal(new[] { 1.5f, 2.25f }, back);
    }

    [Fact]
    public void Decode_OldStringDateTimeForm_BackwardCompatible()
    {
        // The pre-native codec encoded DateTime via IFormattable.ToString(null, InvariantCulture):
        // "06/16/2026 12:30:45" (US-invariant, no 'T'/'Z'). New Decode must still parse it.
        var back = (IList<DateTime>)AttributeValueCodec.Decode(
            "[\"06/16/2026 12:30:45\"]", DataType.List, DataType.DateTime)!;
        Assert.Equal(new DateTime(2026, 6, 16, 12, 30, 45, DateTimeKind.Unspecified), back[0]);
    }

    [Fact]
    public void Decode_NewNativeIntForm_Parses()
    {
        var back = (IList<int>)AttributeValueCodec.Decode("[10,20]", DataType.List, DataType.Int32)!;
        Assert.Equal(new[] { 10, 20 }, back);
    }

    [Fact]
    public void Decode_OldStringIntForm_BackwardCompatible()
    {
        var back = (IList<int>)AttributeValueCodec.Decode("[\"10\",\"20\"]", DataType.List, DataType.Int32)!;
        Assert.Equal(new[] { 10, 20 }, back);
    }

    [Theory]
    [InlineData("[true,false]")]
    [InlineData("[\"True\",\"False\"]")]
    public void Decode_BoolForms_BothParse(string json)
    {
        var back = (IList<bool>)AttributeValueCodec.Decode(json, DataType.List, DataType.Boolean)!;
        Assert.Equal(new[] { true, false }, back);
    }

    [Fact]
    public void RoundTrip_Int32List()
    {
        var json = AttributeValueCodec.Encode(new List<int> { 1, 2, 3 });
        var back = (IList<int>)AttributeValueCodec.Decode(json, DataType.List, DataType.Int32)!;
        Assert.Equal(new[] { 1, 2, 3 }, back);
    }

    [Fact]
    public void RoundTrip_DoubleList_IsCultureInvariant()
    {
        var original = CultureInfo.CurrentCulture;
        try
        {
            CultureInfo.CurrentCulture = new CultureInfo("de-DE");
            var json = AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 });
            var back = (IList<double>)AttributeValueCodec.Decode(json, DataType.List, DataType.Double)!;
            Assert.Equal(new[] { 1.5, 2.5 }, back);
        }
        finally
        {
            CultureInfo.CurrentCulture = original;
        }
    }

    [Fact]
    public void RoundTrip_BoolList()
    {
        var json = AttributeValueCodec.Encode(new List<bool> { true, false, true });
        var back = (IList<bool>)AttributeValueCodec.Decode(json, DataType.List, DataType.Boolean)!;
        Assert.Equal(new[] { true, false, true }, back);
    }

    [Fact]
    public void RoundTrip_DateTimeList_Iso8601()
    {
        var a = new DateTime(2026, 6, 15, 13, 45, 30, DateTimeKind.Utc);
        var b = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var json = AttributeValueCodec.Encode(new List<DateTime> { a, b });
        var back = (IList<DateTime>)AttributeValueCodec.Decode(json, DataType.List, DataType.DateTime)!;
        Assert.Equal(new[] { a, b }, back);
    }

    [Fact]
    public void Decode_StringArray_ProducesListOfString()
    {
        var back = (IList<string>)AttributeValueCodec.Decode("[\"a\",\"b\"]", DataType.List, DataType.String)!;
        Assert.Equal(new[] { "a", "b" }, back);
    }

    [Fact]
    public void Decode_Scalar_ReturnsString() =>
        Assert.Equal("42", AttributeValueCodec.Decode("42", DataType.Int32, null));

    [Fact]
    public void Decode_List_NullValue_ReturnsNull() =>
        Assert.Null(AttributeValueCodec.Decode(null, DataType.List, DataType.String));

    [Fact]
    public void Decode_List_EmptyValue_ReturnsNull() =>
        Assert.Null(AttributeValueCodec.Decode("", DataType.List, DataType.String));

    [Fact]
    public void Decode_MalformedJson_Throws() =>
        Assert.Throws<FormatException>(() =>
            AttributeValueCodec.Decode("not json", DataType.List, DataType.String));

    [Fact]
    public void Decode_UnparseableElement_Throws() =>
        Assert.Throws<FormatException>(() =>
            AttributeValueCodec.Decode("[\"abc\"]", DataType.List, DataType.Int32));

    [Fact]
    public void Decode_NullElement_Throws() =>
        Assert.Throws<FormatException>(() =>
            AttributeValueCodec.Decode("[null]", DataType.List, DataType.String));

    [Fact]
    public void Decode_List_WithoutElementType_Throws() =>
        Assert.Throws<FormatException>(() =>
            AttributeValueCodec.Decode("[\"a\"]", DataType.List, null));

    [Theory]
    [InlineData(DataType.String, true)]
    [InlineData(DataType.Int32, true)]
    [InlineData(DataType.Float, true)]
    [InlineData(DataType.Double, true)]
    [InlineData(DataType.Boolean, true)]
    [InlineData(DataType.DateTime, true)]
    [InlineData(DataType.Binary, false)]
    [InlineData(DataType.List, false)]
    public void IsValidElementType_MatchesScalarSet(DataType t, bool expected) =>
        Assert.Equal(expected, AttributeValueCodec.IsValidElementType(t));

    // ── ElementClrType + CoerceEnumerable: the shared object-input coercion ──
    // entry point reused by InstanceActor (live OPC UA arrays). Distinct from the
    // string-input path in Decode (JSON elements): here elements arrive as boxed
    // CLR values and are converted via Convert.ToXxx (or parsed when string).

    [Theory]
    [InlineData(DataType.String, typeof(string))]
    [InlineData(DataType.Int32, typeof(int))]
    [InlineData(DataType.Float, typeof(float))]
    [InlineData(DataType.Double, typeof(double))]
    [InlineData(DataType.Boolean, typeof(bool))]
    [InlineData(DataType.DateTime, typeof(DateTime))]
    public void ElementClrType_MapsEachScalar(DataType t, Type expected) =>
        Assert.Equal(expected, AttributeValueCodec.ElementClrType(t));

    [Fact]
    public void ElementClrType_RejectsNonElementType() =>
        Assert.Throws<FormatException>(() => AttributeValueCodec.ElementClrType(DataType.List));

    [Fact]
    public void CoerceEnumerable_BuildsTypedList()
    {
        var list = AttributeValueCodec.CoerceEnumerable(new object[] { 10, 20, 30 }, DataType.Int32);
        var typed = Assert.IsType<List<int>>(list);
        Assert.Equal(new[] { 10, 20, 30 }, typed);
    }

    [Fact]
    public void CoerceEnumerable_ConvertsMixedClrElementTypes()
    {
        // Ints and numeric strings both coerce to List<double> via Convert/parse —
        // the object-input behavior InstanceActor's MV-8 coercion depends on.
        var list = AttributeValueCodec.CoerceEnumerable(new object[] { 1, "2.5", 3.75 }, DataType.Double);
        var typed = Assert.IsType<List<double>>(list);
        Assert.Equal(new[] { 1.0, 2.5, 3.75 }, typed);
    }

    [Fact]
    public void CoerceEnumerable_DateTimeRoundtripsViaInvariantCulture()
    {
        var when = new DateTime(2026, 6, 19, 8, 30, 0, DateTimeKind.Utc);
        var list = AttributeValueCodec.CoerceEnumerable(
            new object[] { when.ToString("O", CultureInfo.InvariantCulture) }, DataType.DateTime);
        var typed = Assert.IsType<List<DateTime>>(list);
        Assert.Equal(when, typed[0]);
    }

    [Fact]
    public void CoerceEnumerable_ThrowsOnUnsupportedElementType() =>
        Assert.Throws<FormatException>(
            () => AttributeValueCodec.CoerceEnumerable(new object[] { new byte[] { 1 } }, DataType.Binary));
}
