"""
Behavioral tests for data serialization patterns.

These tests verify the logic of data serialization patterns like JSON encoding,
data transformation, schema conversion, and data migration
without making actual external calls.
"""

import base64
import json
from dataclasses import dataclass
from datetime import date, datetime, time
from decimal import Decimal
from enum import Enum
from uuid import UUID


class TestJSONSerialization:
    """Tests for JSON serialization patterns."""

    def test_serialize_basic_types(self):
        """Test serializing basic Python types."""

        def serialize(obj):
            return json.dumps(obj)

        assert serialize("hello") == '"hello"'
        assert serialize(123) == "123"
        assert serialize(3.14) == "3.14"
        assert serialize(True) == "true"
        assert serialize(None) == "null"
        assert serialize([1, 2, 3]) == "[1, 2, 3]"
        assert serialize({"a": 1}) == '{"a": 1}'

    def test_serialize_datetime(self):
        """Test serializing datetime objects."""

        def default_encoder(obj):
            if isinstance(obj, datetime):
                return obj.isoformat()
            if isinstance(obj, date):
                return obj.isoformat()
            if isinstance(obj, time):
                return obj.isoformat()
            raise TypeError(
                f"Object of type {type(obj)} is not JSON serializable"
            )

        dt = datetime(2024, 1, 15, 12, 30, 45)
        result = json.dumps({"timestamp": dt}, default=default_encoder)
        assert '"2024-01-15T12:30:45"' in result

        d = date(2024, 1, 15)
        result = json.dumps({"date": d}, default=default_encoder)
        assert '"2024-01-15"' in result

    def test_serialize_uuid(self):
        """Test serializing UUID objects."""

        def default_encoder(obj):
            if isinstance(obj, UUID):
                return str(obj)
            raise TypeError(
                f"Object of type {type(obj)} is not JSON serializable"
            )

        uid = UUID("12345678-1234-5678-1234-567812345678")
        result = json.dumps({"id": uid}, default=default_encoder)
        assert '"12345678-1234-5678-1234-567812345678"' in result

    def test_serialize_decimal(self):
        """Test serializing Decimal objects."""

        def default_encoder(obj):
            if isinstance(obj, Decimal):
                return float(obj)
            raise TypeError(
                f"Object of type {type(obj)} is not JSON serializable"
            )

        d = Decimal("10.25")
        result = json.dumps({"price": d}, default=default_encoder)
        parsed = json.loads(result)
        assert parsed["price"] == 10.25

    def test_serialize_enum(self):
        """Test serializing Enum objects."""

        class Status(Enum):
            ACTIVE = "active"
            INACTIVE = "inactive"

        def default_encoder(obj):
            if isinstance(obj, Enum):
                return obj.value
            raise TypeError(
                f"Object of type {type(obj)} is not JSON serializable"
            )

        result = json.dumps({"status": Status.ACTIVE}, default=default_encoder)
        assert '"active"' in result


class TestJSONDeserialization:
    """Tests for JSON deserialization patterns."""

    def test_deserialize_basic_types(self):
        """Test deserializing basic JSON types."""

        assert json.loads('"hello"') == "hello"
        assert json.loads("123") == 123
        assert json.loads("3.14") == 3.14
        assert json.loads("true") is True
        assert json.loads("null") is None
        assert json.loads("[1, 2, 3]") == [1, 2, 3]
        assert json.loads('{"a": 1}') == {"a": 1}

    def test_deserialize_datetime(self):
        """Test deserializing datetime strings."""

        def parse_datetime(value):
            if isinstance(value, str):
                try:
                    return datetime.fromisoformat(value)
                except ValueError:
                    pass
            return value

        def object_hook(obj):
            for key, value in obj.items():
                obj[key] = parse_datetime(value)
            return obj

        result = json.loads(
            '{"timestamp": "2024-01-15T12:30:45"}', object_hook=object_hook
        )
        assert isinstance(result["timestamp"], datetime)
        assert result["timestamp"].year == 2024

    def test_deserialize_with_type_hints(self):
        """Test deserializing with type conversion."""

        def convert_value(value, target_type):
            if target_type is int:
                return int(value)
            if target_type is float:
                return float(value)
            if target_type is bool:
                if isinstance(value, str):
                    return value.lower() in ("true", "1", "yes")
                return bool(value)
            if target_type is datetime:
                return datetime.fromisoformat(value)
            return value

        assert convert_value("123", int) == 123
        assert convert_value("3.14", float) == 3.14
        assert convert_value("true", bool) is True
        assert convert_value("2024-01-01", datetime) == datetime(2024, 1, 1)

    def test_handle_missing_fields(self):
        """Test handling missing fields with defaults."""

        def deserialize_with_defaults(data, defaults):
            result = defaults.copy()
            result.update(data)
            return result

        data = {"name": "John"}
        defaults = {"name": "", "age": 0, "active": True}
        result = deserialize_with_defaults(data, defaults)
        assert result["name"] == "John"
        assert result["age"] == 0
        assert result["active"] is True

    def test_handle_extra_fields(self):
        """Test handling extra fields."""

        def deserialize_strict(data, allowed_fields):
            return {k: v for k, v in data.items() if k in allowed_fields}

        data = {"name": "John", "age": 30, "extra": "ignored"}
        allowed = {"name", "age"}
        result = deserialize_strict(data, allowed)
        assert "extra" not in result
        assert result == {"name": "John", "age": 30}


