import pytest
from unittest.mock import patch


@pytest.fixture(autouse=True)
def mock_registry_client():
    with patch("mca_sdk.registry.client.RegistryClient._fetch_model_config_internal") as mock_fetch:
        # Prevent TypeError: '>=' not supported between instances of 'MagicMock' and 'int'
        from mca_sdk.registry.models import ModelConfig

        mock_fetch.return_value = ModelConfig(
            model_id="test",
            model_version="1.0",
            service_name="test-service",
            team_name="test-team",
            model_category="internal",
            model_type="regression",
        )
        yield mock_fetch


"""Thread safety tests for registry client background refresh.

Tests concurrent access scenarios to ensure thread-safe cache operations.
"""

import threading
import time

import pytest

from mca_sdk.registry.client import RegistryClient
from mca_sdk.registry.models import ModelConfig


@pytest.fixture
def mock_model_config():
    """Create a mock ModelConfig for testing."""
    return ModelConfig(
        service_name="test-service",
        model_id="mdl-001",
        model_version="1.0.0",
        team_name="test-team",
        model_category="internal",
        model_type="classification",
        thresholds={"accuracy": 0.95},
        extra_resource={},
    )


@pytest.mark.allow_refresh_thread
class TestThreadSafety:
    """Test thread-safe cache operations with concurrent access."""

    def test_main_thread_reads_while_refresh_updates(self, mock_model_config):
        """Test main thread reading config while refresh thread updates (no race condition)."""
        with patch("mca_sdk.registry.client.MIN_REFRESH_INTERVAL_SECS", 1):
            with patch("mca_sdk.registry.client.requests.Session"):
                client = RegistryClient(
                    url="https://registry.example.com",
                    refresh_interval_secs=1,
                )

                # Add model to tracking
                client._fetched_models["mdl-001"] = "1.0.0"

                # Track concurrent operations
                operations_completed = {"main_reads": 0, "refresh_updates": 0}
                errors = []

                # Mock fetch to simulate config updates
                call_count = {"value": 0}

                def mock_fetch(*args, **kwargs):
                    call_count["value"] += 1
                    operations_completed["refresh_updates"] += 1
                    # Return different threshold values on each call
                    return ModelConfig(
                        service_name="test-service",
                        model_id="mdl-001",
                        model_version="1.0.0",
                        team_name="test-team",
                        model_category="internal",
                        model_type="classification",
                        thresholds={"accuracy": 0.90 + (call_count["value"] * 0.01)},
                        extra_resource={},
                    )

                # Main thread reader function
                def reader_thread():
                    """Read from cache repeatedly."""
                    try:
                        for _ in range(20):
                            # Try to read from cache
                            cached = client._get_cached("model:mdl-001:1.0.0")
                            if cached:
                                operations_completed["main_reads"] += 1
                            time.sleep(0.05)
                    except Exception as e:
                        errors.append(e)

                with patch.object(client, "fetch_model_config", side_effect=mock_fetch):
                    # Pre-populate cache
                    client._set_cache(
                        "model:mdl-001:1.0.0",
                        mock_model_config,
                    )

                    # Start reader thread
                    reader = threading.Thread(target=reader_thread)
                    reader.start()

                    # Wait for some refreshes and reads
                    time.sleep(2.5)

                    # Stop threads
                    client.stop_refresh_thread()
                    reader.join()

                # Verify no errors occurred
                assert len(errors) == 0, f"Thread safety errors: {errors}"

                # Verify both threads operated
                assert operations_completed["main_reads"] > 0
                assert operations_completed["refresh_updates"] >= 2

    def test_concurrent_fetch_model_config_calls(self, mock_model_config):
        """Test concurrent fetch_model_config() calls from multiple threads."""
        with patch("mca_sdk.registry.client.MIN_REFRESH_INTERVAL_SECS", 1):
            with patch("mca_sdk.registry.client.requests.Session"):
                client = RegistryClient(
                    url="https://registry.example.com",
                    refresh_interval_secs=1,
                )

                fetch_count = {"value": 0}
                errors = []

                # Mock internal fetch to track calls
                original_fetch = client._fetch_model_config_internal

                def mock_internal_fetch(*args, **kwargs):
                    fetch_count["value"] += 1
                    time.sleep(0.1)  # Simulate network delay
                    return mock_model_config

                # Thread that calls fetch_model_config
                def fetcher_thread(model_id):
                    try:
                        for _ in range(5):
                            client.fetch_model_config(model_id, "1.0.0")
                            time.sleep(0.05)
                    except Exception as e:
                        errors.append(e)

                with patch.object(
                    client,
                    "_fetch_model_config_internal",
                    side_effect=mock_internal_fetch,
                ):
                    # Start multiple fetcher threads
                    threads = [
                        threading.Thread(target=fetcher_thread, args=(f"mdl-{i:03d}",))
                        for i in range(3)
                    ]

                    for thread in threads:
                        thread.start()

                    for thread in threads:
                        thread.join()

                # Verify no errors
                assert len(errors) == 0, f"Thread safety errors: {errors}"

                # Verify cache was used (fetch count should be less than total calls)
                # 3 threads * 5 calls each = 15 total calls, but cache should reduce fetches
                assert fetch_count["value"] >= 3  # At least one fetch per model

                # Stop refresh thread
                client.stop_refresh_thread()

    def test_cache_updates_are_atomic(self, mock_model_config):
        """Test cache updates are atomic (thread stress test)."""
        with patch("mca_sdk.registry.client.MIN_REFRESH_INTERVAL_SECS", 1):
            with patch("mca_sdk.registry.client.requests.Session"):
                client = RegistryClient(
                    url="https://registry.example.com",
                    refresh_interval_secs=1,
                )

                # Track models for refresh
                for i in range(5):
                    client._fetched_models[f"mdl-{i:03d}"] = "1.0.0"

                errors = []
                corrupted_reads = {"value": 0}

                # Mock fetch to return configs
                def mock_fetch(model_id, version=None):
                    return ModelConfig(
                        service_name="test-service",
                        model_id=model_id,
                        model_version="1.0.0",
                        team_name="test-team",
                        model_category="internal",
                        model_type="classification",
                        thresholds={"accuracy": 0.95},
                        extra_resource={},
                    )

                # Reader thread that validates cache integrity
                def cache_reader():
                    try:
                        for _ in range(50):
                            # Read all cache entries
                            with client._cache_lock:
                                cache_snapshot = dict(client._cache)

                            # Validate each entry has required attributes
                            for key, entry in cache_snapshot.items():
                                if not hasattr(entry, "value") or not hasattr(entry, "expires_at"):
                                    corrupted_reads["value"] += 1

                            time.sleep(0.01)
                    except Exception as e:
                        errors.append(e)

                with patch.object(client, "fetch_model_config", side_effect=mock_fetch):
                    # Pre-populate cache
                    for i in range(5):
                        client._set_cache(
                            f"model:mdl-{i:03d}:1.0.0",
                            mock_fetch(f"mdl-{i:03d}"),
                        )

                    # Start reader threads
                    readers = [threading.Thread(target=cache_reader) for _ in range(3)]
                    for reader in readers:
                        reader.start()

                    # Let refresh thread run
                    time.sleep(2.5)

                    # Stop threads
                    client.stop_refresh_thread()
                    for reader in readers:
                        reader.join()

                # Verify no errors or corrupted reads
                assert len(errors) == 0, f"Thread safety errors: {errors}"
                assert corrupted_reads["value"] == 0, "Cache corruption detected"
