"""Step definitions for service_retry_wiring_coverage.feature.

Targets uncovered lines in service_retry_wiring.py:
- Line 149, 220: _apply_settings_defaults with only_services parameter
- Lines 252-254: _apply_config_overrides inner TypeError fallback
- Line 262: _build_cached_wait with string backoff_strategy
- Line 303: _get_or_create_cb returns None when CB disabled
- Line 308: _get_or_create_cb double-check after lock
- Lines 385-387: execute() TypeError on async callable
- Line 407: execute() nesting guard with no CB
- Line 441: execute() retry loop with no CB
- Line 521: async_execute() nesting guard with no CB
- Line 553: async_execute() retry loop with no CB
- Line 625: wrap_service_method with string backoff_strategy
"""

import asyncio
import json
import os

from behave import given, then, when

from cleveragents.application.services import service_retry_wiring as srw_module
from cleveragents.application.services.service_retry_wiring import ServiceRetryWiring
from cleveragents.config.settings import Settings
from cleveragents.core.retry_patterns import _retry_depth
from cleveragents.domain.models.core.retry_policy import (
    CircuitBreakerConfig,
    RetryPolicyConfig,
    RetryStrategy,
    ServiceRetryPolicy,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the service retry wiring module is imported")
def step_module_imported(context):
    """Verify the module is importable."""
    assert ServiceRetryWiring is not None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_settings_via_env(**env_overrides) -> Settings:
    """Create a Settings instance using environment variables for overrides.

    pydantic-settings prioritises env vars over constructor kwargs, so we
    must set env vars to override field defaults.
    """
    env_map = {
        "retry_max_attempts": "CLEVERAGENTS_RETRY_MAX_ATTEMPTS",
        "retry_base_delay": "CLEVERAGENTS_RETRY_BASE_DELAY",
        "retry_service_overrides": "CLEVERAGENTS_RETRY_SERVICE_OVERRIDES",
    }
    saved = {}
    for key, value in env_overrides.items():
        env_key = env_map.get(key, f"CLEVERAGENTS_{key.upper()}")
        saved[env_key] = os.environ.get(env_key)
        os.environ[env_key] = str(value)

    try:
        return Settings(env="test", mock_providers=True)
    finally:
        for env_key, old_val in saved.items():
            if old_val is None:
                os.environ.pop(env_key, None)
            else:
                os.environ[env_key] = old_val


def _make_default_settings() -> Settings:
    """Create a Settings instance with all defaults."""
    return Settings(env="test", mock_providers=True)


# ---------------------------------------------------------------------------
# Line 149, 220: Config overrides add new service, global defaults re-applied
# ---------------------------------------------------------------------------
@given("settings with a non-default retry_max_attempts of {n:d}")
def step_settings_non_default_max_attempts(context, n):
    context.custom_max_attempts = n


@given('settings with retry_service_overrides introducing a new service "{svc_name}"')
def step_settings_with_new_service_override(context, svc_name):
    overrides = {svc_name: {"retry": {"base_delay": 0.5, "max_delay": 5.0}}}
    context.custom_settings = _make_settings_via_env(
        retry_max_attempts=context.custom_max_attempts,
        retry_service_overrides=json.dumps(overrides),
    )


@when("I create a ServiceRetryWiring from those settings")
def step_create_wiring_from_settings(context):
    context.wiring = ServiceRetryWiring(context.custom_settings)


@then('the policy for "{svc_name}" should have max_attempts of {n:d}')
def step_verify_policy_max_attempts(context, svc_name, n):
    policy = context.wiring.get_policy(svc_name)
    assert policy.retry.max_attempts == n, (
        f"Expected max_attempts={n}, got {policy.retry.max_attempts}"
    )


# ---------------------------------------------------------------------------
# Lines 252-254: Invalid JSON + logger.warning raises TypeError
# ---------------------------------------------------------------------------
@given("settings with invalid JSON in retry_service_overrides")
def step_settings_invalid_json(context):
    context.invalid_json_settings = _make_settings_via_env(
        retry_service_overrides="{this is not valid json!!!}",
    )


@given("a logger that raises TypeError on warning calls")
def step_patch_logger_typeerror(context):
    """Install a logger whose .warning() always raises TypeError."""

    class TypeErrorLogger:
        def debug(self, *a, **kw):
            pass

        def info(self, *a, **kw):
            pass

        def warning(self, *a, **kw):
            raise TypeError("simulated logger TypeError")

        def error(self, *a, **kw):
            pass

    context.original_logger = srw_module.logger
    context.type_error_logger = TypeErrorLogger()


@when("I create a ServiceRetryWiring with the patched logger")
def step_create_wiring_patched_logger(context):
    original = srw_module.logger
    srw_module.logger = context.type_error_logger
    try:
        context.wiring = ServiceRetryWiring(context.invalid_json_settings)
        context.construction_error = None
    except Exception as exc:
        context.construction_error = exc
    finally:
        srw_module.logger = original


@then("no exception should be raised during construction")
def step_verify_no_construction_error(context):
    assert context.construction_error is None, (
        f"Expected no error, got {context.construction_error!r}"
    )


# ---------------------------------------------------------------------------
# Line 262: _build_cached_wait with plain string backoff_strategy
# ---------------------------------------------------------------------------
@given('a ServiceRetryPolicy with a plain string backoff_strategy "{strategy}"')
def step_policy_with_string_backoff(context, strategy):
    policy = ServiceRetryPolicy(
        service_name="string_backoff_svc",
        retry=RetryPolicyConfig(
            max_attempts=2,
            base_delay=0.1,
            max_delay=1.0,
            backoff_strategy=RetryStrategy.EXPONENTIAL,
        ),
    )
    # Force the backoff_strategy to a plain string to bypass enum
    object.__setattr__(policy.retry, "backoff_strategy", strategy)
    context.string_backoff_policy = policy


@when("I call _build_cached_wait with the string-strategy policy")
def step_call_build_cached_wait(context):
    context.wait_strategy = ServiceRetryWiring._build_cached_wait(
        context.string_backoff_policy
    )


@then("a valid wait strategy should be returned")
def step_verify_wait_strategy(context):
    assert context.wait_strategy is not None


# ---------------------------------------------------------------------------
# Line 303: _get_or_create_cb returns None when CB disabled
# ---------------------------------------------------------------------------
@given("a ServiceRetryWiring with a service that has circuit breaker disabled")
def step_wiring_with_disabled_cb(context):
    settings = _make_default_settings()
    context.wiring = ServiceRetryWiring(settings)
    # Register a service with CB disabled
    disabled_cb_policy = ServiceRetryPolicy(
        service_name="no_cb_service",
        retry=RetryPolicyConfig(max_attempts=2, base_delay=0.01, max_delay=0.1),
        circuit_breaker=CircuitBreakerConfig(enabled=False),
    )
    context.wiring._registry.register(disabled_cb_policy)


@when("I call _get_or_create_cb for the disabled-CB service")
def step_call_get_or_create_cb_disabled(context):
    context.cb_result = context.wiring._get_or_create_cb("no_cb_service")


@then("srwcov the result should be None")
def step_verify_cb_none(context):
    assert context.cb_result is None


# ---------------------------------------------------------------------------
# Line 308: _get_or_create_cb double-check path
# ---------------------------------------------------------------------------
@given("a ServiceRetryWiring with default settings")
def step_wiring_default_settings(context):
    settings = _make_default_settings()
    context.wiring = ServiceRetryWiring(settings)


@when("I concurrently request the circuit breaker for a new service with CB enabled")
def step_concurrent_cb_request(context):
    """Simulate the double-check-after-lock path by using threading to
    insert a CB between the initial dict miss and the lock acquisition."""
    svc_name = "double_check_svc"
    # Ensure the service has a policy with CB enabled
    policy = ServiceRetryPolicy(
        service_name=svc_name,
        retry=RetryPolicyConfig(max_attempts=2, base_delay=0.01, max_delay=0.1),
        circuit_breaker=CircuitBreakerConfig(enabled=True),
    )
    context.wiring._registry.register(policy)

    # First call creates the CB normally
    cb1 = context.wiring._get_or_create_cb(svc_name)
    assert cb1 is not None

    # Now simulate the double-check race condition (line 308):
    # Remove the CB from the fast dict, then use a thread to re-insert
    # it before the lock-guarded second check in _get_or_create_cb.
    existing_cb = context.wiring._circuit_breakers.pop(svc_name)

    # Replace _cache_lock with a wrapper that injects the CB before
    # the code inside the `with` block runs.
    original_lock = context.wiring._cache_lock

    class InjectingLock:
        """A lock wrapper that injects the CB after acquire but before
        the inner code checks the dict again (line 306-308)."""

        def __init__(self, real_lock, inject_fn):
            self._real_lock = real_lock
            self._inject_fn = inject_fn

        def acquire(self, *args, **kwargs):
            result = self._real_lock.acquire(*args, **kwargs)
            self._inject_fn()
            return result

        def release(self, *args, **kwargs):
            return self._real_lock.release(*args, **kwargs)

        def __enter__(self):
            self.acquire()
            return self

        def __exit__(self, *args):
            self.release()

    def inject_cb():
        context.wiring._circuit_breakers[svc_name] = existing_cb

    context.wiring._cache_lock = InjectingLock(original_lock, inject_cb)
    try:
        result = context.wiring._get_or_create_cb(svc_name)
    finally:
        context.wiring._cache_lock = original_lock

    context.double_check_cb = result
    context.existing_cb = existing_cb


@then("the circuit breaker should be created once and cached")
def step_verify_cb_cached(context):
    # The double-check path should return the pre-existing CB
    assert context.double_check_cb is context.existing_cb


# ---------------------------------------------------------------------------
# Lines 385-387: execute() with async callable raises TypeError
# ---------------------------------------------------------------------------
@given("an async callable function")
def step_async_callable(context):
    async def async_fn():
        return "async_result"

    context.async_fn = async_fn


@when("I call sync execute with the async callable")
def step_call_sync_execute_with_async(context):
    try:
        context.wiring.execute(
            service_name="plan_service",
            operation_name="test_op",
            func=context.async_fn,
        )
        context.execute_error = None
    except TypeError as exc:
        context.execute_error = exc


@then("a TypeError should be raised mentioning async_execute")
def step_verify_type_error(context):
    assert context.execute_error is not None
    assert "async_execute" in str(context.execute_error)


# ---------------------------------------------------------------------------
# Line 407: execute() nesting guard with no CB
# ---------------------------------------------------------------------------
@given("the retry nesting depth is at the maximum")
def step_set_max_nesting_depth(context):
    """Set the retry depth context var to the maximum nesting depth."""
    context.depth_token = _retry_depth.set(srw_module.MAX_RETRY_NESTING_DEPTH)

    def restore():
        _retry_depth.reset(context.depth_token)

    context.add_cleanup(restore)


@when("I call execute for the disabled-CB service with a simple function")
def step_execute_nesting_guard_no_cb(context):
    def simple_fn():
        return "nesting_guard_result"

    context.execute_result = context.wiring.execute(
        service_name="no_cb_service",
        operation_name="test_op",
        func=simple_fn,
    )


@then("the function should be called directly without retry wrapping")
def step_verify_nesting_guard_result(context):
    assert context.execute_result == "nesting_guard_result"


# ---------------------------------------------------------------------------
# Line 441: execute() retry loop with no CB
# ---------------------------------------------------------------------------
@when("I call execute for the disabled-CB service with a simple function normally")
def step_execute_no_cb_normal(context):
    def simple_fn():
        return "no_cb_result"

    context.execute_result = context.wiring.execute(
        service_name="no_cb_service",
        operation_name="test_op",
        func=simple_fn,
    )


@then("the function result should be returned successfully")
def step_verify_no_cb_result(context):
    assert context.execute_result == "no_cb_result"


# ---------------------------------------------------------------------------
# Line 521: async_execute() nesting guard with no CB
# ---------------------------------------------------------------------------
@when("I call async_execute for the disabled-CB service with an async function")
def step_async_execute_nesting_guard_no_cb(context):
    async def async_fn():
        return "async_nesting_guard_result"

    loop = asyncio.new_event_loop()
    try:
        context.async_result = loop.run_until_complete(
            context.wiring.async_execute(
                service_name="no_cb_service",
                operation_name="test_op",
                func=async_fn,
            )
        )
    finally:
        loop.close()


@then("the async function should be called directly without retry wrapping")
def step_verify_async_nesting_guard_result(context):
    assert context.async_result == "async_nesting_guard_result"


# ---------------------------------------------------------------------------
# Line 553: async_execute() retry loop with no CB
# ---------------------------------------------------------------------------
@when(
    "I call async_execute for the disabled-CB service with an async function normally"
)
def step_async_execute_no_cb_normal(context):
    async def async_fn():
        return "async_no_cb_result"

    loop = asyncio.new_event_loop()
    try:
        context.async_result = loop.run_until_complete(
            context.wiring.async_execute(
                service_name="no_cb_service",
                operation_name="test_op",
                func=async_fn,
            )
        )
    finally:
        loop.close()


@then("the async function result should be returned successfully")
def step_verify_async_no_cb_result(context):
    assert context.async_result == "async_no_cb_result"


# ---------------------------------------------------------------------------
# Line 625: wrap_service_method with string backoff_strategy
# ---------------------------------------------------------------------------
@given("a ServiceRetryWiring where a service has a plain string backoff_strategy")
def step_wiring_with_string_backoff(context):
    settings = _make_default_settings()
    context.wiring = ServiceRetryWiring(settings)
    # Create a policy with a string backoff_strategy and CB disabled
    # (CB disabled avoids the _cache_lock deadlock inside wrap_service_method).
    policy = ServiceRetryPolicy(
        service_name="str_backoff_wrap_svc",
        retry=RetryPolicyConfig(
            max_attempts=2,
            base_delay=0.01,
            max_delay=0.1,
            backoff_strategy=RetryStrategy.EXPONENTIAL,
        ),
        circuit_breaker=CircuitBreakerConfig(enabled=False),
    )
    # Force backoff_strategy to a plain string to exercise line 625
    object.__setattr__(policy.retry, "backoff_strategy", "exponential")
    context.string_policy_for_wrap = policy


@when("I call wrap_service_method for that service")
def step_call_wrap_service_method(context):
    # Patch registry.get to return our policy with string backoff_strategy.
    # We must patch at the registry level because registry.get() deep-copies
    # the stored policy, which would re-validate and coerce the string back
    # to the RetryStrategy enum.
    original_get = context.wiring._registry.get

    def patched_get(service_name):
        if service_name == "str_backoff_wrap_svc":
            return context.string_policy_for_wrap
        return original_get(service_name)

    context.wiring._registry.get = patched_get
    try:
        context.decorator = context.wiring.wrap_service_method(
            service_name="str_backoff_wrap_svc",
            operation_name="test_op",
        )
    finally:
        context.wiring._registry.get = original_get


@then("a callable decorator should be returned")
def step_verify_decorator_returned(context):
    assert callable(context.decorator)
