"""Step definitions for project_context_cli.feature.

Tests the ``agents project context set/show/inspect/simulate``
CLI commands using an in-memory SQLite database.
"""

from __future__ import annotations

import json
from io import StringIO
from typing import Any
from unittest.mock import MagicMock, patch

from behave import given, then, when  # type: ignore[import-untyped]
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

# ------------------------------------------------------------------
# Session wrapper (no-op close)
# ------------------------------------------------------------------


class _UnclosableSession:
    """Wraps a SQLAlchemy Session; ``close()`` is a no-op."""

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

    def close(self) -> None:
        """No-op."""

    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_ctx_session_factory(
    context: Any,
) -> tuple[Any, Any]:
    """In-memory SQLite with all tables."""
    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_session = sessionmaker(
        bind=engine,
        expire_on_commit=False,
        autoflush=True,
        autocommit=False,
    )()
    wrapper = _UnclosableSession(real_session)

    def _factory() -> Any:
        return wrapper

    return engine, _factory


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


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

    engine, session_factory = _make_ctx_session_factory(context)
    context.ctx_engine = engine
    context.ctx_session_factory = session_factory
    context.ctx_project_repo = NamespacedProjectRepository(
        session_factory=session_factory
    )
    context.ctx_output = ""
    context.ctx_exit_code = 0
    context.ctx_error = None


@given('a project "{name}" exists for context CLI')
def step_create_project_for_ctx(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.ctx_project_repo.create(proj)


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


def _mock_container(context: Any) -> MagicMock:
    """Build a mock container wired to the test DB."""
    from cleveragents.application.services.context_tiers import (
        ContextTierService,
    )

    mock_container = MagicMock()
    mock_container.namespaced_project_repo.return_value = context.ctx_project_repo
    mock_container.session_factory.return_value = context.ctx_session_factory
    # Provide a real (in-memory) ContextTierService for inspect/simulate
    if not hasattr(context, "ctx_tier_service"):
        context.ctx_tier_service = ContextTierService()
    mock_container.context_tier_service.return_value = context.ctx_tier_service
    return mock_container


def _run_with_container(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
    """Execute a CLI function with a mocked DI container."""
    import typer
    from rich.console import Console as RichConsole

    mock_cont = _mock_container(context)
    buf = StringIO()
    # force_terminal=False and highlight=False prevent Rich from emitting
    # ANSI escape codes when FORCE_COLOR is set or a real colour terminal
    # is detected.
    test_console = RichConsole(
        file=buf,
        no_color=True,
        highlight=False,
        force_terminal=False,
        width=500,
        soft_wrap=True,
    )

    def _test_format_output(data, format_type):
        import sys as _sys
        from io import StringIO as _SIO

        from cleveragents.cli.formatting import format_output as _fo

        _b = _SIO()
        _old = _sys.stdout
        _sys.stdout = _b
        try:
            _r = _fo(data, format_type)
        finally:
            _sys.stdout = _old
        return _r or _b.getvalue().rstrip("\n")

    with (
        patch(
            "cleveragents.application.container.get_container",
            return_value=mock_cont,
        ),
        patch(
            "cleveragents.cli.commands.project_context.console",
            test_console,
        ),
        patch(
            "cleveragents.cli.commands.project_context.err_console",
            test_console,
        ),
        patch(
            "cleveragents.cli.commands.project_context.format_output",
            _test_format_output,
        ),
    ):
        try:
            func(*args, **kwargs)
            context.ctx_exit_code = 0
        except typer.Exit as exc:
            context.ctx_exit_code = exc.exit_code
        except typer.Abort:
            context.ctx_exit_code = 1
        except NotImplementedError as exc:
            context.ctx_error = exc
            context.ctx_exit_code = 99

    context.ctx_output = buf.getvalue()


# ------------------------------------------------------------------
# context set
# ------------------------------------------------------------------


@when(
    'I run context set on "{project}" with view "{view}" and include-resource "{res}"'
)
def step_ctx_set_include_res(context: Any, project: str, view: str, res: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        include_resource=[res],
    )


@when(
    'I run context set on "{project}" with view "{view}" and exclude-resource "{res}"'
)
def step_ctx_set_exclude_res(context: Any, project: str, view: str, res: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        exclude_resource=[res],
    )


@when('I run context set on "{project}" with view "{view}" and include-path "{path}"')
def step_ctx_set_include_path(context: Any, project: str, view: str, path: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        include_path=[path],
    )


@when('I run context set on "{project}" with view "{view}" and exclude-path "{path}"')
def step_ctx_set_exclude_path(context: Any, project: str, view: str, path: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        exclude_path=[path],
    )


@when('I run context set on "{project}" with view "{view}" and max-file-size {size:d}')
def step_ctx_set_max_file_size(
    context: Any, project: str, view: str, size: int
) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        max_file_size=size,
    )


@when('I run context set on "{project}" with view "{view}" and max-total-size {size:d}')
def step_ctx_set_max_total_size(
    context: Any, project: str, view: str, size: int
) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        max_total_size=size,
    )


