using System.Dynamic;
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types;

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

/// <summary>
/// Tests for <see cref="DynamicJsonElement"/>, including the Commons-002 regression:
/// a wrapped element must remain valid for deferred (script-time) access even after
/// the <see cref="JsonDocument"/> it was parsed from has been disposed.
/// </summary>
public class DynamicJsonElementTests
{
    private static DynamicJsonElement Wrap(string json)
    {
        using var doc = JsonDocument.Parse(json);
        return new DynamicJsonElement(doc.RootElement);
        // doc is disposed here — a wrapper that retained a non-cloned element would
        // now throw ObjectDisposedException on the first member access.
    }

    // ── Commons-002 regression: lifetime independence from the source document ──

    [Fact]
    public void MemberAccess_WorksAfterSourceDocumentDisposed()
    {
        dynamic obj = Wrap("""{ "name": "pump", "id": 7 }""");

        Assert.Equal("pump", (string)obj.name);
        Assert.Equal(7, (int)obj.id);
    }

    [Fact]
    public void IndexAccess_WorksAfterSourceDocumentDisposed()
    {
        dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ] }""");

        Assert.Equal("b", obj.items[1]);
    }

    [Fact]
    public void NestedAccess_WorksAfterSourceDocumentDisposed()
    {
        dynamic obj = Wrap("""{ "outer": { "inner": { "value": 42 } } }""");

        Assert.Equal(42, (int)obj.outer.inner.value);
    }

    [Fact]
    public void ToString_WorksAfterSourceDocumentDisposed()
    {
        dynamic obj = Wrap("""{ "label": "site-1" }""");

        Assert.Equal("site-1", obj.label.ToString());
    }

    [Fact]
    public void Access_SurvivesGarbageCollection_OfSourceDocument()
    {
        // No reference to the source document is held anywhere; force collection
        // and finalization to prove the wrapper does not depend on it.
        var obj = MakeWrapperAndDropDocument();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        dynamic d = obj;
        Assert.Equal("ok", (string)d.status);
    }

    private static DynamicJsonElement MakeWrapperAndDropDocument()
    {
        var doc = JsonDocument.Parse("""{ "status": "ok" }""");
        var wrapper = new DynamicJsonElement(doc.RootElement);
        doc.Dispose();
        return wrapper;
    }

    // ── Basic conversion / access behavior ──────────────────────────

    [Fact]
    public void Convert_NumberToInt()
    {
        dynamic obj = Wrap("""{ "n": 123 }""");
        Assert.Equal(123, (int)obj.n);
    }

    [Fact]
    public void Convert_BoolFromJson()
    {
        dynamic obj = Wrap("""{ "flag": true }""");
        Assert.True((bool)obj.flag);
    }

    [Fact]
    public void MissingMember_Throws()
    {
        // TryGetMember returns false for an absent property, so the dynamic binder
        // surfaces a RuntimeBinderException — the standard DynamicObject contract.
        dynamic obj = Wrap("""{ "a": 1 }""");
        Assert.Throws<Microsoft.CSharp.RuntimeBinder.RuntimeBinderException>(
            () => { var _ = obj.doesNotExist; });
    }

    // ── Commons-006 regression: TryConvert(object) must never null out a present value ──

    [Fact]
    public void TryConvert_ObjectTarget_OnPresentValue_ReturnsNonNull()
    {
        // Directly exercise the DynamicObject.TryConvert contract for an `object`
        // target: a present JSON object/array/string must not convert to null.
        using var objDoc = JsonDocument.Parse("""{ "x": 1 }""");
        var objWrapper = new DynamicJsonElement(objDoc.RootElement);
        var convBinder = (ConvertBinder)Microsoft.CSharp.RuntimeBinder.Binder.Convert(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None, typeof(object), typeof(DynamicJsonElementTests));

        Assert.True(objWrapper.TryConvert(convBinder, out var result));
        Assert.NotNull(result);
    }

    [Fact]
    public void TryConvert_ObjectTarget_OnJsonNull_ReturnsNull()
    {
        // Only a genuinely null JSON value converts to a null object.
        using var doc = JsonDocument.Parse("""{ "v": null }""");
        var nullWrapper = new DynamicJsonElement(doc.RootElement.GetProperty("v"));
        var convBinder = (ConvertBinder)Microsoft.CSharp.RuntimeBinder.Binder.Convert(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None, typeof(object), typeof(DynamicJsonElementTests));

        Assert.True(nullWrapper.TryConvert(convBinder, out var result));
        Assert.Null(result);
    }

    [Fact]
    public void TryConvert_NonObjectTarget_OnUnconvertibleValue_ReportsFailure()
    {
        // Requesting int from a JSON string is genuinely unconvertible: TryConvert
        // must report false rather than a null success.
        using var doc = JsonDocument.Parse("""{ "s": "not-a-number" }""");
        var strWrapper = new DynamicJsonElement(doc.RootElement.GetProperty("s"));
        var convBinder = (ConvertBinder)Microsoft.CSharp.RuntimeBinder.Binder.Convert(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None, typeof(int), typeof(DynamicJsonElementTests));

        Assert.False(strWrapper.TryConvert(convBinder, out var result));
        Assert.Null(result);
    }

    // ── Commons-013 regression: integral index values other than int must work ──

    [Fact]
    public void IndexAccess_WithLongIndex_Works()
    {
        // DynamicJsonElement.Wrap surfaces JSON numbers as long; an index computed
        // from a wrapped JSON number (obj.items[obj.count - 1]) arrives as a long.
        dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ] }""");
        long idx = 1L;

        Assert.Equal("b", obj.items[idx]);
    }

    [Fact]
    public void IndexAccess_WithIndexDerivedFromWrappedJsonNumber_Works()
    {
        // The exact failing case from Commons-013: count is a wrapped JSON number
        // (unwrapped as long), so count - 1 is a long.
        dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ], "count": 3 }""");

        Assert.Equal("c", obj.items[obj.count - 1]);
    }

    [Theory]
    [InlineData((byte)0, "a")]
    [InlineData((short)1, "b")]
    public void IndexAccess_WithWideningIntegralIndex_Works(object index, string expected)
    {
        dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ] }""");

        Assert.Equal(expected, obj.items[index]);
    }

    [Fact]
    public void IndexAccess_WithLongIndexOutOfRange_Throws()
    {
        // An out-of-range long index is still rejected (binder surfaces the error).
        dynamic obj = Wrap("""{ "items": [ "a" ] }""");
        long idx = 5L;

        Assert.Throws<Microsoft.CSharp.RuntimeBinder.RuntimeBinderException>(
            () => { var _ = obj.items[idx]; });
    }
}
