import pytest
from pathlib import Path

from compas.geometry import Point
from compas.geometry import Frame
from compas.geometry import Vector
from compas.geometry import Line
from compas.geometry import Circle

from compas_pb import pb_dump
from compas_pb import pb_load
from compas_pb import pb_dump_json
from compas_pb import pb_load_json
from compas_pb.core import serialize_message_to_json
from compas_pb.core import deserialize_message_from_json


@pytest.fixture
def temp_file():
    filepath = Path(__file__).parent / "nested_data.bin"
    return filepath


@pytest.fixture
def point():
    return Point(1, 2, 3)


@pytest.fixture
def line():
    return Line(Point(1, 2, 3), Point(4, 5, 6))


@pytest.fixture
def frame():
    return Frame(Point(1, 2, 3), Vector(4, 5, 6), Vector(7, 8, 9))


@pytest.fixture
def vector():
    return Vector(1, 2, 3)


@pytest.fixture
def nested_list():
    return [Point(4.0, 5.0, 6.0), [Vector(7.0, 8.0, 9.0), [Point(10.0, 11.0, 12.0)]]]


@pytest.fixture
def primitive_data():
    return (["I am String", [0.0, 0.5, 1.5], True, 5, 10],)


@pytest.fixture
def nested_dict():
    return {
        "point": Point(1.0, 2.0, 3.0),
        "line": [Point(1.0, 2.0, 3.0), Point(4.0, 5.0, 6.0)],
        "list of Object": [Point(4.0, 5.0, 6.0), [Vector(7.0, 8.0, 9.0), Point(0.0, 0.5, 0.3)]],  # Nested list
        "frame": Frame(Point(1.0, 2.0, 3.0), Vector(4.0, 5.0, 6.0), Vector(7.0, 8.0, 9.0)),
        "list of primtive": ["I am String", [0.0, 0.5, 1.5], True, 5, 10],
        "bytestream": b"this is a byte stream",
        "circle": Circle.from_point_and_radius(Point(1.0, 2.0, 3.0), 5.0),
    }


def test_pb_dump(temp_file, nested_dict):
    # Test pb_dump with a file path
    pb_dump(nested_dict, filepath=temp_file.as_posix())
    assert temp_file.exists()


def test_pb_load(temp_file, nested_dict):
    # Test pb_load with a file path
    pb_dump(nested_dict, filepath=temp_file.as_posix())
    loaded_data = pb_load(filepath=temp_file.as_posix())
    assert loaded_data == nested_dict


def test_serialize_message_to_json(nested_dict):
    """Test serialize_message_to_json function with various data types."""
    # Test with nested dictionary
    json_string = serialize_message_to_json(nested_dict)
    assert isinstance(json_string, str)
    assert len(json_string) > 0
    assert '"data"' in json_string  # Should contain protobuf message structure

    # Test with simple point
    point = Point(1, 2, 3)
    json_string = serialize_message_to_json(point)
    assert isinstance(json_string, str)
    assert len(json_string) > 0
    assert '"data"' in json_string
    assert '"@type"' in json_string  # Protobuf message type annotation

    # Test with list
    point_list = [Point(1, 2, 3), Point(4, 5, 6)]
    json_string = serialize_message_to_json(point_list)
    assert isinstance(json_string, str)
    assert len(json_string) > 0
    assert '"data"' in json_string

    # Test with primitive data
    primitive = ["test", 123, True, 3.14]
    json_string = serialize_message_to_json(primitive)
    assert isinstance(json_string, str)
    assert len(json_string) > 0
    assert '"data"' in json_string