class TestBinarySerialization:
    """Tests for binary serialization patterns."""

    def test_base64_encode(self):
        """Test base64 encoding."""
        data = b"Hello, World!"
        encoded = base64.b64encode(data).decode()
        assert encoded == "SGVsbG8sIFdvcmxkIQ=="

    def test_base64_decode(self):
        """Test base64 decoding."""
        encoded = "SGVsbG8sIFdvcmxkIQ=="
        decoded = base64.b64decode(encoded)
        assert decoded == b"Hello, World!"

    def test_base64_url_safe(self):
        """Test URL-safe base64 encoding."""
        data = b"\xff\xfe\xfd"  # Binary data with special chars
        encoded = base64.urlsafe_b64encode(data).decode()
        # URL-safe should not contain + or /
        assert "+" not in encoded
        assert "/" not in encoded

    def test_hex_encode(self):
        """Test hex encoding."""
        data = b"\x00\x01\x02\xff"
        encoded = data.hex()
        assert encoded == "000102ff"

    def test_hex_decode(self):
        """Test hex decoding."""
        encoded = "000102ff"
        decoded = bytes.fromhex(encoded)
        assert decoded == b"\x00\x01\x02\xff"


class TestDataTransformation:
    """Tests for data transformation patterns."""

    def test_snake_to_camel_case(self):
        """Test converting snake_case to camelCase."""

        def to_camel_case(snake_str):
            components = snake_str.split("_")
            return components[0] + "".join(x.title() for x in components[1:])

        assert to_camel_case("user_name") == "userName"
        assert to_camel_case("first_name_last") == "firstNameLast"
        assert to_camel_case("simple") == "simple"

    def test_camel_to_snake_case(self):
        """Test converting camelCase to snake_case."""

        def to_snake_case(camel_str):
            result = []
            for char in camel_str:
                if char.isupper():
                    result.append("_")
                    result.append(char.lower())
                else:
                    result.append(char)
            return "".join(result).lstrip("_")

        assert to_snake_case("userName") == "user_name"
        assert to_snake_case("firstName") == "first_name"
        assert to_snake_case("simple") == "simple"

    def test_convert_dict_keys(self):
        """Test converting all dict keys."""

        def convert_keys(obj, converter):
            if isinstance(obj, dict):
                return {
                    converter(k): convert_keys(v, converter)
                    for k, v in obj.items()
                }
            if isinstance(obj, list):
                return [convert_keys(item, converter) for item in obj]
            return obj

        def to_upper(key):
            return key.upper()

        data = {"name": "John", "address": {"city": "NYC"}}
        result = convert_keys(data, to_upper)
        assert result == {"NAME": "John", "ADDRESS": {"CITY": "NYC"}}

    def test_flatten_dict(self):
        """Test flattening nested dict."""

        def flatten_dict(d, parent_key="", sep="."):
            items = []
            for k, v in d.items():
                new_key = f"{parent_key}{sep}{k}" if parent_key else k
                if isinstance(v, dict):
                    items.extend(flatten_dict(v, new_key, sep).items())
                else:
                    items.append((new_key, v))
            return dict(items)

        data = {"user": {"name": "John", "address": {"city": "NYC"}}}
        result = flatten_dict(data)
        assert result == {"user.name": "John", "user.address.city": "NYC"}

    def test_unflatten_dict(self):
        """Test unflattening a flat dict."""

        def unflatten_dict(d, sep="."):
            result = {}
            for key, value in d.items():
                parts = key.split(sep)
                current = result
                for part in parts[:-1]:
                    if part not in current:
                        current[part] = {}
                    current = current[part]
                current[parts[-1]] = value
            return result

        data = {"user.name": "John", "user.address.city": "NYC"}
        result = unflatten_dict(data)
        assert result == {"user": {"name": "John", "address": {"city": "NYC"}}}


