"""
Contract Compliance Tests for ThrillWiki API

These tests verify that API responses match frontend TypeScript interfaces exactly,
preventing runtime errors and ensuring type safety.
"""

from django.test import Client, TestCase
from rest_framework.test import APITestCase

from apps.api.v1.serializers.shared import (
    ensure_filter_option_format,
    ensure_range_format,
    validate_filter_metadata_contract,
)
from apps.parks.services.hybrid_loader import smart_park_loader
from apps.rides.services.hybrid_loader import SmartRideLoader


class FilterMetadataContractTests(TestCase):
    """Test that filter metadata follows the expected contract."""

    def setUp(self):
        self.client = Client()

    def test_parks_filter_metadata_structure(self):
        """Test that parks filter metadata has correct structure."""
        # Get filter metadata from the service
        metadata = smart_park_loader.get_filter_metadata()

        # Should have required top-level keys
        self.assertIn("categorical", metadata)
        self.assertIn("ranges", metadata)
        self.assertIn("total_count", metadata)

        # Categorical filters should be objects with value/label/count
        categorical = metadata["categorical"]
        self.assertIsInstance(categorical, dict)

        for filter_name, filter_options in categorical.items():
            with self.subTest(filter_name=filter_name):
                self.assertIsInstance(filter_options, list, f"Filter '{filter_name}' should be a list")

                for i, option in enumerate(filter_options):
                    with self.subTest(filter_name=filter_name, option_index=i):
                        self.assertIsInstance(
                            option,
                            dict,
                            f"Filter '{filter_name}' option {i} should be an object, not {type(option).__name__}",
                        )

                        # Check required properties
                        self.assertIn("value", option, f"Filter '{filter_name}' option {i} missing 'value' property")
                        self.assertIn("label", option, f"Filter '{filter_name}' option {i} missing 'label' property")

                        # Check types
                        self.assertIsInstance(
                            option["value"], str, f"Filter '{filter_name}' option {i} 'value' should be string"
                        )
                        self.assertIsInstance(
                            option["label"], str, f"Filter '{filter_name}' option {i} 'label' should be string"
                        )

                        # Count is optional but should be int if present
                        if "count" in option and option["count"] is not None:
                            self.assertIsInstance(
                                option["count"], int, f"Filter '{filter_name}' option {i} 'count' should be int"
                            )

    def test_rides_filter_metadata_structure(self):
        """Test that rides filter metadata has correct structure."""
        loader = SmartRideLoader()
        metadata = loader.get_filter_metadata()

        # Should have required top-level keys
        self.assertIn("categorical", metadata)
        self.assertIn("ranges", metadata)
        self.assertIn("total_count", metadata)

        # Categorical filters should be objects with value/label/count
        categorical = metadata["categorical"]
        self.assertIsInstance(categorical, dict)

        # Test specific categorical filters that were problematic
        critical_filters = ["categories", "statuses", "roller_coaster_types", "track_materials"]

        for filter_name in critical_filters:
            if filter_name in categorical:
                with self.subTest(filter_name=filter_name):
                    filter_options = categorical[filter_name]
                    self.assertIsInstance(filter_options, list)

                    for i, option in enumerate(filter_options):
                        with self.subTest(filter_name=filter_name, option_index=i):
                            self.assertIsInstance(
                                option,
                                dict,
                                f"CRITICAL: Filter '{filter_name}' option {i} is {type(option).__name__} but should be dict",
                            )

                            self.assertIn("value", option)
                            self.assertIn("label", option)
                            self.assertIn("count", option)

    def test_range_metadata_structure(self):
        """Test that range metadata has correct structure."""
        # Test parks ranges
        parks_metadata = smart_park_loader.get_filter_metadata()
        ranges = parks_metadata["ranges"]

        for range_name, range_data in ranges.items():
            with self.subTest(range_name=range_name):
                self.assertIsInstance(range_data, dict, f"Range '{range_name}' should be an object")

                # Check required properties
                self.assertIn("min", range_data)
                self.assertIn("max", range_data)
                self.assertIn("step", range_data)
                self.assertIn("unit", range_data)

                # Check types (min/max can be None)
                if range_data["min"] is not None:
                    self.assertIsInstance(range_data["min"], (int, float))
                if range_data["max"] is not None:
                    self.assertIsInstance(range_data["max"], (int, float))

                self.assertIsInstance(range_data["step"], (int, float))
                # Unit can be None or string
                if range_data["unit"] is not None:
                    self.assertIsInstance(range_data["unit"], str)


