"""Tests for fix applier."""

import pytest

from risicare.runtime.applier import FixApplier
from risicare.runtime.cache import FixCache
from risicare.runtime.config import ActiveFix, FixRuntimeConfig


@pytest.fixture
def config():
    """Create test configuration."""
    return FixRuntimeConfig(
        enabled=True,
        dry_run=False,
        ab_testing_enabled=True,
    )


@pytest.fixture
def cache(config):
    """Create test cache."""
    return FixCache(config)


@pytest.fixture
def applier(config, cache):
    """Create test applier."""
    return FixApplier(config, cache)


class TestPromptFix:
    """Tests for prompt fix application."""

    def test_append_to_system(self, applier):
        """Test appending to system message."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="REASONING.OUTPUT.FORMAT",
            fix_type="prompt",
            config={
                "modification_type": "append",
                "target": "system",
                "content": "Always respond in JSON format.",
            },
            traffic_percentage=100,
            version=1,
        )

        messages = [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": "Hello"},
        ]

        modified, result = applier.apply_prompt_fix(fix, messages)

        assert result.applied is True
        assert "JSON format" in modified[0]["content"]
        assert "helpful assistant" in modified[0]["content"]

    def test_prepend_to_system(self, applier):
        """Test prepending to system message."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="PERCEPTION.INPUT.MALFORMED",
            fix_type="prompt",
            config={
                "modification_type": "prepend",
                "target": "system",
                "content": "IMPORTANT: Validate all inputs.",
            },
            traffic_percentage=100,
            version=1,
        )

        messages = [
            {"role": "system", "content": "You are an assistant."},
        ]

        modified, result = applier.apply_prompt_fix(fix, messages)

        assert result.applied is True
        assert modified[0]["content"].startswith("IMPORTANT")

    def test_replace_pattern(self, applier):
        """Test pattern replacement in messages."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.FORMAT.INVALID",
            fix_type="prompt",
            config={
                "modification_type": "replace",
                "pattern": r"respond freely",
                "content": "respond in JSON format",
            },
            traffic_percentage=100,
            version=1,
        )

        messages = [
            {"role": "system", "content": "Please respond freely with your answer."},
        ]

        modified, result = applier.apply_prompt_fix(fix, messages)

        assert result.applied is True
        assert "JSON format" in modified[0]["content"]
        assert "freely" not in modified[0]["content"]

    def test_few_shot_examples(self, applier):
        """Test adding few-shot examples."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="REASONING.INFERENCE.INCORRECT",
            fix_type="prompt",
            config={
                "modification_type": "few_shot",
                "examples": [
                    {"user": "What is 2+2?", "assistant": "4"},
                    {"user": "What is 3+3?", "assistant": "6"},
                ],
            },
            traffic_percentage=100,
            version=1,
        )

        messages = [
            {"role": "system", "content": "You are a math helper."},
            {"role": "user", "content": "What is 5+5?"},
        ]

        modified, result = applier.apply_prompt_fix(fix, messages)

        assert result.applied is True
        assert len(modified) == 6  # system + 2 examples (4 messages) + user


class TestParameterFix:
    """Tests for parameter fix application."""

    def test_update_parameters(self, applier):
        """Test updating LLM parameters."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.GENERATION.RANDOM",
            fix_type="parameter",
            config={
                "parameters": {
                    "temperature": 0.1,
                    "max_tokens": 500,
                },
            },
            traffic_percentage=100,
            version=1,
        )

        params = {"model": "gpt-4o", "temperature": 0.7}

        modified, result = applier.apply_parameter_fix(fix, params)

        assert result.applied is True
        assert modified["temperature"] == 0.1
        assert modified["max_tokens"] == 500
        assert modified["model"] == "gpt-4o"  # Preserved


class TestRetryFix:
    """Tests for retry fix application."""

    def test_successful_retry(self, applier):
        """Test retry on transient failure."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="TOOL.EXECUTION.TIMEOUT",
            fix_type="retry",
            config={
                "max_retries": 3,
                "initial_delay_ms": 10,
                "exponential_base": 2.0,
                "jitter": False,
            },
            traffic_percentage=100,
            version=1,
        )

        call_count = 0

        def flaky_operation():
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise TimeoutError("Connection timeout")
            return "success"

        result_value, apply_result = applier.apply_retry_fix(fix, flaky_operation)

        assert result_value == "success"
        assert apply_result.applied is True
        assert call_count == 3

    def test_exhausted_retries(self, applier):
        """Test exhausting all retries."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="TOOL.EXECUTION.FAILURE",
            fix_type="retry",
            config={
                "max_retries": 2,
                "initial_delay_ms": 10,
                "jitter": False,
            },
            traffic_percentage=100,
            version=1,
        )

        def always_fail():
            raise RuntimeError("Always fails")

        with pytest.raises(RuntimeError):
            applier.apply_retry_fix(fix, always_fail)


class TestFallbackFix:
    """Tests for fallback fix application."""

    def test_model_fallback(self, applier):
        """Test switching to fallback model."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="TOOL.EXECUTION.RATE_LIMIT",
            fix_type="fallback",
            config={
                "fallback_type": "model",
                "fallback_config": {
                    "model": "gpt-4o-mini",
                },
            },
            traffic_percentage=100,
            version=1,
        )

        params = {"model": "gpt-4o"}

        modified, result = applier.apply_fallback_fix(fix, params)

        assert result.applied is True
        assert modified["model"] == "gpt-4o-mini"