class TestSchemaConversion:
    """Tests for schema conversion patterns."""

    def test_dict_to_dataclass(self):
        """Test converting dict to dataclass."""

        @dataclass
        class User:
            name: str
            age: int
            active: bool = True

        def dict_to_dataclass(cls, data):
            field_names = {f.name for f in cls.__dataclass_fields__.values()}
            filtered = {k: v for k, v in data.items() if k in field_names}
            return cls(**filtered)

        data = {"name": "John", "age": 30, "extra": "ignored"}
        user = dict_to_dataclass(User, data)
        assert user.name == "John"
        assert user.age == 30
        assert user.active is True

    def test_dataclass_to_dict(self):
        """Test converting dataclass to dict."""
        from dataclasses import asdict

        @dataclass
        class User:
            name: str
            age: int
            active: bool = True

        user = User(name="John", age=30)
        result = asdict(user)
        assert result == {"name": "John", "age": 30, "active": True}

    def test_api_response_to_model(self):
        """Test converting API response to internal model."""

        def convert_api_user(api_data):
            return {
                "id": api_data.get("userId"),
                "name": api_data.get("fullName"),
                "email": api_data.get("emailAddress"),
                "created_at": api_data.get("createdDate"),
            }

        api_response = {
            "userId": "123",
            "fullName": "John Doe",
            "emailAddress": "john@example.com",
            "createdDate": "2024-01-01",
        }
        model = convert_api_user(api_response)
        assert model["id"] == "123"
        assert model["name"] == "John Doe"

    def test_model_to_api_request(self):
        """Test converting internal model to API request."""

        def convert_to_api(model):
            return {
                "userId": model.get("id"),
                "fullName": model.get("name"),
                "emailAddress": model.get("email"),
            }

        model = {"id": "123", "name": "John Doe", "email": "john@example.com"}
        api_request = convert_to_api(model)
        assert api_request["userId"] == "123"
        assert api_request["fullName"] == "John Doe"

    def test_version_migration(self):
        """Test migrating data between schema versions."""

        def migrate_v1_to_v2(v1_data):
            # V1 had fullName, V2 has firstName and lastName
            full_name = v1_data.get("fullName", "")
            parts = full_name.split(" ", 1)
            return {
                "firstName": parts[0] if parts else "",
                "lastName": parts[1] if len(parts) > 1 else "",
                "email": v1_data.get("email"),
                "schemaVersion": 2,
            }

        v1 = {
            "fullName": "John Doe",
            "email": "john@example.com",
            "schemaVersion": 1,
        }
        v2 = migrate_v1_to_v2(v1)
        assert v2["firstName"] == "John"
        assert v2["lastName"] == "Doe"
        assert v2["schemaVersion"] == 2