class ContractValidationUtilityTests(TestCase):
    """Test contract validation utility functions."""

    def test_validate_filter_metadata_contract_valid(self):
        """Test validation passes for valid filter metadata."""
        valid_metadata = {
            "categorical": {
                "statuses": [
                    {"value": "OPERATING", "label": "Operating", "count": 5},
                    {"value": "CLOSED_TEMP", "label": "Temporarily Closed", "count": 2},
                ]
            },
            "ranges": {"rating": {"min": 1.0, "max": 10.0, "step": 0.1, "unit": "stars"}},
            "total_count": 100,
        }

        # Should not raise an exception
        validated = validate_filter_metadata_contract(valid_metadata)
        self.assertIsInstance(validated, dict)
        self.assertEqual(validated["total_count"], 100)

    def test_validate_filter_metadata_contract_invalid(self):
        """Test validation fails for invalid filter metadata."""
        from rest_framework import serializers

        invalid_metadata = {
            "categorical": {"statuses": ["OPERATING", "CLOSED_TEMP"]},  # Should be objects, not strings
            "ranges": {},
            "total_count": 100,
        }

        # Should raise ValidationError
        with self.assertRaises(serializers.ValidationError):
            validate_filter_metadata_contract(invalid_metadata)

    def test_ensure_filter_option_format_strings(self):
        """Test converting string arrays to proper format."""
        string_options = ["OPERATING", "CLOSED_TEMP", "UNDER_CONSTRUCTION"]

        formatted = ensure_filter_option_format(string_options)

        self.assertEqual(len(formatted), 3)
        for i, option in enumerate(formatted):
            self.assertIsInstance(option, dict)
            self.assertIn("value", option)
            self.assertIn("label", option)
            self.assertIn("count", option)
            self.assertIn("selected", option)

            self.assertEqual(option["value"], string_options[i])
            self.assertEqual(option["label"], string_options[i])
            self.assertIsNone(option["count"])
            self.assertFalse(option["selected"])

    def test_ensure_filter_option_format_tuples(self):
        """Test converting tuple arrays to proper format."""
        tuple_options = [("OPERATING", "Operating", 5), ("CLOSED_TEMP", "Temporarily Closed", 2)]

        formatted = ensure_filter_option_format(tuple_options)

        self.assertEqual(len(formatted), 2)
        self.assertEqual(formatted[0]["value"], "OPERATING")
        self.assertEqual(formatted[0]["label"], "Operating")
        self.assertEqual(formatted[0]["count"], 5)

        self.assertEqual(formatted[1]["value"], "CLOSED_TEMP")
        self.assertEqual(formatted[1]["label"], "Temporarily Closed")
        self.assertEqual(formatted[1]["count"], 2)

    def test_ensure_filter_option_format_dicts(self):
        """Test that properly formatted dicts pass through correctly."""
        dict_options = [
            {"value": "OPERATING", "label": "Operating", "count": 5},
            {"value": "CLOSED_TEMP", "label": "Temporarily Closed", "count": 2},
        ]

        formatted = ensure_filter_option_format(dict_options)

        self.assertEqual(len(formatted), 2)
        self.assertEqual(formatted[0]["value"], "OPERATING")
        self.assertEqual(formatted[0]["label"], "Operating")
        self.assertEqual(formatted[0]["count"], 5)

    def test_ensure_range_format(self):
        """Test range format utility."""
        range_data = {"min": 1.0, "max": 10.0, "step": 0.5, "unit": "stars"}

        formatted = ensure_range_format(range_data)

        self.assertEqual(formatted["min"], 1.0)
        self.assertEqual(formatted["max"], 10.0)
        self.assertEqual(formatted["step"], 0.5)
        self.assertEqual(formatted["unit"], "stars")

    def test_ensure_range_format_missing_step(self):
        """Test range format with missing step defaults to 1.0."""
        range_data = {"min": 1, "max": 10}

        formatted = ensure_range_format(range_data)

        self.assertEqual(formatted["step"], 1.0)
        self.assertIsNone(formatted["unit"])


class APIEndpointContractTests(APITestCase):
    """Test actual API endpoints for contract compliance."""

    def test_parks_hybrid_endpoint_contract(self):
        """Test parks hybrid endpoint returns proper contract."""
        # This would require actual data in the database
        # For now, we'll test the structure
        pass

    def test_rides_hybrid_endpoint_contract(self):
        """Test rides hybrid endpoint returns proper contract."""
        # This would require actual data in the database
        # For now, we'll test the structure
        pass


