using System;
using Google.Protobuf;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.Conversion;
using ProtobufTimestamp = Google.Protobuf.WellKnownTypes.Timestamp;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.Conversion;

public sealed class VariantConverterTests
{
    private readonly VariantConverter _converter = new();

    /// <summary>Verifies that supported scalar types are converted with correct data type and value kind.</summary>
    /// <param name="value">Scalar value to convert.</param>
    /// <param name="expectedDataType">Expected MxDataType of the converted value.</param>
    /// <param name="expectedKind">Expected KindOneofCase of the converted value.</param>
    [Theory]
    [InlineData(true, MxDataType.Boolean, MxValue.KindOneofCase.BoolValue)]
    [InlineData(42, MxDataType.Integer, MxValue.KindOneofCase.Int32Value)]
    [InlineData(42L, MxDataType.Integer, MxValue.KindOneofCase.Int64Value)]
    [InlineData(1.25f, MxDataType.Float, MxValue.KindOneofCase.FloatValue)]
    [InlineData(2.5d, MxDataType.Double, MxValue.KindOneofCase.DoubleValue)]
    [InlineData("value", MxDataType.String, MxValue.KindOneofCase.StringValue)]
    public void Convert_WithSupportedScalar_ProjectsTypedValue(
        object value,
        MxDataType expectedDataType,
        MxValue.KindOneofCase expectedKind)
    {
        MxValue converted = _converter.Convert(value);

        Assert.Equal(expectedDataType, converted.DataType);
        Assert.Equal(expectedKind, converted.KindCase);
        Assert.False(string.IsNullOrWhiteSpace(converted.VariantType));
    }

    /// <summary>Verifies that DateTime values are converted to protobuf timestamps.</summary>
    [Fact]
    public void Convert_WithDateTime_ProjectsTimestamp()
    {
        DateTime dateTime = new(2026, 4, 26, 17, 45, 0, DateTimeKind.Utc);

        MxValue converted = _converter.Convert(dateTime);

        Assert.Equal(MxDataType.Time, converted.DataType);
        Assert.Equal(ProtobufTimestamp.FromDateTime(dateTime), converted.TimestampValue);
        Assert.Equal("VT_DATE", converted.VariantType);
    }

    /// <summary>Verifies that scalar MxValue kinds convert to the matching boxed CLR type for a COM write.</summary>
    [Fact]
    public void ConvertToComValue_WithInt32_ReturnsBoxedInt()
    {
        object? result = _converter.ConvertToComValue(new MxValue { Int32Value = 123 });

        Assert.Equal(123, Assert.IsType<int>(result));
    }

    /// <summary>Verifies that a boolean MxValue converts to a boxed bool for a COM write.</summary>
    [Fact]
    public void ConvertToComValue_WithBool_ReturnsBoxedBool()
    {
        object? result = _converter.ConvertToComValue(new MxValue { BoolValue = true });

        Assert.True(Assert.IsType<bool>(result));
    }

    /// <summary>Verifies that a string MxValue converts to a string for a COM write.</summary>
    [Fact]
    public void ConvertToComValue_WithString_ReturnsString()
    {
        object? result = _converter.ConvertToComValue(new MxValue { StringValue = "abc" });

        Assert.Equal("abc", Assert.IsType<string>(result));
    }

    /// <summary>Verifies that a timestamp MxValue converts to a UTC DateTime the COM marshaler renders as VT_DATE.</summary>
    [Fact]
    public void ConvertToComValue_WithTimestamp_ReturnsUtcDateTime()
    {
        DateTime dateTime = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);

        object? result = _converter.ConvertToComValue(
            new MxValue { TimestampValue = ProtobufTimestamp.FromDateTime(dateTime) });