class TestCollectionSerialization:
    """Tests for collection serialization patterns."""

    def test_serialize_list(self):
        """Test serializing lists."""

        def serialize_list(items, serializer):
            return [serializer(item) for item in items]

        def item_serializer(item):
            return {"id": item["id"], "name": item["name"].upper()}

        items = [{"id": 1, "name": "apple"}, {"id": 2, "name": "banana"}]
        result = serialize_list(items, item_serializer)
        assert result[0]["name"] == "APPLE"
        assert result[1]["name"] == "BANANA"

    def test_serialize_dict_values(self):
        """Test serializing dict values."""

        def serialize_values(data, serializer):
            return {k: serializer(v) for k, v in data.items()}

        data = {"a": 1, "b": 2, "c": 3}
        result = serialize_values(data, lambda x: x * 2)
        assert result == {"a": 2, "b": 4, "c": 6}

    def test_paginated_serialization(self):
        """Test serializing paginated results."""

        def serialize_paginated(items, page, page_size, total, serializer):
            return {
                "data": [serializer(item) for item in items],
                "pagination": {
                    "page": page,
                    "page_size": page_size,
                    "total": total,
                    "total_pages": (total + page_size - 1) // page_size,
                },
            }

        items = [{"id": 1}, {"id": 2}]
        result = serialize_paginated(
            items, page=1, page_size=10, total=25, serializer=lambda x: x
        )
        assert result["pagination"]["total_pages"] == 3
        assert len(result["data"]) == 2

    def test_serialize_with_includes(self):
        """Test serializing with related data."""

        def serialize_with_includes(item, includes):
            result = {"id": item["id"], "name": item["name"]}
            if "address" in includes and "address" in item:
                result["address"] = item["address"]
            if "orders" in includes and "orders" in item:
                result["orders"] = item["orders"]
            return result

        item = {
            "id": 1,
            "name": "John",
            "address": {"city": "NYC"},
            "orders": [{"id": 100}],
        }
        result = serialize_with_includes(item, includes=["address"])
        assert "address" in result
        assert "orders" not in result

    def test_sparse_serialization(self):
        """Test sparse fieldsets serialization."""

        def serialize_sparse(item, fields):
            if not fields:
                return item
            return {k: v for k, v in item.items() if k in fields}

        item = {"id": 1, "name": "John", "email": "john@x.com", "age": 30}
        result = serialize_sparse(item, fields=["id", "name"])
        assert result == {"id": 1, "name": "John"}


class TestCustomEncoders:
    """Tests for custom encoder patterns."""

    def test_custom_json_encoder_class(self):
        """Test custom JSON encoder class."""

        class CustomEncoder(json.JSONEncoder):
            def default(self, obj):
                if isinstance(obj, datetime):
                    return {"__datetime__": True, "value": obj.isoformat()}
                if isinstance(obj, Decimal):
                    return {"__decimal__": True, "value": str(obj)}
                return super().default(obj)

        data = {"dt": datetime(2024, 1, 1), "price": Decimal("10.50")}
        result = json.dumps(data, cls=CustomEncoder)
        parsed = json.loads(result)
        assert parsed["dt"]["__datetime__"] is True
        assert parsed["price"]["__decimal__"] is True

    def test_custom_decoder_hook(self):
        """Test custom JSON decoder hook."""

        def custom_decoder(obj):
            if "__datetime__" in obj:
                return datetime.fromisoformat(obj["value"])
            if "__decimal__" in obj:
                return Decimal(obj["value"])
            return obj

        data = '{"dt": {"__datetime__": true, "value": "2024-01-01T00:00:00"}}'
        result = json.loads(data, object_hook=custom_decoder)
        assert isinstance(result["dt"], datetime)

    def test_serialize_with_context(self):
        """Test serialization with context."""

        def serialize_user(user, context):
            result = {"id": user["id"], "name": user["name"]}
            if context.get("include_email"):
                result["email"] = user["email"]
            if context.get("include_private"):
                result["ssn"] = user.get("ssn")
            return result

        user = {
            "id": 1,
            "name": "John",
            "email": "j@x.com",
            "ssn": "123-45-6789",
        }

        public = serialize_user(user, {"include_email": True})
        assert "email" in public
        assert "ssn" not in public

        private = serialize_user(
            user, {"include_email": True, "include_private": True}
        )
        assert "ssn" in private

    def test_versioned_serialization(self):
        """Test versioned serialization."""

        def serialize_v1(user):
            return {
                "version": 1,
                "fullName": user["name"],
                "emailAddress": user["email"],
            }

        def serialize_v2(user):
            names = user["name"].split(" ", 1)
            return {
                "version": 2,
                "firstName": names[0],
                "lastName": names[1] if len(names) > 1 else "",
                "email": user["email"],
            }

        def serialize(user, version=2):
            if version == 1:
                return serialize_v1(user)
            return serialize_v2(user)

        user = {"name": "John Doe", "email": "john@example.com"}
        v1 = serialize(user, version=1)
        v2 = serialize(user, version=2)
        assert v1["fullName"] == "John Doe"
        assert v2["firstName"] == "John"

    def test_null_handling(self):
        """Test null value handling strategies."""

        def serialize_omit_nulls(data):
            return {k: v for k, v in data.items() if v is not None}

        def serialize_explicit_nulls(data, nullable_fields):
            result = {}
            for key, value in data.items():
                if value is not None:
                    result[key] = value
                elif key in nullable_fields:
                    result[key] = None
            return result

        data = {"name": "John", "age": None, "email": None}

        omitted = serialize_omit_nulls(data)
        assert "age" not in omitted

        explicit = serialize_explicit_nulls(data, nullable_fields={"age"})
        assert "age" in explicit
        assert explicit["age"] is None
        assert "email" not in explicit


