"""Unit tests for Fifth Round Adversarial Review Fixes.

Tests for 11 critical bugs found in the fourth round fixes:
- Issue #1: Naive memory protection (removed)
- Issue #2: Bypassed centralized error handling
- Issue #3: Validation sequence bug
- Issue #4: Hardcoded metric names
- Issue #5: Dictionary clobbering in logs
- Issue #6: Eager UUID generation
- Issue #7: Inconsistent OpenTelemetry units
- Issue #8: Redundant sanitization
- Issue #9: Unbounded exception names
- Issue #10: Exception swallowing
- Issue #11: Inline imports in hot paths
"""

import uuid
import pytest
from unittest.mock import Mock, patch, MagicMock
from mca_sdk import MCAClient
from mca_sdk.integrations.decorators import track_batch_prediction

pytestmark = pytest.mark.error_scenario


class TestIssue1NaiveMemoryProtectionRemoved:
    """Test that naive sys.getsizeof() check was removed (Issue #1)."""

    def test_no_size_check_for_large_nested_dict(self):
        """Verify large nested structures don't raise validation errors."""
        client = MCAClient(
            service_name="test",
            model_id="mdl-001",
            team_name="test",
            strict_validation=True,
        )

        # Create deeply nested dict (sys.getsizeof would show ~200 bytes but contains much more)
        nested = {"level": 1}
        for i in range(100):
            nested = {"data": "x" * 1000, "nested": nested}

        # Should not raise - size check removed
        try:
            client.record_prediction(input_data=nested, latency=0.1)
        except Exception as e:
            pytest.fail(f"Should not raise on large nested structures: {e}")

        client.shutdown()


class TestIssue2CentralizedErrorHandling:
    """Test that track_batch_prediction uses centralized record_error (Issue #2)."""

    def test_batch_errors_use_record_error(self):
        """Verify track_batch_prediction uses record_error() instead of direct counter access."""
        from unittest.mock import patch

        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        @track_batch_prediction(client)
        def batch_predict(inputs):
            raise RuntimeError("Batch failed")

        with patch.object(client, "record_error") as mock_record_error:
            try:
                batch_predict([1, 2, 3])
            except RuntimeError:
                pass
            mock_record_error.assert_called()

        client.shutdown()


class TestIssue3ValidationSequence:
    """Test that validation happens AFTER system attribute injection (Issue #3)."""

    def test_validation_after_system_injection(self):
        """Verify validation counts system + user attributes together."""
        client = MCAClient(
            service_name="test",
            model_id="mdl-001",
            team_name="test",
            strict_validation=True,
        )

        # Create exactly 126 user attributes (limit is 128)
        # After adding status + error_type, total = 128 (at limit)
        user_attrs = {f"attr_{i}": f"value_{i}" for i in range(126)}

        # Should succeed - total is exactly at limit
        try:
            client.record_error(ValueError("test"), **user_attrs)
        except Exception as e:
            # If validation happened before injection, this would pass then fail
            # With fix, should either pass (if 128) or fail here (if >128)
            pass

        client.shutdown()


class TestIssue4HardcodedMetricNames:
    """Test that batch_size_count uses model. prefix (Issue #4)."""

    def test_batch_size_metric_has_emms_prefix(self):
        """Verify track_batch_prediction uses model.batch_size_count (dot notation)."""
        client = MCAClient(
            service_name="test",
            model_id="mdl-001",
            team_name="test",
            strict_validation=False,  # Disable validation to check metric name only
        )

        recorded_metrics = []
        original_record_metric = client.record_metric

        def capture_record_metric(name, value, **attrs):
            recorded_metrics.append(name)
            return original_record_metric(name, value, **attrs)

        client.record_metric = capture_record_metric

        @track_batch_prediction(client)
        def batch_predict(inputs):
            return [x * 2 for x in inputs]

        batch_predict([1, 2, 3])

        # SDK uses model.* dot notation (OTel semantic conventions)
        assert (
            "model.batch_size_count" in recorded_metrics
        ), f"Should use model.batch_size_count, got: {recorded_metrics}"

        client.shutdown()


class TestIssue5DictionaryClobbering:
    """Test that system keys can't be clobbered by user attributes (Issue #5)."""

    def test_system_keys_not_clobbered_in_logs(self):
        """Verify user attributes can't overwrite system diagnostic keys."""
        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        logged_data = None

        # Capture log extra dict
        def capture_error(msg, extra=None, **kwargs):
            nonlocal logged_data
            logged_data = extra

        client._logger.error = capture_error

        # Try to clobber system keys (pass as attribute, not parameter)
        client.record_error(
            ValueError("real error"),
            error_message="fake message",  # User tries to overwrite via attribute
            error_type="FakeError",  # User tries to overwrite
        )

        # System keys should win (system keys come last in extra dict)
        assert (
            "real error" in logged_data["error_message"]
        ), "System error_message was clobbered by user attribute"
        assert (
            logged_data["error_type"] == "ValueError"
        ), "System error_type was clobbered by user attribute"

        client.shutdown()


