"""Step definitions for project_context_set_exec_env_priority.feature.

Regression tests for bug #1079: the ``--execution-env-priority`` flag
on ``project context set``.
"""

from __future__ import annotations

import json as _json
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
from typer.testing import CliRunner

from cleveragents.cli.commands.project_context import app as project_context_app

# ------------------------------------------------------------------
# Session wrapper — prevents the production ``close()`` from
# destroying our shared in-memory session.
# ------------------------------------------------------------------


class _SafeSession:
    """Thin wrapper preventing close() from destroying shared in-memory session."""

    def __init__(self, real: Session) -> None:
        object.__setattr__(self, "_real", real)

    def close(self) -> None:
        """Reset session state without closing the underlying connection."""
        real: Session = object.__getattribute__(self, "_real")
        real.rollback()

    def __getattr__(self, name: str) -> Any:
        return getattr(object.__getattribute__(self, "_real"), name)

    def __setattr__(self, name: str, value: Any) -> None:
        setattr(object.__getattribute__(self, "_real"), name, value)


def _make_session_factory(context: Any) -> tuple[Any, Any]:
    from cleveragents.infrastructure.database.models import Base

    engine = create_engine(
        "sqlite:///:memory:",
        echo=False,
        connect_args={"check_same_thread": False},
    )
    Base.metadata.create_all(engine)
    real = sessionmaker(
        bind=engine, expire_on_commit=False, autoflush=True, autocommit=False
    )()
    wrapper = _SafeSession(real)
    return engine, lambda: wrapper


# ------------------------------------------------------------------
# Mock container
# ------------------------------------------------------------------


def _mock_container(context: Any) -> MagicMock:
    from cleveragents.application.services.context_tiers import ContextTierService

    mc = MagicMock()
    mc.namespaced_project_repo.return_value = context.bug1079_project_repo
    mc.session_factory.return_value = context.bug1079_session_factory
    if not hasattr(context, "bug1079_tier_service"):
        context.bug1079_tier_service = ContextTierService()
    mc.context_tier_service.return_value = context.bug1079_tier_service
    return mc


def _invoke_set(context: Any, cli_args: list[str]) -> None:
    """Invoke ``project context set`` via the Typer CliRunner.

    Uses the CLI interface to verify the ``--execution-env-priority``
    flag is accepted and produces a zero exit code.
    """
    mc = _mock_container(context)
    runner = CliRunner()
    with patch(
        "cleveragents.application.container.get_container",
        return_value=mc,
    ):
        context.bug1079_result = runner.invoke(project_context_app, cli_args)
    context.bug1079_exit_code = context.bug1079_result.exit_code
    context.bug1079_output = context.bug1079_result.output


def _load_raw_blob(context: Any, ns: str) -> dict[str, Any] | None:
    session = context.bug1079_session_factory()
    row = session.execute(
        text("SELECT context_policy_json FROM ns_projects WHERE namespaced_name = :ns"),
        {"ns": ns},
    ).fetchone()
    if row is None or row[0] is None:
        return None
    return _json.loads(row[0])  # type: ignore[no-any-return]


# ------------------------------------------------------------------
# Background
# ------------------------------------------------------------------


@given("a bug 1079 in-memory database is initialized")
def step_bug1079_init_db(context: Any) -> None:
    from cleveragents.infrastructure.database.repositories import (
        NamespacedProjectRepository,
    )

    engine, sf = _make_session_factory(context)
    context.bug1079_engine = engine
    context.bug1079_session_factory = sf
    context.bug1079_project_repo = NamespacedProjectRepository(session_factory=sf)
    context.bug1079_output = ""
    context.bug1079_exit_code = 0
    context.add_cleanup(engine.dispose)


@given('a project "{name}" exists for bug 1079')
def step_bug1079_create_project(context: Any, name: str) -> None:
    from cleveragents.domain.models.core.project import (
        NamespacedProject,
        parse_namespaced_name,
    )

    parsed = parse_namespaced_name(name)
    proj = NamespacedProject(name=parsed.name, namespace=parsed.namespace)
    context.bug1079_project_repo.create(proj)


# ------------------------------------------------------------------
# When steps
# ------------------------------------------------------------------