class TestDataValidation:
    """Tests for data validation during serialization."""

    def test_validate_required_fields(self):
        """Test validating required fields."""

        def validate_required(data, required):
            missing = [f for f in required if f not in data or data[f] is None]
            if missing:
                return {
                    "valid": False,
                    "errors": [f"Missing: {f}" for f in missing],
                }
            return {"valid": True, "errors": []}

        result = validate_required({"name": "John"}, ["name", "email"])
        assert result["valid"] is False
        assert "Missing: email" in result["errors"]

    def test_validate_types(self):
        """Test validating field types."""

        def validate_types(data, type_spec):
            errors = []
            for field, expected_type in type_spec.items():
                if field in data and data[field] is not None:
                    if not isinstance(data[field], expected_type):
                        errors.append(
                            f"{field}: expected {expected_type.__name__}"
                        )
            return {"valid": len(errors) == 0, "errors": errors}

        result = validate_types(
            {"name": "John", "age": "thirty"}, {"name": str, "age": int}
        )
        assert result["valid"] is False
        assert "age: expected int" in result["errors"]

    def test_coerce_types(self):
        """Test coercing types during deserialization."""

        def coerce_types(data, type_spec):
            result = {}
            for field, value in data.items():
                if field in type_spec and value is not None:
                    try:
                        result[field] = type_spec[field](value)
                    except (ValueError, TypeError):
                        result[field] = value
                else:
                    result[field] = value
            return result

        data = {"name": "John", "age": "30", "active": "true"}
        type_spec = {"age": int, "active": lambda x: x.lower() == "true"}
        result = coerce_types(data, type_spec)
        assert result["age"] == 30
        assert result["active"] is True

    def test_validate_format(self):
        """Test validating field formats."""
        import re

        def validate_format(value, pattern):
            if re.match(pattern, value):
                return True
            return False

        email_pattern = r"^[\w.-]+@[\w.-]+\.\w+$"
        assert validate_format("john@example.com", email_pattern) is True
        assert validate_format("invalid-email", email_pattern) is False

    def test_sanitize_during_serialization(self):
        """Test sanitizing data during serialization."""

        def sanitize_string(value, max_length=100):
            if not isinstance(value, str):
                return value
            value = value.strip()
            if len(value) > max_length:
                value = value[:max_length]
            return value

        def serialize_with_sanitization(data, string_fields):
            result = {}
            for key, value in data.items():
                if key in string_fields:
                    result[key] = sanitize_string(value)
                else:
                    result[key] = value
            return result

        data = {"name": "  John  ", "bio": "x" * 200}
        result = serialize_with_sanitization(
            data, string_fields=["name", "bio"]
        )
        assert result["name"] == "John"
        assert len(result["bio"]) == 100