def test_deserialize_message_from_json(nested_dict):
    """Test deserialize_message_from_json function with various data types."""
    # Test with nested dictionary
    json_string = serialize_message_to_json(nested_dict)
    loaded_data = deserialize_message_from_json(json_string)
    assert loaded_data == nested_dict

    # Test with simple point
    point = Point(1, 2, 3)
    json_string = serialize_message_to_json(point)
    loaded_point = deserialize_message_from_json(json_string)
    assert loaded_point == point

    # Test with list
    point_list = [Point(1, 2, 3), Point(4, 5, 6)]
    json_string = serialize_message_to_json(point_list)
    loaded_list = deserialize_message_from_json(json_string)
    assert loaded_list == point_list

    # Test with primitive data (accounting for float precision)
    primitive = ["test", 123, True, 3.14]
    json_string = serialize_message_to_json(primitive)
    loaded_primitive = deserialize_message_from_json(json_string)
    # Check each element individually to handle float precision
    assert len(loaded_primitive) == len(primitive)
    assert loaded_primitive[0] == primitive[0]  # string
    assert loaded_primitive[1] == primitive[1]  # int
    assert loaded_primitive[2] == primitive[2]  # bool
    assert abs(loaded_primitive[3] - primitive[3]) < 1e-6  # float with tolerance


def test_pb_dump_json(nested_dict):
    """Test pb_dump_json function."""
    json_string = pb_dump_json(nested_dict)
    assert isinstance(json_string, str)
    assert len(json_string) > 0
    assert '"data"' in json_string  # Should contain protobuf message structure
    # Verify it's valid JSON by parsing it
    import json

    parsed_json = json.loads(json_string)
    assert "data" in parsed_json


def test_pb_load_json(nested_dict):
    """Test pb_load_json function."""
    json_string = pb_dump_json(nested_dict)
    loaded_data = pb_load_json(json_string)
    assert loaded_data == nested_dict


def test_json_structure_validation():
    """Test that JSON output has the expected protobuf message structure."""
    # Test with a simple point
    point = Point(1, 2, 3)
    json_string = serialize_message_to_json(point)

    # Parse the JSON to validate structure
    import json

    parsed = json.loads(json_string)

    # Should have the basic protobuf message structure
    assert "data" in parsed
    assert "data" in parsed["data"]  # Nested data field
    assert "@type" in parsed["data"]["data"]  # Type annotation

    # The @type should contain the protobuf message type
    assert "type.googleapis.com" in parsed["data"]["data"]["@type"]
    assert "PointData" in parsed["data"]["data"]["@type"]

    # Should contain point coordinates
    assert "x" in parsed["data"]["data"]
    assert "y" in parsed["data"]["data"]
    assert "z" in parsed["data"]["data"]

    # Test with a simple list
    simple_list = [1, 2, 3]
    json_string = serialize_message_to_json(simple_list)
    parsed = json.loads(json_string)

    assert "data" in parsed
    assert "data" in parsed["data"]  # Nested data field


def test_json_exact_match():
    """Test that JSON output exactly matches expected format."""
    # Test with a simple point
    point = Point(1, 2, 3)
    json_string = serialize_message_to_json(point)

    # Parse and normalize the JSON for comparison
    import json

    parsed = json.loads(json_string)

    # Expected structure for a Point
    expected_structure = {
        "data": {
            "data": {
                "@type": "type.googleapis.com/compas_pb.data.PointData",
                "guid": parsed["data"]["data"]["guid"],  # GUID is dynamic, so we use the actual one
                "name": "Point",
                "x": 1.0,
                "y": 2.0,
                "z": 3.0,
            }
        }
    }

    assert parsed == expected_structure

    # Test with primitive data
    primitive = ["test", 123, True, 3.14]
    json_string = serialize_message_to_json(primitive)
    parsed = json.loads(json_string)

    # Expected structure for a list of primitives
    expected_structure = {
        "data": {
            "data": {
                "@type": "type.googleapis.com/compas_pb.data.ListData",
                "data": [
                    {"data": {"@type": "type.googleapis.com/compas_pb.data.PrimitiveData", "str": "test"}},
                    {"data": {"@type": "type.googleapis.com/compas_pb.data.PrimitiveData", "int": 123}},
                    {"data": {"@type": "type.googleapis.com/compas_pb.data.PrimitiveData", "bool": True}},
                    {"data": {"@type": "type.googleapis.com/compas_pb.data.PrimitiveData", "float": 3.14}},
                ],
            }
        }
    }

    assert parsed == expected_structure


