# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Tests for JSON graph serialization/deserialization roundtrips."""

from __future__ import annotations

import json
from typing import Any, Callable

import pytest
import tvm_ffi
import tvm_ffi.testing
from tvm_ffi.serialization import from_json_graph_str, to_json_graph_str


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _roundtrip(obj: Any) -> Any:
    """Serialize then deserialize and return the result."""
    return from_json_graph_str(to_json_graph_str(obj))


def _assert_roundtrip_eq(obj: Any, cmp: Callable[..., Any] | None = None) -> None:
    """Assert that roundtrip preserves the value."""
    result = _roundtrip(obj)
    if cmp is not None:
        cmp(result)
    elif isinstance(obj, float):
        assert isinstance(result, float)
        assert result == pytest.approx(obj)
    elif obj is None:
        assert result is None
    else:
        _assert_any_equal(result, obj)


def _assert_any_equal(a: Any, b: Any) -> None:
    """Recursively compare two tvm_ffi values for equality."""
    if isinstance(b, tvm_ffi.Array):
        assert len(a) == len(b)
        for x, y in zip(a, b):
            _assert_any_equal(x, y)
    elif isinstance(b, (tvm_ffi.Map, tvm_ffi.Dict)):
        assert len(a) == len(b)
        for k in b:
            _assert_any_equal(a[k], b[k])
    elif isinstance(b, tvm_ffi.Shape):
        assert list(a) == list(b)
    elif isinstance(b, str):
        # tvm_ffi String inherits from str
        assert str(a) == str(b)
    else:
        assert a == b


# ---------------------------------------------------------------------------
# Primitive types
# ---------------------------------------------------------------------------
class TestNone:
    """Roundtrip tests for None."""

    def test_none(self) -> None:
        """None roundtrips to None."""
        assert _roundtrip(None) is None


class TestBool:
    """Roundtrip tests for bool values."""

    def test_true(self) -> None:
        """True roundtrips to True."""
        assert _roundtrip(True) is True

    def test_false(self) -> None:
        """False roundtrips to False."""
        assert _roundtrip(False) is False


class TestInt:
    """Roundtrip tests for integer values."""

    def test_zero(self) -> None:
        """Zero roundtrips correctly."""
        result = _roundtrip(0)
        assert result == 0

    def test_positive(self) -> None:
        """Positive int roundtrips correctly."""
        result = _roundtrip(42)
        assert result == 42

    def test_negative(self) -> None:
        """Negative int roundtrips correctly."""
        result = _roundtrip(-1)
        assert result == -1

    def test_large_positive(self) -> None:
        """Large positive int roundtrips correctly."""
        result = _roundtrip(10**15)
        assert result == 10**15

    def test_large_negative(self) -> None:
        """Large negative int roundtrips correctly."""
        result = _roundtrip(-(10**15))
        assert result == -(10**15)


class TestFloat:
    """Roundtrip tests for float values."""

    def test_zero(self) -> None:
        """Float zero roundtrips correctly."""
        result = _roundtrip(0.0)
        assert result == 0.0

    def test_positive(self) -> None:
        """Positive float roundtrips correctly."""
        result = _roundtrip(3.14159)
        assert result == pytest.approx(3.14159)

    def test_negative(self) -> None:
        """Negative float roundtrips correctly."""
        result = _roundtrip(-2.718)
        assert result == pytest.approx(-2.718)

    def test_very_small(self) -> None:
        """Very small float roundtrips correctly."""
        result = _roundtrip(1e-300)
        assert result == pytest.approx(1e-300)

    def test_very_large(self) -> None:
        """Very large float roundtrips correctly."""
        result = _roundtrip(1e300)
        assert result == pytest.approx(1e300)


# ---------------------------------------------------------------------------
# String types
# ---------------------------------------------------------------------------
class TestString:
    """Roundtrip tests for ffi.String values."""

    def test_empty(self) -> None:
        """Empty string roundtrips correctly."""
        _assert_roundtrip_eq(tvm_ffi.convert(""))

    def test_short(self) -> None:
        """Short string roundtrips correctly."""
        _assert_roundtrip_eq(tvm_ffi.convert("hello"))

    def test_long(self) -> None:
        """Long string roundtrips correctly."""
        _assert_roundtrip_eq(tvm_ffi.convert("x" * 1000))

    def test_special_chars(self) -> None:
        """String with special characters roundtrips correctly."""
        _assert_roundtrip_eq(tvm_ffi.convert('hello\nworld\t"quotes"'))

    def test_unicode(self) -> None:
        """Unicode string roundtrips correctly."""
        _assert_roundtrip_eq(tvm_ffi.convert("hello 世界"))