class TestGuardFix:
    """Tests for guard fix application."""

    def test_format_check_json_valid(self, applier):
        """Test JSON format validation passing."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.FORMAT.JSON_INVALID",
            fix_type="guard",
            config={
                "guard_type": "format_check",
                "guard_config": {"format": "json"},
            },
            traffic_percentage=100,
            version=1,
        )

        content = '{"key": "value"}'
        _, passed, result = applier.apply_guard_fix(fix, content, is_input=False)

        assert result.applied is True
        assert passed is True

    def test_format_check_json_invalid(self, applier):
        """Test JSON format validation failing."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.FORMAT.JSON_INVALID",
            fix_type="guard",
            config={
                "guard_type": "format_check",
                "guard_config": {"format": "json"},
            },
            traffic_percentage=100,
            version=1,
        )

        content = "not valid json {"
        _, passed, result = applier.apply_guard_fix(fix, content, is_input=False)

        assert result.applied is True
        assert passed is False

    def test_content_filter(self, applier):
        """Test content filter guard."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.SAFETY.BLOCKED",
            fix_type="guard",
            config={
                "guard_type": "content_filter",
                "guard_config": {
                    "blocked_patterns": [r"password", r"secret"],
                },
            },
            traffic_percentage=100,
            version=1,
        )

        # Should pass
        _, passed, _ = applier.apply_guard_fix(fix, "Hello world", is_input=True)
        assert passed is True

        # Should fail
        _, passed, _ = applier.apply_guard_fix(
            fix, "The password is 123", is_input=True
        )
        assert passed is False

    def test_length_validation(self, applier):
        """Test length validation guard."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="OUTPUT.LENGTH.EXCEEDED",
            fix_type="guard",
            config={
                "guard_type": "output_validation",
                "guard_config": {"min_length": 10, "max_length": 100},
            },
            traffic_percentage=100,
            version=1,
        )

        # Too short
        _, passed, _ = applier.apply_guard_fix(fix, "Hi", is_input=False)
        assert passed is False

        # Just right
        _, passed, _ = applier.apply_guard_fix(
            fix, "This is a valid response.", is_input=False
        )
        assert passed is True

        # Too long
        _, passed, _ = applier.apply_guard_fix(fix, "x" * 200, is_input=False)
        assert passed is False


class TestDryRun:
    """Tests for dry run mode."""

    def test_dry_run_prompt_fix(self):
        """Test prompt fix in dry run mode."""
        config = FixRuntimeConfig(enabled=True, dry_run=True)
        cache = FixCache(config)
        applier = FixApplier(config, cache)

        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="ERROR.1",
            fix_type="prompt",
            config={"modification_type": "append", "content": "Added content"},
            traffic_percentage=100,
            version=1,
        )

        messages = [{"role": "system", "content": "Original"}]
        modified, result = applier.apply_prompt_fix(fix, messages)

        # Should not modify in dry run
        assert modified[0]["content"] == "Original"
        assert result.applied is False


class TestABTesting:
    """Tests for A/B testing logic."""

    def test_traffic_percentage_100(self, applier, cache):
        """Test 100% traffic always applies."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="ERROR.1",
            fix_type="retry",
            config={},
            traffic_percentage=100,
            version=1,
        )
        cache.set(fix)

        # Should always get fix
        for i in range(10):
            result = applier.get_fix_for_error("ERROR.1", f"session-{i}")
            assert result is not None

    def test_traffic_percentage_0(self, applier, cache):
        """Test 0% traffic never applies."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="ERROR.1",
            fix_type="retry",
            config={},
            traffic_percentage=0,
            version=1,
        )
        cache.set(fix)

        # Should never get fix
        for i in range(10):
            result = applier.get_fix_for_error("ERROR.1", f"session-{i}")
            assert result is None

    def test_traffic_percentage_50(self, applier, cache):
        """Test 50% traffic applies to roughly half."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="ERROR.1",
            fix_type="retry",
            config={},
            traffic_percentage=50,
            version=1,
        )
        cache.set(fix)

        applied_count = 0
        for i in range(100):
            result = applier.get_fix_for_error("ERROR.1", f"session-{i}")
            if result is not None:
                applied_count += 1

        # Should be roughly 50%, allow for variance
        assert 30 < applied_count < 70

    def test_consistent_bucketing(self, applier, cache):
        """Test same session always gets same result."""
        fix = ActiveFix(
            fix_id="fix-1",
            deployment_id="d-1",
            error_code="ERROR.1",
            fix_type="retry",
            config={},
            traffic_percentage=50,
            version=1,
        )
        cache.set(fix)

        session_id = "consistent-session"
        first_result = applier.get_fix_for_error("ERROR.1", session_id)

        # Same session should always get same result
        for _ in range(10):
            result = applier.get_fix_for_error("ERROR.1", session_id)
            if first_result is None:
                assert result is None
            else:
                assert result is not None