def test_json_roundtrip_complex_data(nested_dict, nested_list, primitive_data):
    """Test JSON serialization/deserialization roundtrip with complex data structures."""
    # Test with nested dictionary
    json_string = serialize_message_to_json(nested_dict)
    assert '"data"' in json_string  # DictData structure
    assert '"data"' in json_string  # Nested data field
    loaded_dict = deserialize_message_from_json(json_string)
    assert loaded_dict == nested_dict

    # Test with nested list
    json_string = serialize_message_to_json(nested_list)
    assert '"data"' in json_string  # ListData structure
    assert '"data"' in json_string  # Nested data field
    loaded_list = deserialize_message_from_json(json_string)
    assert loaded_list == nested_list

    # Test with primitive data (accounting for tuple vs list conversion)
    json_string = serialize_message_to_json(primitive_data)
    assert '"data"' in json_string  # ListData structure
    loaded_primitive = deserialize_message_from_json(json_string)
    # primitive_data is a tuple, but deserialization returns a list
    # Convert tuple to list for comparison
    expected_list = list(primitive_data)
    assert loaded_primitive == expected_list


def test_json_roundtrip_simple_objects(point, line, frame, vector):
    """Test JSON serialization/deserialization roundtrip with simple COMPAS objects."""
    # Test Point
    json_string = serialize_message_to_json(point)
    assert '"@type"' in json_string  # Protobuf message type annotation
    assert '"data"' in json_string
    assert '"type.googleapis.com/compas_pb.data.PointData"' in json_string  # Full protobuf type URL
    loaded_point = deserialize_message_from_json(json_string)
    assert loaded_point == point

    # Test Line
    json_string = serialize_message_to_json(line)
    assert '"@type"' in json_string
    assert '"data"' in json_string
    assert '"type.googleapis.com/compas_pb.data.LineData"' in json_string  # Full protobuf type URL
    loaded_line = deserialize_message_from_json(json_string)
    assert loaded_line == line

    # Test Frame
    json_string = serialize_message_to_json(frame)
    assert '"@type"' in json_string
    assert '"data"' in json_string
    assert '"type.googleapis.com/compas_pb.data.FrameData"' in json_string  # Full protobuf type URL
    loaded_frame = deserialize_message_from_json(json_string)
    assert loaded_frame == frame

    # Test Vector
    json_string = serialize_message_to_json(vector)
    assert '"@type"' in json_string
    assert '"data"' in json_string
    assert '"type.googleapis.com/compas_pb.data.VectorData"' in json_string  # Full protobuf type URL
    loaded_vector = deserialize_message_from_json(json_string)
    assert loaded_vector == vector


def test_json_exact_strings():
    """Test that JSON output exactly matches expected string format."""
    # Test with a simple point - exact string comparison
    point = Point(1, 2, 3)
    json_string = serialize_message_to_json(point)

    # Parse to get the actual GUID for comparison
    import json

    parsed = json.loads(json_string)
    guid = parsed["data"]["data"]["guid"]

    # Expected exact JSON string (with actual GUID)
    expected_json = f'''{{
  "data": {{
    "data": {{
      "@type": "type.googleapis.com/compas_pb.data.PointData",
      "guid": "{guid}",
      "name": "Point",
      "x": 1.0,
      "y": 2.0,
      "z": 3.0
    }}
  }}
}}'''

    # Normalize both strings for comparison (remove whitespace differences)
    import re

    normalized_actual = re.sub(r"\s+", "", json_string)
    normalized_expected = re.sub(r"\s+", "", expected_json)

    assert normalized_actual == normalized_expected

    # Test with simple primitive data
    primitive = 42
    json_string = serialize_message_to_json(primitive)
    parsed = json.loads(json_string)

    # Expected structure for integer primitive
    expected_structure = {"data": {"data": {"@type": "type.googleapis.com/compas_pb.data.PrimitiveData", "int": 42}}}

    assert parsed == expected_structure