        Assert.Equal(dateTime, Assert.IsType<DateTime>(result));
    }

    /// <summary>Verifies that an MXAccess-null MxValue converts to a CLR null.</summary>
    [Fact]
    public void ConvertToComValue_WithNull_ReturnsNull()
    {
        object? result = _converter.ConvertToComValue(new MxValue { IsNull = true });

        Assert.Null(result);
    }

    /// <summary>Verifies that an integer-array MxValue converts to an int array the COM marshaler renders as a SAFEARRAY.</summary>
    [Fact]
    public void ConvertToComValue_WithInt32Array_ReturnsInt32Array()
    {
        MxValue value = new()
        {
            ArrayValue = new MxArray
            {
                Int32Values = new Int32Array { Values = { 1, 2, 3 } },
            },
        };

        object? result = _converter.ConvertToComValue(value);

        Assert.Equal(new[] { 1, 2, 3 }, Assert.IsType<int[]>(result));
    }

    /// <summary>Verifies that an MxValue with no value kind set cannot be converted for a COM write.</summary>
    [Fact]
    public void ConvertToComValue_WithNoKind_Throws()
    {
        Assert.Throws<ArgumentException>(() => _converter.ConvertToComValue(new MxValue()));
    }

    /// <summary>Verifies that file time values with expected time data type are converted to protobuf timestamps.</summary>
    [Fact]
    public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
    {
        DateTime dateTime = new(2026, 4, 26, 17, 45, 0, DateTimeKind.Utc);

        MxValue converted = _converter.Convert(dateTime.ToFileTimeUtc(), MxDataType.Time);

        Assert.Equal(MxDataType.Time, converted.DataType);
        Assert.Equal(ProtobufTimestamp.FromDateTime(dateTime), converted.TimestampValue);
        Assert.Equal("VT_I8", converted.VariantType);
    }

    /// <summary>
    /// A 32-bit <see cref="uint"/> with an expected
    /// data type of <see cref="MxDataType.Time"/> must not be projected as a
    /// Windows FILETIME. A uint can only hold the low 32 bits of a FILETIME,
    /// which would silently render as a near-1601 timestamp; the converter
    /// must fall through to an integer projection instead.
    /// </summary>
    [Fact]
    public void Convert_WithUInt32AndExpectedTime_DoesNotProjectFileTime()
    {
        const uint value = 123456789u;

        MxValue converted = _converter.Convert(value, MxDataType.Time);

        Assert.Equal(MxDataType.Integer, converted.DataType);
        Assert.Equal(MxValue.KindOneofCase.Int64Value, converted.KindCase);
        Assert.Equal(value, converted.Int64Value);
        Assert.Equal("VT_UI4", converted.VariantType);
    }

    /// <summary>Verifies that null-like values preserve their null semantics and variant type.</summary>
    /// <param name="value">Null-like value to convert.</param>
    /// <param name="expectedVariantType">Expected variant type string.</param>
    [Theory]
    [InlineData(null, "VT_EMPTY")]
    [InlineData(typeof(DBNull), "VT_NULL")]
    public void Convert_WithNullLikeValue_PreservesNull(
        object? value,
        string expectedVariantType)
    {
        object? actualValue = value is System.Type ? DBNull.Value : value;

        MxValue converted = _converter.Convert(actualValue);

        Assert.True(converted.IsNull);
        Assert.Equal(MxDataType.NoData, converted.DataType);
        Assert.Equal(expectedVariantType, converted.VariantType);
        Assert.Equal(MxValue.KindOneofCase.None, converted.KindCase);
    }

    /// <summary>Verifies that supported array types are converted with correct element type and dimensions.</summary>
    [Fact]
    public void ConvertArray_WithSupportedArrays_ProjectsTypedValuesAndDimensions()
    {
        MxValue bools = _converter.Convert(new[] { true, false });
        MxValue ints = _converter.Convert(new[] { 1, 2, 3 });
        MxValue floats = _converter.Convert(new[] { 1.25f, 2.5f });
        MxValue doubles = _converter.Convert(new[] { 1.25d, 2.5d });
        MxValue strings = _converter.Convert(new[] { "one", "two" });
        MxValue times = _converter.Convert(new[]
        {
            new DateTime(2026, 4, 26, 17, 45, 0, DateTimeKind.Utc),
            new DateTime(2026, 4, 26, 17, 46, 0, DateTimeKind.Utc),
        });

        Assert.Equal(new[] { true, false }, bools.ArrayValue.BoolValues.Values);
        Assert.Equal(new[] { 1, 2, 3 }, ints.ArrayValue.Int32Values.Values);
        Assert.Equal(new[] { 1.25f, 2.5f }, floats.ArrayValue.FloatValues.Values);
        Assert.Equal(new[] { 1.25d, 2.5d }, doubles.ArrayValue.DoubleValues.Values);
        Assert.Equal(new[] { "one", "two" }, strings.ArrayValue.StringValues.Values);
        Assert.Equal(2, times.ArrayValue.TimestampValues.Values.Count);
        Assert.Equal(new uint[] { 2 }, bools.ArrayValue.Dimensions);
        Assert.Equal(MxDataType.Boolean, bools.ArrayValue.ElementDataType);
    }

    /// <summary>Verifies that multidimensional arrays preserve rank and dimension information.</summary>
    [Fact]
    public void ConvertArray_WithMultidimensionalArray_PreservesRankAndDimensions()
    {
        int[,] values =
        {
            { 1, 2, 3 },
            { 4, 5, 6 },
        };

        MxValue converted = _converter.Convert(values);

        Assert.Equal(new uint[] { 2, 3 }, converted.ArrayValue.Dimensions);
        Assert.Equal(new[] { 1, 2, 3, 4, 5, 6 }, converted.ArrayValue.Int32Values.Values);
    }

    /// <summary>Verifies that file time arrays with expected time data type are converted to timestamp arrays.</summary>
    [Fact]
    public void ConvertArray_WithExpectedTimeAndFileTimeValues_ProjectsTimestampArray()
    {
        DateTime first = new(2026, 4, 26, 17, 45, 0, DateTimeKind.Utc);
        DateTime second = new(2026, 4, 26, 17, 46, 0, DateTimeKind.Utc);

        MxValue converted = _converter.Convert(
            new[] { first.ToFileTimeUtc(), second.ToFileTimeUtc() },
            MxDataType.Time);

        Assert.Equal(MxDataType.Time, converted.ArrayValue.ElementDataType);
        Assert.Equal(
            new[] { ProtobufTimestamp.FromDateTime(first), ProtobufTimestamp.FromDateTime(second) },
            converted.ArrayValue.TimestampValues.Values);
    }

    /// <summary>Verifies that unknown scalar types preserve raw value and diagnostic metadata.</summary>
    [Fact]
    public void Convert_WithUnknownScalar_PreservesRawMetadata()
    {
        UnsupportedVariant value = new("opaque");

        MxValue converted = _converter.Convert(value);

        Assert.Equal(MxDataType.Unknown, converted.DataType);
        Assert.Equal(MxValue.KindOneofCase.RawValue, converted.KindCase);
        Assert.Contains(typeof(UnsupportedVariant).FullName!, converted.VariantType);
        Assert.Contains(typeof(UnsupportedVariant).FullName!, converted.RawDiagnostic);
        Assert.Equal(ByteString.CopyFromUtf8("opaque"), converted.RawValue);
    }

    /// <summary>Verifies that unknown array types preserve raw values and diagnostic metadata.</summary>
    [Fact]
    public void ConvertArray_WithUnknownArray_PreservesRawMetadata()
    {
        UnsupportedVariant[] values =
        [
            new("first"),
            new("second"),
        ];

        MxValue converted = _converter.Convert(values);

        Assert.Equal(MxDataType.Unknown, converted.ArrayValue.ElementDataType);
        Assert.Equal(MxArray.ValuesOneofCase.RawValues, converted.ArrayValue.ValuesCase);
        Assert.Equal(new uint[] { 2 }, converted.ArrayValue.Dimensions);
        Assert.Equal("first", converted.ArrayValue.RawValues.Values[0].ToStringUtf8());
        Assert.Contains(typeof(UnsupportedVariant).FullName!, converted.ArrayValue.RawDiagnostic);
    }

    /// <summary>Fake unsupported variant type for testing unknown type handling.</summary>
    private sealed class UnsupportedVariant
    {
        private readonly string _value;

        /// <summary>Initializes a new instance of the UnsupportedVariant class.</summary>
        /// <param name="value">The opaque value.</param>
        public UnsupportedVariant(string value)
        {
            _value = value;
        }

        /// <inheritdoc />
        public override string ToString()
        {
            return _value;
        }
    }
}