# ---------------------------------------------------------------------------
# DataType
# ---------------------------------------------------------------------------
class TestDataType:
    """Roundtrip tests for DLDataType values."""

    def test_int32(self) -> None:
        """int32 dtype roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.dtype("int32"))
        result = from_json_graph_str(s)
        assert str(result) == "int32"

    def test_float64(self) -> None:
        """float64 dtype roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.dtype("float64"))
        result = from_json_graph_str(s)
        assert str(result) == "float64"

    def test_bool(self) -> None:
        """Bool dtype roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.dtype("bool"))
        result = from_json_graph_str(s)
        assert str(result) == "bool"

    def test_vector(self) -> None:
        """Vector dtype roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.dtype("float32x4"))
        result = from_json_graph_str(s)
        assert str(result) == "float32x4"


# ---------------------------------------------------------------------------
# Device
# ---------------------------------------------------------------------------
class TestDevice:
    """Roundtrip tests for DLDevice values."""

    def test_cpu(self) -> None:
        """CPU device roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.Device("cpu", 0))
        result = from_json_graph_str(s)
        assert result.dlpack_device_type() == tvm_ffi.Device("cpu", 0).dlpack_device_type()
        assert result.index == 0

    def test_cuda(self) -> None:
        """CUDA device roundtrips correctly."""
        s = to_json_graph_str(tvm_ffi.Device("cuda", 1))
        result = from_json_graph_str(s)
        assert result.dlpack_device_type() == tvm_ffi.Device("cuda", 1).dlpack_device_type()
        assert result.index == 1


# ---------------------------------------------------------------------------
# Containers
# ---------------------------------------------------------------------------
class TestArray:
    """Roundtrip tests for ffi.Array containers."""

    def test_empty(self) -> None:
        """Empty array roundtrips correctly."""
        arr = tvm_ffi.convert([])
        _assert_roundtrip_eq(arr)

    def test_single_element(self) -> None:
        """Single-element array roundtrips correctly."""
        arr = tvm_ffi.convert([42])
        result = _roundtrip(arr)
        assert len(result) == 1
        assert result[0] == 42

    def test_multiple_elements(self) -> None:
        """Multi-element array roundtrips correctly."""
        arr = tvm_ffi.convert([1, 2, 3])
        result = _roundtrip(arr)
        assert len(result) == 3
        assert list(result) == [1, 2, 3]

    def test_mixed_types(self) -> None:
        """Array with mixed types roundtrips correctly."""
        arr = tvm_ffi.convert([42, "hello", True, None])
        result = _roundtrip(arr)
        assert len(result) == 4
        assert result[0] == 42
        assert result[1] == "hello"
        assert result[2] is True
        assert result[3] is None

    def test_nested_arrays(self) -> None:
        """Nested arrays roundtrip correctly."""
        inner1 = tvm_ffi.convert([1, 2])
        inner2 = tvm_ffi.convert([3])
        outer = tvm_ffi.convert([inner1, inner2])
        result = _roundtrip(outer)
        assert len(result) == 2
        assert list(result[0]) == [1, 2]
        assert list(result[1]) == [3]

    def test_duplicated_elements(self) -> None:
        """Array with duplicated elements roundtrips correctly."""
        arr = tvm_ffi.convert([42, 42, 42])
        result = _roundtrip(arr)
        assert len(result) == 3
        assert all(x == 42 for x in result)


class TestMap:
    """Roundtrip tests for ffi.Map containers."""

    def test_empty(self) -> None:
        """Empty map roundtrips correctly."""
        m = tvm_ffi.convert({})
        _assert_roundtrip_eq(m)

    def test_single_entry(self) -> None:
        """Single-entry map roundtrips correctly."""
        m = tvm_ffi.convert({"key": 42})
        result = _roundtrip(m)
        assert len(result) == 1
        assert result["key"] == 42

    def test_multiple_entries(self) -> None:
        """Multi-entry map roundtrips correctly."""
        m = tvm_ffi.convert({"a": 1, "b": 2, "c": 3})
        result = _roundtrip(m)
        assert len(result) == 3
        assert result["a"] == 1
        assert result["b"] == 2
        assert result["c"] == 3

    def test_mixed_value_types(self) -> None:
        """Map with mixed value types roundtrips correctly."""
        m = tvm_ffi.convert({"int": 42, "str": "hello", "bool": True, "none": None})
        result = _roundtrip(m)
        assert result["int"] == 42
        assert result["str"] == "hello"
        assert result["bool"] is True
        assert result["none"] is None

    def test_nested_map(self) -> None:
        """Nested maps roundtrip correctly."""
        inner = tvm_ffi.convert({"x": 1})
        outer = tvm_ffi.convert({"inner": inner})
        result = _roundtrip(outer)
        assert result["inner"]["x"] == 1

    def test_map_with_array_value(self) -> None:
        """Map with array values roundtrips correctly."""
        arr = tvm_ffi.convert([10, 20])
        m = tvm_ffi.convert({"nums": arr})
        result = _roundtrip(m)
        assert list(result["nums"]) == [10, 20]


class TestDict:
    """Roundtrip tests for ffi.Dict containers."""

    def test_empty(self) -> None:
        """Empty dict roundtrips correctly."""
        d = tvm_ffi.Dict({})
        _assert_roundtrip_eq(d)

    def test_single_entry(self) -> None:
        """Single-entry dict roundtrips correctly."""
        d = tvm_ffi.Dict({"key": 42})
        result = _roundtrip(d)
        assert len(result) == 1
        assert result["key"] == 42

    def test_multiple_entries(self) -> None:
        """Multi-entry dict roundtrips correctly."""
        d = tvm_ffi.Dict({"a": 1, "b": 2, "c": 3})
        result = _roundtrip(d)
        assert len(result) == 3
        assert result["a"] == 1
        assert result["b"] == 2
        assert result["c"] == 3

    def test_mixed_value_types(self) -> None:
        """Dict with mixed value types roundtrips correctly."""
        d = tvm_ffi.Dict({"int": 42, "str": "hello", "bool": True, "none": None})
        result = _roundtrip(d)
        assert result["int"] == 42
        assert result["str"] == "hello"
        assert result["bool"] is True
        assert result["none"] is None

    def test_nested_dict(self) -> None:
        """Nested dicts roundtrip correctly."""
        inner = tvm_ffi.Dict({"x": 1})
        outer = tvm_ffi.Dict({"inner": inner})
        result = _roundtrip(outer)
        assert result["inner"]["x"] == 1

    def test_dict_with_array_value(self) -> None:
        """Dict with array values roundtrips correctly."""
        arr = tvm_ffi.convert([10, 20])
        d = tvm_ffi.Dict({"nums": arr})
        result = _roundtrip(d)
        assert list(result["nums"]) == [10, 20]


class TestShape:
    """Roundtrip tests for ffi.Shape containers."""

    def test_empty(self) -> None:
        """Empty shape roundtrips correctly."""
        shape = tvm_ffi.Shape(())
        _assert_roundtrip_eq(shape)

    def test_1d(self) -> None:
        """1D shape roundtrips correctly."""
        shape = tvm_ffi.Shape((10,))
        result = _roundtrip(shape)
        assert list(result) == [10]

    def test_nd(self) -> None:
        """N-D shape roundtrips correctly."""
        shape = tvm_ffi.Shape((1, 2, 3, 4))
        result = _roundtrip(shape)
        assert list(result) == [1, 2, 3, 4]


# ---------------------------------------------------------------------------
# Objects with reflection
# ---------------------------------------------------------------------------
class TestObjectSerialization:
    """Roundtrip tests for objects with reflection metadata."""

    def test_int_pair_roundtrip(self) -> None:
        """TestIntPair has refl::init and POD int64 fields."""
        pair = tvm_ffi.testing.TestIntPair(3, 7)
        s = to_json_graph_str(pair)
        result = from_json_graph_str(s)
        assert result.a == 3
        assert result.b == 7

    def test_int_pair_zero_values(self) -> None:
        """TestIntPair with zero values roundtrips correctly."""
        pair = tvm_ffi.testing.TestIntPair(0, 0)
        result = _roundtrip(pair)
        assert result.a == 0
        assert result.b == 0

    def test_int_pair_negative_values(self) -> None:
        """TestIntPair with negative values roundtrips correctly."""
        pair = tvm_ffi.testing.TestIntPair(-100, -200)
        result = _roundtrip(pair)
        assert result.a == -100
        assert result.b == -200

    def test_int_pair_large_values(self) -> None:
        """TestIntPair with large values roundtrips correctly."""
        pair = tvm_ffi.testing.TestIntPair(10**15, -(10**15))
        result = _roundtrip(pair)
        assert result.a == 10**15
        assert result.b == -(10**15)


# ---------------------------------------------------------------------------
# JSON structure verification
# ---------------------------------------------------------------------------
class TestJSONStructure:
    """Tests verifying the internal JSON graph structure."""

    def test_null_json_structure(self) -> None:
        """None produces a single node with type 'None'."""
        s = to_json_graph_str(None)
        parsed = json.loads(s)
        assert parsed["root_index"] == 0
        assert len(parsed["nodes"]) == 1
        assert parsed["nodes"][0]["type"] == "None"

    def test_bool_json_structure(self) -> None:
        """Bool produces a node with type 'bool'."""
        s = to_json_graph_str(True)
        parsed = json.loads(s)
        assert parsed["nodes"][0]["type"] == "bool"
        assert parsed["nodes"][0]["data"] is True

    def test_int_json_structure(self) -> None:
        """Int produces a node with type 'int'."""
        s = to_json_graph_str(42)
        parsed = json.loads(s)
        assert parsed["nodes"][0]["type"] == "int"
        assert parsed["nodes"][0]["data"] == 42

    def test_float_json_structure(self) -> None:
        """Float produces a node with type 'float'."""
        s = to_json_graph_str(3.14)
        parsed = json.loads(s)
        assert parsed["nodes"][0]["type"] == "float"
        assert parsed["nodes"][0]["data"] == pytest.approx(3.14)

    def test_string_json_structure(self) -> None:
        """String produces a node with type 'ffi.String'."""
        s = to_json_graph_str(tvm_ffi.convert("hello"))
        parsed = json.loads(s)
        assert parsed["nodes"][parsed["root_index"]]["type"] == "ffi.String"
        assert parsed["nodes"][parsed["root_index"]]["data"] == "hello"

    def test_array_json_structure(self) -> None:
        """Array data contains node index references."""
        s = to_json_graph_str(tvm_ffi.convert([1, 2]))
        parsed = json.loads(s)
        root = parsed["nodes"][parsed["root_index"]]
        assert root["type"] == "ffi.Array"
        # data should be list of node indices
        assert isinstance(root["data"], list)
        assert len(root["data"]) == 2

    def test_map_json_structure(self) -> None:
        """Map data contains flattened key-value node index pairs."""
        s = to_json_graph_str(tvm_ffi.convert({"a": 1}))
        parsed = json.loads(s)
        root = parsed["nodes"][parsed["root_index"]]
        assert root["type"] == "ffi.Map"
        assert isinstance(root["data"], list)
        # key-value pairs flattened: [key_idx, val_idx]
        assert len(root["data"]) == 2

    def test_dict_json_structure(self) -> None:
        """Dict data contains flattened key-value node index pairs."""
        s = to_json_graph_str(tvm_ffi.Dict({"a": 1}))
        parsed = json.loads(s)
        root = parsed["nodes"][parsed["root_index"]]
        assert root["type"] == "ffi.Dict"
        assert isinstance(root["data"], list)
        # key-value pairs flattened: [key_idx, val_idx]
        assert len(root["data"]) == 2

    def test_node_dedup(self) -> None:
        """Duplicate values should share the same node index."""
        s = to_json_graph_str(tvm_ffi.convert([42, 42, 42]))
        parsed = json.loads(s)
        root = parsed["nodes"][parsed["root_index"]]
        # all three elements should reference the same node
        assert root["data"][0] == root["data"][1] == root["data"][2]

    def test_object_pod_fields_are_inlined(self) -> None:
        """POD fields (int, bool, float) are inlined directly via field_static_type_index."""
        pair = tvm_ffi.testing.TestIntPair(3, 7)
        s = to_json_graph_str(pair)
        parsed = json.loads(s)
        root = parsed["nodes"][parsed["root_index"]]
        assert root["type"] == "testing.TestIntPair"
        # fields 'a' and 'b' should be inlined as direct int values (not node indices)
        assert root["data"]["a"] == 3
        assert root["data"]["b"] == 7


# ---------------------------------------------------------------------------
# Metadata
# ---------------------------------------------------------------------------
class TestMetadata:
    """Tests for optional metadata in serialized output."""

    def test_with_metadata(self) -> None:
        """Metadata dict appears in serialized JSON when provided."""
        s = to_json_graph_str(42, {"version": "1.0"})
        parsed = json.loads(s)
        assert "metadata" in parsed
        assert parsed["metadata"]["version"] == "1.0"

    def test_without_metadata(self) -> None:
        """Metadata key is absent when not provided."""
        s = to_json_graph_str(42)
        parsed = json.loads(s)
        assert "metadata" not in parsed


# ---------------------------------------------------------------------------
# Error cases
# ---------------------------------------------------------------------------
class TestErrors:
    """Tests for error handling in deserialization."""

    def test_invalid_json_string(self) -> None:
        """Invalid JSON string raises an error."""
        with pytest.raises(Exception):
            from_json_graph_str("not valid json")

    def test_empty_json_string(self) -> None:
        """Empty string raises an error."""
        with pytest.raises(Exception):
            from_json_graph_str("")

    def test_missing_root_index(self) -> None:
        """JSON without root_index raises an error."""
        with pytest.raises(Exception):
            from_json_graph_str('{"nodes": []}')

    def test_missing_nodes(self) -> None:
        """JSON without nodes raises an error."""
        with pytest.raises(Exception):
            from_json_graph_str('{"root_index": 0}')
