"""Step definitions for tdd_automation_profile_di_bypass.feature.

These steps verify that ``_get_service()`` in
``cleveragents.cli.commands.automation_profile`` delegates to
``container.automation_profile_service()`` rather than manually constructing
the service with ``create_engine`` / ``sessionmaker`` / repositories.

Bug #990 was fixed by PR #1181. The ``@tdd_expected_fail`` tag was omitted
because the fix landed on master before this TDD test PR merged; these
scenarios now run as permanent regression tests confirming the DI bypass
remains fixed.

Step namespacing: all steps use the ``ap990-`` prefix to avoid collisions
with other test suites.  The defensive patching approach provides both a DI
container mock *and* infrastructure mocks so that a regression causes the
tests to fail for the right reason (the DI bypass itself) rather than an
unrelated import error or attribute lookup.
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context

# ---------------------------------------------------------------------------
# Constants — patch targets
# ---------------------------------------------------------------------------

_PATCH_GET_CONTAINER = "cleveragents.application.container.get_container"
_PATCH_CREATE_ENGINE = "sqlalchemy.create_engine"
_PATCH_SESSIONMAKER = "sqlalchemy.orm.sessionmaker"


# ---------------------------------------------------------------------------
# Scenario 1: _get_service delegates to container.automation_profile_service
# ---------------------------------------------------------------------------


@given("ap990- the DI container has an automation_profile_service provider")
def step_ap990_container_has_provider(context: Context) -> None:
    """Set up a mock container with an automation_profile_service() callable.

    Defensive patching: both a DI container mock and silent sqlalchemy stubs
    are set up so that if _get_service() regresses to the bug #990 pattern it
    reaches the Then assertion (and fails there with a meaningful message)
    rather than crashing on a missing DB URL.
    """
    mock_service: MagicMock = MagicMock(name="MockAutomationProfileService")
    mock_container: MagicMock = MagicMock(name="MockContainer")
    mock_container.automation_profile_service.return_value = mock_service

    context.ap990_mock_container: MagicMock = mock_container
    context.ap990_mock_service: MagicMock = mock_service
    # Shared slot used by the When step that is also shared with Scenarios 2/3.
    context.ap990_di_mock_container: MagicMock = mock_container
    context.ap990_returned_service: Any = None


@when("ap990- _get_service is called with the mocked container")
def step_ap990_call_get_service_with_mock(context: Context) -> None:
    """Call _get_service() with get_container() returning the mock container."""
    from cleveragents.cli.commands.automation_profile import _get_service

    with patch(_PATCH_GET_CONTAINER, return_value=context.ap990_mock_container):
        context.ap990_returned_service = _get_service()


@then(
    "ap990- the returned service should be the one from container.automation_profile_service"
)
def step_ap990_verify_service_identity(context: Context) -> None:
    """The returned service must be exactly what container.automation_profile_service() returns.

    An identity check (``is``) catches regressions where _get_service()
    bypasses the container and constructs a fresh AutomationProfileService
    independently — the resulting object would differ from the mock sentinel.
    """
    assert context.ap990_returned_service is context.ap990_mock_service, (
        f"Expected the service returned by container.automation_profile_service(), "
        f"but got {type(context.ap990_returned_service)!r}. "
        "_get_service() is NOT delegating to the DI container."
    )


# ---------------------------------------------------------------------------
# Scenario 2: _get_service does not call create_engine directly
# ---------------------------------------------------------------------------


@given("ap990- create_engine is patched to detect direct calls")
def step_ap990_patch_create_engine(context: Context) -> None:
    """Patch create_engine and set up a DI mock so _get_service() can succeed.

    Defensive patching: the DI mock ensures the fixed code path (container
    delegation) completes successfully.  The create_engine mock catches any
    regression where _get_service() rebuilds the session factory manually.
    """
    create_engine_mock: MagicMock = MagicMock(name="MockCreateEngine")
    patcher = patch(_PATCH_CREATE_ENGINE, create_engine_mock)
    patcher.start()
    context.add_cleanup(patcher.stop)
    context.ap990_create_engine_mock: MagicMock = create_engine_mock

    mock_service: MagicMock = MagicMock(name="MockAutomationProfileServiceForEngine")
    mock_container: MagicMock = MagicMock(name="MockContainerForEngine")
    mock_container.automation_profile_service.return_value = mock_service
    context.ap990_di_mock_container: MagicMock = mock_container


@when("ap990- _get_service is called with a valid DI container")
def step_ap990_call_get_service_di_path(context: Context) -> None:
    """Call _get_service() via the DI path; the mock container is set in the Given step."""
    from cleveragents.cli.commands.automation_profile import _get_service

    with patch(_PATCH_GET_CONTAINER, return_value=context.ap990_di_mock_container):
        context.ap990_result: Any = _get_service()


@then("ap990- create_engine should not have been called")
def step_ap990_verify_create_engine_not_called(context: Context) -> None:
    """create_engine must never be called; the DI container owns engine construction.

    If this assertion fails, _get_service() has regressed to the bug #990
    pattern of manually building the persistence layer with a raw SQLAlchemy
    engine instead of delegating to container.automation_profile_service().
    """
    mock: MagicMock = context.ap990_create_engine_mock
    assert not mock.called, (
        f"Bug #990 regression: _get_service() called create_engine() directly "
        f"({mock.call_count} time(s)) instead of delegating to "
        "container.automation_profile_service()."
    )


# ---------------------------------------------------------------------------
# Scenario 3: _get_service does not call sessionmaker directly
# ---------------------------------------------------------------------------


@given("ap990- sessionmaker is patched to detect direct calls")
def step_ap990_patch_sessionmaker(context: Context) -> None:
    """Patch sessionmaker and set up a DI mock so _get_service() can succeed.

    Defensive patching mirrors Scenario 2: the DI mock ensures the fixed
    code path completes without error, and the sessionmaker mock detects any
    regression where _get_service() builds the ORM session factory directly.
    """
    sessionmaker_mock: MagicMock = MagicMock(name="MockSessionmaker")
    patcher = patch(_PATCH_SESSIONMAKER, sessionmaker_mock)
    patcher.start()
    context.add_cleanup(patcher.stop)
    context.ap990_sessionmaker_mock: MagicMock = sessionmaker_mock

    mock_service: MagicMock = MagicMock(name="MockAutomationProfileServiceForSM")
    mock_container: MagicMock = MagicMock(name="MockContainerForSM")
    mock_container.automation_profile_service.return_value = mock_service
    context.ap990_di_mock_container: MagicMock = mock_container


@then("ap990- sessionmaker should not have been called")
def step_ap990_verify_sessionmaker_not_called(context: Context) -> None:
    """sessionmaker must never be called; the DI container owns session factory setup.

    If this assertion fails, _get_service() has regressed to the bug #990
    pattern of manually constructing the ORM session factory with a raw
    sessionmaker call instead of delegating to container.automation_profile_service().
    """
    mock: MagicMock = context.ap990_sessionmaker_mock
    assert not mock.called, (
        f"Bug #990 regression: _get_service() called sessionmaker() directly "
        f"({mock.call_count} time(s)) instead of delegating to "
        "container.automation_profile_service()."
    )
