using System.Text.Json;
using ScadaLink.Commons.Types;

namespace ScadaLink.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; });
    }
}