@when('I run context set on "{project}" with invalid view "{view}"')
def step_ctx_set_invalid_view(context: Any, project: str, view: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
    )


@when('I run context set on "{project}" with view "{view}" and clear flag')
def step_ctx_set_clear(context: Any, project: str, view: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view=view,
        clear=True,
    )


# ------------------------------------------------------------------
# context show
# ------------------------------------------------------------------


@when('I run context show on "{project}" without options')
def step_ctx_show(context: Any, project: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_show,
    )

    _run_with_container(context, context_show, project=project)


@when('I run context show on "{project}" with view "{view}"')
def step_ctx_show_view(context: Any, project: str, view: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_show,
    )

    _run_with_container(context, context_show, project=project, view=view)


@when('I run context show on "{project}" with format "{fmt}"')
def step_ctx_show_format(context: Any, project: str, fmt: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_show,
    )

    _run_with_container(
        context,
        context_show,
        project=project,
        output_format=fmt,
    )


# ------------------------------------------------------------------
# context inspect / simulate stubs
# ------------------------------------------------------------------


@when('I run context inspect on "{project}"')
def step_ctx_inspect(context: Any, project: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_inspect,
    )

    _run_with_container(context, context_inspect, project=project)


@when('I run context simulate on "{project}"')
def step_ctx_simulate(context: Any, project: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_simulate,
    )

    _run_with_container(context, context_simulate, project=project)


# ------------------------------------------------------------------
# Given steps (pre-conditions)
# ------------------------------------------------------------------


@given('I have set a strategize view on "{project}" with defaults')
def step_given_strategize_view(context: Any, project: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view="strategize",
        include_resource=["strat-default"],
    )


@given('I have set a default view on "{project}" with include-resource "{res}"')
def step_given_default_view(context: Any, project: str, res: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view="default",
        include_resource=[res],
    )


@given('I have set a strategize view on "{project}" with include-resource "{res}"')
def step_given_strategize_view_res(context: Any, project: str, res: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view="strategize",
        include_resource=[res],
    )


@given('I have set an execute view on "{project}" with include-resource "{res}"')
def step_given_execute_view_res(context: Any, project: str, res: str) -> None:
    from cleveragents.cli.commands.project_context import (
        context_set,
    )

    _run_with_container(
        context,
        context_set,
        project=project,
        view="execute",
        include_resource=[res],
    )


# ------------------------------------------------------------------
# Then assertions
# ------------------------------------------------------------------


@then("the context set command should succeed")
def step_ctx_set_ok(context: Any) -> None:
    assert context.ctx_exit_code == 0, (
        f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
    )


@then("the context set command should fail")
def step_ctx_set_fail(context: Any) -> None:
    assert context.ctx_exit_code != 0, (
        f"Expected non-zero exit, got {context.ctx_exit_code}"
    )


@then("the context show command should succeed")
def step_ctx_show_ok(context: Any) -> None:
    assert context.ctx_exit_code == 0, (
        f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
    )


@then("the context show command should fail")
def step_ctx_show_fail(context: Any) -> None:
    assert context.ctx_exit_code != 0, (
        f"Expected non-zero exit, got {context.ctx_exit_code}"
    )