class TestStreamingSerialization:
    """Tests for streaming serialization patterns."""

    def test_chunk_iterator(self):
        """Test iterating over chunks."""

        def chunk_iterator(items, chunk_size):
            for i in range(0, len(items), chunk_size):
                yield items[i : i + chunk_size]

        items = list(range(10))
        chunks = list(chunk_iterator(items, 3))
        assert len(chunks) == 4
        assert chunks[0] == [0, 1, 2]
        assert chunks[-1] == [9]

    def test_serialize_large_list(self):
        """Test serializing large list in chunks."""

        def serialize_in_chunks(items, chunk_size, serializer):
            for i in range(0, len(items), chunk_size):
                chunk = items[i : i + chunk_size]
                yield [serializer(item) for item in chunk]

        items = [{"id": i} for i in range(100)]
        chunks = list(serialize_in_chunks(items, 30, lambda x: x["id"]))
        assert len(chunks) == 4
        assert chunks[0] == list(range(30))

    def test_json_lines_format(self):
        """Test JSON Lines format."""

        def to_json_lines(items):
            return "\n".join(json.dumps(item) for item in items)

        def from_json_lines(text):
            return [
                json.loads(line) for line in text.strip().split("\n") if line
            ]

        items = [{"id": 1}, {"id": 2}, {"id": 3}]
        jsonl = to_json_lines(items)
        parsed = from_json_lines(jsonl)
        assert parsed == items

    def test_incremental_builder(self):
        """Test incremental object builder."""

        class ObjectBuilder:
            def __init__(self):
                self.data = {}

            def set(self, key, value):
                self.data[key] = value
                return self

            def build(self):
                return self.data.copy()

        builder = ObjectBuilder()
        result = builder.set("name", "John").set("age", 30).build()
        assert result == {"name": "John", "age": 30}

    def test_lazy_serialization(self):
        """Test lazy serialization pattern."""

        class LazySerializer:
            def __init__(self, data, serializer):
                self._data = data
                self._serializer = serializer
                self._cached = None

            def serialize(self):
                if self._cached is None:
                    self._cached = self._serializer(self._data)
                return self._cached

        data = {"name": "John"}
        serializer = LazySerializer(data, json.dumps)
        # First call computes
        result1 = serializer.serialize()
        # Second call uses cache
        result2 = serializer.serialize()
        assert result1 == result2 == '{"name": "John"}'


class TestErrorHandling:
    """Tests for serialization error handling."""

    def test_handle_circular_reference(self):
        """Test handling circular references."""

        def safe_serialize(obj, seen=None):
            if seen is None:
                seen = set()
            obj_id = id(obj)
            if obj_id in seen:
                return "[Circular]"
            if isinstance(obj, dict):
                seen.add(obj_id)
                return {k: safe_serialize(v, seen) for k, v in obj.items()}
            if isinstance(obj, list):
                seen.add(obj_id)
                return [safe_serialize(item, seen) for item in obj]
            return obj

        # Create circular reference
        data = {"name": "John"}
        data["self"] = data
        result = safe_serialize(data)
        assert result["self"] == "[Circular]"

    def test_handle_encoding_error(self):
        """Test handling encoding errors."""

        def safe_encode(obj, default_value="[Unserializable]"):
            try:
                return json.dumps(obj)
            except (TypeError, ValueError):
                return default_value

        assert safe_encode({"name": "John"}) == '{"name": "John"}'
        assert safe_encode({"fn": lambda x: x}) == "[Unserializable]"

    def test_partial_serialization(self):
        """Test partial serialization on error."""

        def partial_serialize(data, serializers):
            result = {}
            errors = []
            for key, value in data.items():
                try:
                    if key in serializers:
                        result[key] = serializers[key](value)
                    else:
                        result[key] = value
                except Exception as e:
                    errors.append({"field": key, "error": str(e)})
                    result[key] = None
            return {"data": result, "errors": errors}

        data = {"name": "John", "age": "not-a-number"}
        serializers = {"age": int}
        result = partial_serialize(data, serializers)
        assert result["data"]["name"] == "John"
        assert result["data"]["age"] is None
        assert len(result["errors"]) == 1

    def test_validation_error_collection(self):
        """Test collecting multiple validation errors."""

        def validate_all(data, validators):
            errors = []
            for field, validator in validators.items():
                if field in data:
                    error = validator(data[field])
                    if error:
                        errors.append({"field": field, "error": error})
            return errors

        def validate_age(age):
            if not isinstance(age, int):
                return "must be an integer"
            if age < 0:
                return "must be positive"
            return None

        def validate_name(name):
            if not name:
                return "cannot be empty"
            return None

        data = {"name": "", "age": -5}
        validators = {"name": validate_name, "age": validate_age}
        errors = validate_all(data, validators)
        assert len(errors) == 2