class TestIssue6EagerUUIDGeneration:
    """Test that UUID is only generated when needed (Issue #6)."""

    def test_uuid_not_generated_when_provided(self):
        """Verify UUID only generated if prediction_id not in attributes."""
        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        uuid4_calls = 0
        original_uuid4 = uuid.uuid4

        def counting_uuid4():
            nonlocal uuid4_calls
            uuid4_calls += 1
            return original_uuid4()

        with patch("uuid.uuid4", side_effect=counting_uuid4):
            # Call with prediction_id provided
            client.record_error(ValueError("test"), prediction_id="existing-id")

        assert (
            uuid4_calls == 0
        ), f"UUID generated {uuid4_calls} times even when prediction_id provided"

        client.shutdown()


class TestIssue7UnitConsistency:
    """Test that _errors_counter doesn't have unit parameter (Issue #7)."""

    def test_errors_counter_no_unit_param(self):
        """Verify _errors_counter has no unit parameter for Prometheus consistency."""
        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        # Check that _errors_counter was created
        assert hasattr(client, "_errors_counter"), "Client should have _errors_counter"

        # Both counters should be consistent (no way to directly check unit, but ensure they exist)
        assert hasattr(client, "_predictions_counter"), "Client should have _predictions_counter"

        client.shutdown()


class TestIssue8RedundantSanitization:
    """Test that error message sanitization happens only once (Issue #8)."""

    def test_sanitization_happens_once(self):
        """Verify record_error returns sanitized message for reuse."""
        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        # record_error should now return the sanitized message
        # Note: SDK no longer masks PHI, only credentials
        sanitized = client.record_error(ValueError("test error with data"))

        # Verify it's a string (not None)
        assert isinstance(sanitized, str), "record_error should return sanitized message"
        assert "test error with data" in sanitized

        client.shutdown()


class TestIssue9UnboundedExceptionNames:
    """Test that exception class names are truncated (Issue #9)."""

    def test_exception_name_truncated(self):
        """Verify long exception names are truncated to 64 characters."""
        client = MCAClient(service_name="test", model_id="mdl-001", team_name="test")

        # Create exception with very long name
        class AVeryLongExceptionNameThatGoesOnAndOnAndOnAndShouldBeTruncatedToAvoidBlowingUpMetricLabels(
            Exception
        ):
            pass

        recorded_attrs = None
        original_add = client._predictions_counter.add

        def capture_add(value, attributes):
            nonlocal recorded_attrs
            recorded_attrs = attributes
            return original_add(value, attributes)

        client._predictions_counter.add = capture_add

        client.record_error(
            AVeryLongExceptionNameThatGoesOnAndOnAndOnAndShouldBeTruncatedToAvoidBlowingUpMetricLabels(
                "test"
            )
        )

        assert (
            len(recorded_attrs["error_type"]) <= 64
        ), f"Exception name not truncated: {len(recorded_attrs['error_type'])} chars"

        client.shutdown()


class TestIssue10ExceptionSwallowing:
    """Test that permissive mode sanitizes invalid attributes (Issue #10 - Updated in Re-Review)."""

    def test_permissive_mode_sanitizes_invalid_attributes(self):
        """Verify permissive mode sanitizes invalid attributes instead of crashing (updated in re-review)."""
        client = MCAClient(
            service_name="test",
            model_id="mdl-001",
            team_name="test",
            strict_validation=False,  # Permissive mode
        )

        recorded_attrs = None
        original_add = client._predictions_counter.add

        def capture_add(value, attributes):
            nonlocal recorded_attrs
            recorded_attrs = attributes
            return original_add(value, attributes)

        client._predictions_counter.add = capture_add

        # Provide attributes that will fail validation (too long)
        huge_value = "x" * 10000  # Exceeds max_value_length
        client.record_error(ValueError("test"), bad_attr=huge_value)

        # RE-REVIEW UPDATE: Should sanitize (truncate) not drop entirely
        assert (
            "bad_attr" in recorded_attrs
        ), "Invalid attribute should be sanitized (truncated), not dropped"
        assert (
            len(recorded_attrs["bad_attr"]) <= 1024
        ), "Invalid attribute should be truncated to max length"
        assert "status" in recorded_attrs, "System attributes should remain"
        assert "error_type" in recorded_attrs, "System attributes should remain"

        client.shutdown()


class TestIssue11InlineImports:
    """Test that imports are at module top level (Issue #11)."""

    def test_no_inline_imports_in_genai(self):
        """Verify _sanitize_error_message is imported at top of genai.py."""
        # Import the module
        from mca_sdk.integrations import genai

        # Check that _sanitize_error_message is available at module level
        assert hasattr(
            genai, "_sanitize_error_message"
        ), "_sanitize_error_message should be imported at module top"


class TestAllFixesIntegration:
    """Integration tests verifying all fixes work together."""

    def test_error_recording_complete_flow(self):
        """Test complete error recording flow with all fixes applied."""
        client = MCAClient(
            service_name="test",
            model_id="mdl-001",
            team_name="test",
            strict_validation=True,
        )

        # Test with provided prediction_id (Issue #6)
        sanitized = client.record_error(
            ValueError("Error with SSN 123-45-6789"),
            prediction_id="test-id-123",
            custom_attr="value",
        )

        # Verify sanitization happened (Issue #8)
        assert "[SSN-REDACTED]" in sanitized
        assert isinstance(sanitized, str)

        # Test batch prediction error path (Issue #2)
        @track_batch_prediction(client)
        def failing_batch(inputs):
            raise RuntimeError("Batch error")

        try:
            failing_batch([1, 2, 3])
        except RuntimeError:
            pass

        client.shutdown()
