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

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

public class ScriptParametersTests
{
    // ── Non-nullable scalar Get<T> ──────────────────────────────────

    [Fact]
    public void Get_Int_ExactTypeMatch()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42 });
        Assert.Equal(42, p.Get<int>("x"));
    }

    [Fact]
    public void Get_Int_FromLong()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42L });
        Assert.Equal(42, p.Get<int>("x"));
    }

    [Fact]
    public void Get_Int_FromString()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "42" });
        Assert.Equal(42, p.Get<int>("x"));
    }

    [Fact]
    public void Get_Int_MissingKey_Throws()
    {
        var p = new ScriptParameters();
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
        Assert.Contains("Parameter 'x' not found", ex.Message);
    }

    [Fact]
    public void Get_Int_NullValue_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = null });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
        Assert.Contains("Parameter 'x' value is null", ex.Message);
    }

    [Fact]
    public void Get_Int_Unparsable_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "abc" });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
        Assert.Contains("could not be parsed as Int32", ex.Message);
    }

    [Fact]
    public void Get_String_DirectReturn()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = "hello" });
        Assert.Equal("hello", p.Get<string>("s"));
    }

    [Fact]
    public void Get_String_NullValue_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = null });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<string>("s"));
        Assert.Contains("value is null", ex.Message);
    }

    [Fact]
    public void Get_String_FromInt()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = 42 });
        Assert.Equal("42", p.Get<string>("s"));
    }

    [Fact]
    public void Get_Bool_True()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = true });
        Assert.True(p.Get<bool>("b"));
    }

    [Fact]
    public void Get_Double_FromLong()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 42L });
        Assert.Equal(42.0, p.Get<double>("d"));
    }

    [Fact]
    public void Get_Double_FromFloat()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 3.14f });
        Assert.Equal(3.14, p.Get<double>("d"), 2);
    }

    [Fact]
    public void Get_Long_FromInt()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["l"] = 42 });
        Assert.Equal(42L, p.Get<long>("l"));
    }

    [Fact]
    public void Get_Float_FromDouble()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["f"] = 3.14 });
        Assert.Equal(3.14f, p.Get<float>("f"), 2);
    }

    [Fact]
    public void Get_DateTime_FromString()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["dt"] = "2026-03-22T10:30:00Z" });
        var result = p.Get<DateTime>("dt");
        Assert.Equal(new DateTime(2026, 3, 22, 10, 30, 0, DateTimeKind.Utc), result);
    }

    [Fact]
    public void Get_DateTime_ExactType()
    {
        var dt = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        var p = new ScriptParameters(new Dictionary<string, object?> { ["dt"] = dt });
        Assert.Equal(dt, p.Get<DateTime>("dt"));
    }

    // ── Nullable scalar Get<T?> ─────────────────────────────────────

    [Fact]
    public void Get_NullableInt_MissingKey_ReturnsNull()
    {
        var p = new ScriptParameters();
        Assert.Null(p.Get<int?>("x"));
    }

    [Fact]
    public void Get_NullableInt_NullValue_ReturnsNull()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = null });
        Assert.Null(p.Get<int?>("x"));
    }

    [Fact]
    public void Get_NullableInt_ValidValue_ReturnsValue()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = 42L });
        Assert.Equal(42, p.Get<int?>("x"));
    }

    // Commons-003: a parameter that is *present but unconvertible* is a caller/script
    // bug and must throw — not be silently mapped to null (which a script would
    // misread as "not supplied"). Genuinely absent/null still returns null.
    [Fact]
    public void Get_NullableInt_PresentButUnparsable_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = "banana" });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int?>("x"));
        Assert.Contains("could not be parsed as Int32", ex.Message);
    }

    [Fact]
    public void Get_NullableInt_PresentButOverflowing_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = long.MaxValue });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int?>("x"));
        Assert.Contains("could not be parsed as Int32", ex.Message);
    }

    [Fact]
    public void Get_NullableDateTime_PresentButUnparsable_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["dt"] = "not-a-date" });
        Assert.Throws<ScriptParameterException>(() => p.Get<DateTime?>("dt"));
    }

    [Fact]
    public void Get_NullableDouble_ValidValue()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 3.14 });
        Assert.Equal(3.14, p.Get<double?>("d"));
    }

    [Fact]
    public void Get_NullableBool_ValidValue()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = true });
        Assert.True(p.Get<bool?>("b"));
    }

    // ── Array Get<T[]> ──────────────────────────────────────────────

    [Fact]
    public void Get_IntArray_FromListOfLongs()
    {
        var list = new List<object?> { 1L, 2L, 3L };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
        Assert.Equal(new[] { 1, 2, 3 }, p.Get<int[]>("arr"));
    }

    [Fact]
    public void Get_IntArray_EmptyList()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = new List<object?>() });
        Assert.Empty(p.Get<int[]>("arr"));
    }

    [Fact]
    public void Get_IntArray_NonList_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = "not a list" });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
        Assert.Contains("is not a list or array", ex.Message);
    }

    [Fact]
    public void Get_IntArray_UnparsableElement_ThrowsWithIndex()
    {
        var list = new List<object?> { 1L, 2L, "bad" };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
        Assert.Contains("element at index 2", ex.Message);
        Assert.Contains("'bad'", ex.Message);
        Assert.Contains("Int32", ex.Message);
    }

    [Fact]
    public void Get_IntArray_NullElement_Throws()
    {
        var list = new List<object?> { 1L, null, 3L };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
        Assert.Contains("element at index 1 is null", ex.Message);
    }

    [Fact]
    public void Get_StringArray_FromListOfStrings()
    {
        var list = new List<object?> { "a", "b", "c" };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
        Assert.Equal(new[] { "a", "b", "c" }, p.Get<string[]>("arr"));
    }

    [Fact]
    public void Get_DoubleArray_FromListOfNumbers()
    {
        var list = new List<object?> { 1.1, 2.2, 3.3 };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["arr"] = list });
        Assert.Equal(new[] { 1.1, 2.2, 3.3 }, p.Get<double[]>("arr"));
    }

    [Fact]
    public void Get_IntArray_MissingKey_Throws()
    {
        var p = new ScriptParameters();
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int[]>("arr"));
        Assert.Contains("not found", ex.Message);
    }

    // ── List<T> Get<List<T>> ────────────────────────────────────────

    [Fact]
    public void Get_ListInt_FromListOfLongs()
    {
        var list = new List<object?> { 10L, 20L, 30L };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        var result = p.Get<List<int>>("items");
        Assert.Equal(new List<int> { 10, 20, 30 }, result);
    }

    [Fact]
    public void Get_ListString_FromListOfStrings()
    {
        var list = new List<object?> { "x", "y" };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        Assert.Equal(new List<string> { "x", "y" }, p.Get<List<string>>("items"));
    }

    [Fact]
    public void Get_ListInt_UnparsableElement_ThrowsWithIndex()
    {
        var list = new List<object?> { 1L, "oops" };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<List<int>>("items"));
        Assert.Contains("element at index 1", ex.Message);
    }

    [Fact]
    public void Get_ListDouble_FromMixedNumbers()
    {
        var list = new List<object?> { 1L, 2.5, 3 };
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        var result = p.Get<List<double>>("items");
        Assert.Equal(3, result.Count);
        Assert.Equal(1.0, result[0]);
        Assert.Equal(2.5, result[1]);
        Assert.Equal(3.0, result[2]);
    }

    // ── Backward compatibility ──────────────────────────────────────

    [Fact]
    public void Indexer_Works()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = "val" });
        Assert.Equal("val", p["key"]);
    }

    [Fact]
    public void ContainsKey_Works()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = 1 });
        Assert.True(p.ContainsKey("key"));
        Assert.False(p.ContainsKey("missing"));
    }

    [Fact]
    public void TryGetValue_Works()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["key"] = 42 });
        Assert.True(p.TryGetValue("key", out var val));
        Assert.Equal(42, val);
    }

    [Fact]
    public void Count_Works()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 });
        Assert.Equal(2, p.Count);
    }

    [Fact]
    public void Enumeration_Works()
    {
        var dict = new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 };
        var p = new ScriptParameters(dict);
        var keys = p.Select(kv => kv.Key).OrderBy(k => k).ToList();
        Assert.Equal(new[] { "a", "b" }, keys);
    }

    [Fact]
    public void EmptyConstructor_ProducesEmptyDictionary()
    {
        var p = new ScriptParameters();
        Assert.Empty(p);
        Assert.False(p.ContainsKey("anything"));
    }

    // ── Edge cases ──────────────────────────────────────────────────

    [Fact]
    public void Get_Double_FromInt()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = 42 });
        Assert.Equal(42.0, p.Get<double>("d"));
    }

    // ── JsonElement values (from JSON deserialization) ─────────────

    [Fact]
    public void Get_Int_FromJsonElement()
    {
        using var doc = JsonDocument.Parse("{\"x\": 42}");
        var element = doc.RootElement.GetProperty("x").Clone();
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = element });
        Assert.Equal(42, p.Get<int>("x"));
    }

    [Fact]
    public void Get_IntArray_FromJsonElementList()
    {
        // Simulates what JsonSerializer.Deserialize<List<object?>> produces
        using var doc = JsonDocument.Parse("[10, 20, 30]");
        var list = JsonSerializer.Deserialize<List<object?>>(doc.RootElement.GetRawText())!;
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        Assert.Equal(new[] { 10, 20, 30 }, p.Get<int[]>("items"));
    }

    [Fact]
    public void Get_ListInt_FromJsonElementList()
    {
        using var doc = JsonDocument.Parse("[1, 2, 3]");
        var list = JsonSerializer.Deserialize<List<object?>>(doc.RootElement.GetRawText())!;
        var p = new ScriptParameters(new Dictionary<string, object?> { ["items"] = list });
        Assert.Equal(new List<int> { 1, 2, 3 }, p.Get<List<int>>("items"));
    }

    [Fact]
    public void Get_String_FromJsonElement()
    {
        using var doc = JsonDocument.Parse("{\"s\": \"hello\"}");
        var element = doc.RootElement.GetProperty("s").Clone();
        var p = new ScriptParameters(new Dictionary<string, object?> { ["s"] = element });
        Assert.Equal("hello", p.Get<string>("s"));
    }

    [Fact]
    public void Get_Double_FromJsonElement()
    {
        using var doc = JsonDocument.Parse("{\"d\": 3.14}");
        var element = doc.RootElement.GetProperty("d").Clone();
        var p = new ScriptParameters(new Dictionary<string, object?> { ["d"] = element });
        Assert.Equal(3.14, p.Get<double>("d"));
    }

    [Fact]
    public void Get_Bool_FromJsonElement()
    {
        using var doc = JsonDocument.Parse("{\"b\": true}");
        var element = doc.RootElement.GetProperty("b").Clone();
        var p = new ScriptParameters(new Dictionary<string, object?> { ["b"] = element });
        Assert.True(p.Get<bool>("b"));
    }

    [Fact]
    public void Get_Int_OverflowFromLong_Throws()
    {
        var p = new ScriptParameters(new Dictionary<string, object?> { ["x"] = long.MaxValue });
        var ex = Assert.Throws<ScriptParameterException>(() => p.Get<int>("x"));
        Assert.Contains("could not be parsed as Int32", ex.Message);
    }
}