@when(
    'I run bug 1079 context set on "{project}" '
    'with execution_environment "{env}" and execution_env_priority "{priority}"'
)
def step_bug1079_set_env_and_priority(
    context: Any, project: str, env: str, priority: str
) -> None:
    """Run project context set with both --execution-environment and --execution-env-priority.

    This exercises the code path that bug #1079 describes as missing.
    The ``--execution-env-priority`` flag does not exist on ``context_set`` yet,
    so the CLI runner will reject it with a "No such option" error — proving the bug.
    """
    _invoke_set(
        context,
        [
            "set",
            project,
            "--execution-environment",
            env,
            "--execution-env-priority",
            priority,
        ],
    )


@when(
    'I run bug 1079 context set on "{project}" '
    'with execution_env_priority "{priority}" but no execution_environment'
)
def step_bug1079_set_priority_no_env(context: Any, project: str, priority: str) -> None:
    """Run project context set with --execution-env-priority but no --execution-environment.

    When the bug is fixed, the command should reject this combination with a
    clear error message, mirroring the plan use validation behaviour.
    """
    _invoke_set(
        context,
        [
            "set",
            project,
            "--execution-env-priority",
            priority,
        ],
    )


@when(
    'I run bug 1079 context set on "{project}" '
    'with execution_environment "{env}" but no execution_env_priority'
)
def step_bug1079_set_env_no_priority(context: Any, project: str, env: str) -> None:
    """Run project context set with --execution-environment but no --execution-env-priority.

    When the bug is fixed, the command should default the priority to "fallback".
    """
    _invoke_set(
        context,
        [
            "set",
            project,
            "--execution-environment",
            env,
        ],
    )


@when('I run bug 1079 context show on "{project}" with format "{fmt}"')
def step_bug1079_show(context: Any, project: str, fmt: str) -> None:
    """Run project context show to check persisted state."""
    mc = _mock_container(context)
    runner = CliRunner()
    with patch(
        "cleveragents.application.container.get_container",
        return_value=mc,
    ):
        context.bug1079_result = runner.invoke(
            project_context_app,
            ["show", project, "--format", fmt],
        )
    context.bug1079_exit_code = context.bug1079_result.exit_code
    context.bug1079_output = context.bug1079_result.output


# ------------------------------------------------------------------
# Then steps
# ------------------------------------------------------------------


@then("the bug 1079 command should succeed")
def step_bug1079_ok(context: Any) -> None:
    assert context.bug1079_exit_code == 0, (
        f"Expected exit 0, got {context.bug1079_exit_code}. "
        f"Output: {context.bug1079_output}"
    )


@then("the bug 1079 command should fail")
def step_bug1079_fail(context: Any) -> None:
    assert context.bug1079_exit_code != 0, (
        f"Expected non-zero exit code, got {context.bug1079_exit_code}. "
        f"Output: {context.bug1079_output}"
    )


@then('the stored bug 1079 execution_env_priority should be "{priority}"')
def step_bug1079_stored_priority(context: Any, priority: str) -> None:
    """Assert execution_env_priority was persisted in the project context JSON.

    When the bug is fixed, the ``context_set`` command will persist this
    value alongside the execution_environment.  Currently the flag does
    not exist, so the blob will either be None or lack the key — causing
    this assertion to fail (as expected for a TDD bug-capture test).
    """
    blob = _load_raw_blob(context, "local/bug1079-app")
    assert blob is not None, "No policy blob stored — context set did not persist data"
    stored = blob.get("execution_env_priority")
    assert stored == priority, (
        f"Expected execution_env_priority='{priority}', got {stored!r}. "
        f"Full blob: {blob}"
    )


@then('the bug 1079 output should contain "{text}"')
def step_bug1079_output_contains(context: Any, text: str) -> None:
    assert text in context.bug1079_output, (
        f"Expected output to contain '{text}'. Output: {context.bug1079_output!r}"
    )


@then('the bug 1079 json output should include "{key}" as "{value}"')
def step_bug1079_json_includes(context: Any, key: str, value: str) -> None:
    raw = context.bug1079_output.strip()
    # The output may contain non-JSON lines (e.g. logging); find the JSON object
    start = raw.find("{")
    end = raw.rfind("}") + 1
    assert start >= 0 and end > start, f"No JSON object found in output: {raw!r}"
    data = _json.loads(raw[start:end])
    assert key in data, (
        f"Expected JSON to contain key '{key}'. Keys: {list(data.keys())}"
    )
    assert data[key] == value, f"Expected {key}='{value}', got {data[key]!r}"
