"""Step definitions for services __init__ lazy-import coverage (round 3).

Exercises every lazy-loaded symbol declared in
``cleveragents.application.services._LAZY_IMPORTS`` so that the
``__getattr__`` code path (lines 561-569) is executed at runtime.
Each scenario mocks ``importlib.import_module`` to avoid pulling in the
full dependency graph and simply verifies that the lazy-loading machinery
resolves the requested attribute name correctly.
"""

from __future__ import annotations

import importlib
import sys
import types
from unittest.mock import patch

from behave import given, then, when

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_PKG = "cleveragents.application.services"


def _get_fresh_module():
    """Return the services __init__ module after purging cached lazy attrs.

    We reload the module so that every scenario exercises ``__getattr__``
    afresh rather than hitting the ``globals()`` cache set by a previous
    lazy load.
    """
    mod = sys.modules.get(_PKG)
    if mod is None:
        mod = importlib.import_module(_PKG)
    else:
        # Remove any previously-cached lazy symbols from module globals
        # so __getattr__ is triggered again.
        for attr_name in list(mod._LAZY_IMPORTS):
            mod.__dict__.pop(attr_name, None)
    return mod


def _make_fake_submodule(attr_name: str) -> types.ModuleType:
    """Build a tiny fake module that carries ``attr_name`` as a sentinel."""
    fake = types.ModuleType(f"fake_{attr_name}")
    setattr(fake, attr_name, type(attr_name, (), {}))
    return fake


# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------


@given("svcov3 a fresh services module")
def step_svcov3_fresh_module(context):
    """Prepare a clean services module and set up the import_module mock."""
    context.svcov3_mod = _get_fresh_module()
    context.svcov3_error = None
    context.svcov3_value = None

    # Patch importlib.import_module inside the services __init__ so that we
    # do not trigger heavy transitive imports.  The patched version creates
    # lightweight fake modules on the fly.
    original_import_module = importlib.import_module

    def _patched_import_module(name, package=None):
        # Only intercept calls that originate from the lazy-loader.
        if package == _PKG and name.startswith("."):
            submodule_name = name.lstrip(".")
            full_name = f"{_PKG}.{submodule_name}"
            # If the real submodule is already in sys.modules use it -
            # this avoids breakage when earlier tests legitimately loaded
            # the submodule.
            if full_name in sys.modules:
                return sys.modules[full_name]
            # Otherwise try a real import; fall back to a fake module.
            try:
                return original_import_module(name, package)
            except Exception:
                # Build a fake module that provides every attr declared
                # in _LAZY_IMPORTS for this submodule.
                fake = types.ModuleType(full_name)
                lazy = context.svcov3_mod._LAZY_IMPORTS
                for _sym, (sub, attr) in lazy.items():
                    if sub == submodule_name:
                        setattr(fake, attr, type(attr, (), {}))
                sys.modules[full_name] = fake
                return fake
        return original_import_module(name, package)

    patcher = patch("importlib.import_module", side_effect=_patched_import_module)
    patcher.start()
    context.add_cleanup(patcher.stop)


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


@when('svcov3 I access lazy attribute "{attr_name}"')
def step_svcov3_access_lazy(context, attr_name):
    """Trigger ``__getattr__`` for *attr_name* and store the result."""
    try:
        # Remove from globals to force __getattr__ path
        context.svcov3_mod.__dict__.pop(attr_name, None)
        context.svcov3_value = getattr(context.svcov3_mod, attr_name)
    except Exception as exc:
        context.svcov3_error = exc


@when('svcov3 I access lazy attribute "{attr_name}" again')
def step_svcov3_access_lazy_again(context, attr_name):
    """Access the attribute a second time — should come from globals cache."""
    try:
        context.svcov3_value = getattr(context.svcov3_mod, attr_name)
    except Exception as exc:
        context.svcov3_error = exc


@when('svcov3 I access an unknown attribute "{attr_name}"')
def step_svcov3_access_unknown(context, attr_name):
    """Try to access an attribute not in _LAZY_IMPORTS."""
    try:
        getattr(context.svcov3_mod, attr_name)
    except AttributeError as exc:
        context.svcov3_error = exc
    except Exception as exc:
        context.svcov3_error = exc


# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------


@then('svcov3 the attribute "{attr_name}" should be resolved')
def step_svcov3_resolved(context, attr_name):
    """Assert the lazy attribute was resolved without error."""
    assert context.svcov3_error is None, (
        f"Expected no error for {attr_name!r}, got: {context.svcov3_error}"
    )
    assert context.svcov3_value is not None, (
        f"Expected a non-None value for {attr_name!r}"
    )


@then('svcov3 an AttributeError should be stored for "{attr_name}"')
def step_svcov3_attribute_error(context, attr_name):
    """Assert that an ``AttributeError`` was raised."""
    assert isinstance(context.svcov3_error, AttributeError), (
        f"Expected AttributeError, got {type(context.svcov3_error).__name__}: "
        f"{context.svcov3_error}"
    )
    assert attr_name in str(context.svcov3_error)


@then("svcov3 __all__ should equal sorted _LAZY_IMPORTS keys")
def step_svcov3_all_matches(context):
    """Verify ``__all__`` is the sorted list of ``_LAZY_IMPORTS`` keys."""
    mod = context.svcov3_mod
    expected = sorted(mod._LAZY_IMPORTS.keys())
    assert mod.__all__ == expected, (
        f"__all__ mismatch.\n"
        f"  Missing from __all__: {set(expected) - set(mod.__all__)}\n"
        f"  Extra in __all__:     {set(mod.__all__) - set(expected)}"
    )