class TypeScriptInterfaceComplianceTests(TestCase):
    """Test that responses match TypeScript interfaces exactly."""

    def test_filter_option_interface_compliance(self):
        """Test FilterOption interface compliance."""
        # TypeScript interface:
        # interface FilterOption {
        #     value: string;
        #     label: string;
        #     count?: number;
        #     selected?: boolean;
        # }

        option = {"value": "OPERATING", "label": "Operating", "count": 5, "selected": False}

        # All required fields present
        self.assertIn("value", option)
        self.assertIn("label", option)

        # Correct types
        self.assertIsInstance(option["value"], str)
        self.assertIsInstance(option["label"], str)

        # Optional fields have correct types if present
        if "count" in option and option["count"] is not None:
            self.assertIsInstance(option["count"], int)
        if "selected" in option:
            self.assertIsInstance(option["selected"], bool)

    def test_filter_range_interface_compliance(self):
        """Test FilterRange interface compliance."""
        # TypeScript interface:
        # interface FilterRange {
        #     min: number;
        #     max: number;
        #     step: number;
        #     unit?: string;
        # }

        range_data = {"min": 1.0, "max": 10.0, "step": 0.1, "unit": "stars"}

        # All required fields present
        self.assertIn("min", range_data)
        self.assertIn("max", range_data)
        self.assertIn("step", range_data)

        # Correct types (min/max can be null)
        if range_data["min"] is not None:
            self.assertIsInstance(range_data["min"], (int, float))
        if range_data["max"] is not None:
            self.assertIsInstance(range_data["max"], (int, float))

        self.assertIsInstance(range_data["step"], (int, float))

        # Optional unit field
        if "unit" in range_data and range_data["unit"] is not None:
            self.assertIsInstance(range_data["unit"], str)


class RegressionTests(TestCase):
    """Regression tests for specific contract violations that were fixed."""

    def test_categorical_filters_not_strings(self):
        """Regression test: Ensure categorical filters are never returned as strings."""
        # This was the main issue - categorical filters were returned as:
        # ['OPERATING', 'CLOSED_TEMP'] instead of
        # [{'value': 'OPERATING', 'label': 'Operating', 'count': 5}, ...]

        # Test parks
        parks_metadata = smart_park_loader.get_filter_metadata()
        categorical = parks_metadata.get("categorical", {})

        for filter_name, filter_options in categorical.items():
            with self.subTest(filter_name=filter_name):
                self.assertIsInstance(filter_options, list)

                for i, option in enumerate(filter_options):
                    with self.subTest(filter_name=filter_name, option_index=i):
                        self.assertIsInstance(
                            option,
                            dict,
                            f"REGRESSION: Filter '{filter_name}' option {i} is a {type(option).__name__} "
                            f"but should be a dict. This causes frontend crashes!",
                        )

                        # Must not be a string
                        self.assertNotIsInstance(
                            option,
                            str,
                            f"CRITICAL REGRESSION: Filter '{filter_name}' option {i} is a string '{option}' "
                            f"but frontend expects object with value/label/count properties!",
                        )

        # Test rides
        rides_loader = SmartRideLoader()
        rides_metadata = rides_loader.get_filter_metadata()
        categorical = rides_metadata.get("categorical", {})

        for filter_name, filter_options in categorical.items():
            with self.subTest(filter_name=f"rides_{filter_name}"):
                self.assertIsInstance(filter_options, list)

                for i, option in enumerate(filter_options):
                    with self.subTest(filter_name=f"rides_{filter_name}", option_index=i):
                        self.assertIsInstance(
                            option,
                            dict,
                            f"REGRESSION: Rides filter '{filter_name}' option {i} is a {type(option).__name__} "
                            f"but should be a dict. This causes frontend crashes!",
                        )

    def test_ranges_have_step_and_unit(self):
        """Regression test: Ensure ranges have step and unit properties."""
        # Frontend expects: { min: number, max: number, step: number, unit?: string }
        # Backend was sometimes missing step and unit

        parks_metadata = smart_park_loader.get_filter_metadata()
        ranges = parks_metadata.get("ranges", {})

        for range_name, range_data in ranges.items():
            with self.subTest(range_name=range_name):
                self.assertIn("step", range_data, f"Range '{range_name}' missing 'step' property required by frontend")
                self.assertIn("unit", range_data, f"Range '{range_name}' missing 'unit' property required by frontend")

                # Step should be a number
                self.assertIsInstance(range_data["step"], (int, float), f"Range '{range_name}' step should be a number")

    def test_no_undefined_values(self):
        """Regression test: Ensure no undefined values (should be null)."""
        # JavaScript undefined !== null, and TypeScript interfaces expect null

        parks_metadata = smart_park_loader.get_filter_metadata()

        def check_no_undefined(obj, path=""):
            if isinstance(obj, dict):
                for key, value in obj.items():
                    current_path = f"{path}.{key}" if path else key
                    # Python None is fine (becomes null in JSON)
                    # But we shouldn't have any undefined-like values
                    check_no_undefined(value, current_path)
            elif isinstance(obj, list):
                for i, item in enumerate(obj):
                    current_path = f"{path}[{i}]"
                    check_no_undefined(item, current_path)

        # This will recursively check the entire metadata structure
        check_no_undefined(parks_metadata)