def _read_stored_policy(context: Any) -> Any:
    """Read the stored policy from the test DB."""
    from cleveragents.cli.commands.project_context import (
        _read_policy,
    )

    return _read_policy(context.ctx_session_factory, "local/ctx-app")


@then('the stored policy default view should include resource "{res}"')
def step_policy_include_res(context: Any, res: str) -> None:
    policy = _read_stored_policy(context)
    assert res in policy.default_view.include_resources, (
        f"{res} not in {policy.default_view.include_resources}"
    )


@then('the stored policy default view should exclude resource "{res}"')
def step_policy_exclude_res(context: Any, res: str) -> None:
    policy = _read_stored_policy(context)
    assert res in policy.default_view.exclude_resources, (
        f"{res} not in {policy.default_view.exclude_resources}"
    )


@then('the stored policy default view should include path "{path}"')
def step_policy_include_path(context: Any, path: str) -> None:
    policy = _read_stored_policy(context)
    assert path in policy.default_view.include_paths, (
        f"{path} not in {policy.default_view.include_paths}"
    )


@then('the stored policy default view should exclude path "{path}"')
def step_policy_exclude_path(context: Any, path: str) -> None:
    policy = _read_stored_policy(context)
    assert path in policy.default_view.exclude_paths, (
        f"{path} not in {policy.default_view.exclude_paths}"
    )


@then("the stored policy default view max file size should be {size:d}")
def step_policy_max_file_size(context: Any, size: int) -> None:
    policy = _read_stored_policy(context)
    assert policy.default_view.max_file_size == size, (
        f"Expected {size}, got {policy.default_view.max_file_size}"
    )


@then("the stored policy default view max total size should be {size:d}")
def step_policy_max_total_size(context: Any, size: int) -> None:
    policy = _read_stored_policy(context)
    assert policy.default_view.max_total_size == size, (
        f"Expected {size}, got {policy.default_view.max_total_size}"
    )


@then('the stored policy strategize view should include resource "{res}"')
def step_policy_strat_include_res(context: Any, res: str) -> None:
    policy = _read_stored_policy(context)
    assert policy.strategize_view is not None, "strategize_view is None"
    assert res in policy.strategize_view.include_resources


@then("the stored policy strategize view should be None")
def step_policy_strat_none(context: Any) -> None:
    policy = _read_stored_policy(context)
    assert policy.strategize_view is None, (
        f"Expected None, got {policy.strategize_view}"
    )


@then("the context inspect command should succeed")
def step_ctx_inspect_ok(context: Any) -> None:
    assert context.ctx_exit_code == 0, (
        f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
    )


@then("the context simulate command should succeed")
def step_ctx_simulate_ok(context: Any) -> None:
    assert context.ctx_exit_code == 0, (
        f"Expected exit 0, got {context.ctx_exit_code}. Output: {context.ctx_output}"
    )


@then('a NotImplementedError should be raised with message containing "{msg}"')
def step_not_implemented(context: Any, msg: str) -> None:
    assert context.ctx_error is not None, "No error was raised"
    assert isinstance(context.ctx_error, NotImplementedError), (
        f"Expected NotImplementedError, got {type(context.ctx_error)}"
    )
    assert msg in str(context.ctx_error), f"'{msg}' not in '{context.ctx_error}'"


@then('the resolved view should include resource "{res}"')
def step_resolved_view_include_res(context: Any, res: str) -> None:
    # The show command output contains the resolved view
    # We read the policy directly for verification
    policy = _read_stored_policy(context)
    # Determine which phase was queried from the output
    # For simplicity, check all possible resolved views
    found = False
    for phase in ["default", "strategize", "execute", "apply"]:
        resolved = policy.resolve_view(phase)
        if res in resolved.include_resources:
            found = True
            break
    assert found, f"Resource '{res}' not found in any resolved view"


@then("the context output should be valid JSON")
def step_ctx_output_valid_json(context: Any) -> None:
    try:
        json.loads(context.ctx_output.strip())
    except json.JSONDecodeError as exc:
        raise AssertionError(
            f"Output is not valid JSON: {exc}\nOutput: {context.ctx_output!r}"
        ) from exc
