"""Step definitions for context_cli_wiring.feature.

Tests the ``agents project context set/show/inspect/simulate``
CLI commands wired to the ACMS pipeline using an in-memory
SQLite database and a real ContextTierService instance.
"""

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  # type: ignore[import-untyped]
from sqlalchemy.orm import Session, sessionmaker  # type: ignore[import-untyped]

from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import (
    ContextTier,
    TieredFragment,
)

# ------------------------------------------------------------------
# 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_wiring_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 context wiring in-memory database is initialized")
def step_init_wiring_db(context: Any) -> None:
    from cleveragents.infrastructure.database.repositories import (
        NamespacedProjectRepository,
    )

    engine, session_factory = _make_wiring_session_factory(context)
    context.w_engine = engine
    context.w_session_factory = session_factory
    context.w_project_repo = NamespacedProjectRepository(
        session_factory=session_factory
    )
    context.w_tier_service = ContextTierService()
    context.w_output = ""
    context.w_exit_code = 0
    context.w_error = None


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


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


def _mock_wiring_container(context: Any) -> MagicMock:
    """Build a mock container wired to the test DB and tier service."""
    mock_container = MagicMock()
    mock_container.namespaced_project_repo.return_value = context.w_project_repo
    mock_container.session_factory.return_value = context.w_session_factory
    mock_container.context_tier_service.return_value = context.w_tier_service
    return mock_container


def _test_format_output(data, format_type):
    """Test-friendly format_output: captures stdout writes and returns the string."""
    import sys
    from io import StringIO

    from cleveragents.cli.formatting import format_output

    buf = StringIO()
    old = sys.stdout
    sys.stdout = buf
    try:
        result = format_output(data, format_type)
    finally:
        sys.stdout = old
    return result or buf.getvalue().rstrip("\n")


def _run_wired(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_wiring_container(context)
    buf = StringIO()
    test_console = RichConsole(
        file=buf,
        no_color=True,
        highlight=False,
        force_terminal=False,
        width=500,
        soft_wrap=True,
    )

    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.w_exit_code = 0
        except typer.Exit as exc:
            context.w_exit_code = exc.exit_code
        except typer.Abort:
            context.w_exit_code = 1

    context.w_output = buf.getvalue()


# ------------------------------------------------------------------
# context set with ACMS options
# ------------------------------------------------------------------


@when('I run wired context set on "{project}" with hot-max-tokens {val:d}')
def step_wired_set_hot_max_tokens(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, hot_max_tokens=val)


@when('I run wired context set on "{project}" with warm-max-decisions {val:d}')
def step_wired_set_warm_max_decisions(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, warm_max_decisions=val)


@when('I run wired context set on "{project}" with cold-max-decisions {val:d}')
def step_wired_set_cold_max_decisions(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, cold_max_decisions=val)


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

    _run_wired(context, context_set, project=project, temporal_scope=scope)


@when('I run wired context set on "{project}" with no-auto-refresh')
def step_wired_set_no_auto_refresh(context: Any, project: str) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, auto_refresh=False)


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

    _run_wired(context, context_set, project=project, summarize=False)


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

    _run_wired(context, context_set, project=project, strategy=[strategy])


@when('I run wired context set on "{project}" with skeleton-ratio {val:g}')
def step_wired_set_skeleton_ratio(context: Any, project: str, val: float) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, skeleton_ratio=val)


@when('I run wired context set on "{project}" with default-breadth {val:d}')
def step_wired_set_default_breadth(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, default_breadth=val)


@when('I run wired context set on "{project}" with default-depth {val:d}')
def step_wired_set_default_depth(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, default_depth=str(val))


@when('I run wired context set on "{project}" with query-limit {val:d}')
def step_wired_set_query_limit(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, query_limit=val)


@when('I run wired context set on "{project}" with summary-max-tokens {val:d}')
def step_wired_set_summary_max_tokens(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, summary_max_tokens=val)


# ------------------------------------------------------------------
# context show with ACMS config
# ------------------------------------------------------------------


@given('I have set ACMS config on "{project}" with hot-max-tokens {val:d}')
def step_given_acms_config(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_set

    _run_wired(context, context_set, project=project, hot_max_tokens=val)


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

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


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

    _run_wired(context, context_show, project=project, view=view, output_format=fmt)


# ------------------------------------------------------------------
# context inspect
# ------------------------------------------------------------------


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

    _run_wired(context, context_inspect, project=project)


@when('I run wired inspect for project "{project}" using format "{fmt}"')
def step_wired_inspect_format(context: Any, project: str, fmt: str) -> None:
    from cleveragents.cli.commands.project_context import context_inspect

    _run_wired(context, context_inspect, project=project, output_format=fmt)


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

    _run_wired(context, context_inspect, project=project, view=view)


@given('the tier service has a hot fragment "{fid}" for project "{project}"')
def step_given_hot_fragment(context: Any, fid: str, project: str) -> None:
    frag = TieredFragment(
        fragment_id=fid,
        content="test content for " + fid,
        tier=ContextTier.HOT,
        project_name=project,
        token_count=50,
    )
    context.w_tier_service.store(frag)


@given('the tier service has a warm fragment "{fid}" for project "{project}"')
def step_given_warm_fragment(context: Any, fid: str, project: str) -> None:
    frag = TieredFragment(
        fragment_id=fid,
        content="test content for " + fid,
        tier=ContextTier.WARM,
        project_name=project,
        token_count=30,
    )
    context.w_tier_service.store(frag)


@given(
    'the tier service has a hot fragment "{fid}" for project'
    ' "{project}" sized at {tokens:d} tokens'
)
def step_given_hot_fragment_tokens(
    context: Any, fid: str, project: str, tokens: int
) -> None:
    frag = TieredFragment(
        fragment_id=fid,
        content="test content for simulation " + fid,
        tier=ContextTier.HOT,
        project_name=project,
        token_count=tokens,
    )
    context.w_tier_service.store(frag)


@given(
    'a focused hot fragment "{fid}" with resource "{resource}"'
    " and {tokens:d} tokens exists"
    ' for project "{project}"'
)
def step_given_focused_hot_fragment(
    context: Any, fid: str, resource: str, tokens: int, project: str
) -> None:
    frag = TieredFragment(
        fragment_id=fid,
        content="test content for " + fid,
        tier=ContextTier.HOT,
        project_name=project,
        resource_id=resource,
        token_count=tokens,
    )
    context.w_tier_service.store(frag)


# ------------------------------------------------------------------
# context simulate
# ------------------------------------------------------------------


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

    _run_wired(context, context_simulate, project=project)


@when('I run wired simulate for project "{project}" using format "{fmt}"')
def step_wired_simulate_format(context: Any, project: str, fmt: str) -> None:
    from cleveragents.cli.commands.project_context import context_simulate

    _run_wired(context, context_simulate, project=project, output_format=fmt)


@when('I run wired simulate for project "{project}" using budget {val:d}')
def step_wired_simulate_budget(context: Any, project: str, val: int) -> None:
    from cleveragents.cli.commands.project_context import context_simulate

    _run_wired(context, context_simulate, project=project, budget=val)


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

    _run_wired(context, context_simulate, project=project, view=view)


@when('I run wired simulate for project "{project}" using strategy "{strat}"')
def step_wired_simulate_strategy(context: Any, project: str, strat: str) -> None:
    from cleveragents.cli.commands.project_context import context_simulate

    _run_wired(context, context_simulate, project=project, strategy_hint=[strat])


@when(
    'I run wired simulate for project "{project}"'
    ' focusing on "{focus}" in format "{fmt}"'
)
def step_wired_simulate_focus_format(
    context: Any, project: str, focus: str, fmt: str
) -> None:
    from cleveragents.cli.commands.project_context import context_simulate

    _run_wired(
        context,
        context_simulate,
        project=project,
        focus=[focus],
        output_format=fmt,
    )


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


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


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


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


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


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


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


# ------------------------------------------------------------------
# Then assertions — ACMS config
# ------------------------------------------------------------------


def _read_stored_acms_config(context: Any) -> dict:
    """Read the ACMS config from the test DB."""
    from cleveragents.cli.commands.project_context import _read_acms_config

    return _read_acms_config(context.w_session_factory, "local/wire-app")


@then("the stored ACMS config hot_max_tokens should be {val:d}")
def step_acms_hot_max_tokens(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["hot_max_tokens"] == val, (
        f"Expected {val}, got {acms['hot_max_tokens']}"
    )


@then("the stored ACMS config warm_max_decisions should be {val:d}")
def step_acms_warm_max_decisions(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["warm_max_decisions"] == val, (
        f"Expected {val}, got {acms['warm_max_decisions']}"
    )


@then("the stored ACMS config cold_max_decisions should be {val:d}")
def step_acms_cold_max_decisions(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["cold_max_decisions"] == val, (
        f"Expected {val}, got {acms['cold_max_decisions']}"
    )


@then('the stored ACMS config temporal_scope should be "{val}"')
def step_acms_temporal_scope(context: Any, val: str) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["temporal_scope"] == val, (
        f"Expected {val}, got {acms['temporal_scope']}"
    )


@then("the stored ACMS config auto_refresh should be false")
def step_acms_auto_refresh_false(context: Any) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["auto_refresh"] is False, f"Expected False, got {acms['auto_refresh']}"


@then("the stored ACMS config summarize should be false")
def step_acms_summarize_false(context: Any) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["summarize"] is False, f"Expected False, got {acms['summarize']}"


@then('the stored ACMS config strategies should contain "{strat}"')
def step_acms_strategies_contain(context: Any, strat: str) -> None:
    acms = _read_stored_acms_config(context)
    assert strat in acms.get("strategies", []), (
        f"Expected {strat} in {acms.get('strategies', [])}"
    )


@then("the stored ACMS config skeleton_ratio should be {val:g}")
def step_acms_skeleton_ratio(context: Any, val: float) -> None:
    acms = _read_stored_acms_config(context)
    assert abs(acms["skeleton_ratio"] - val) < 1e-9, (
        f"Expected {val}, got {acms['skeleton_ratio']}"
    )


@then("the stored ACMS config default_breadth should be {val:d}")
def step_acms_default_breadth(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["default_breadth"] == val, (
        f"Expected {val}, got {acms['default_breadth']}"
    )


@then("the stored ACMS config default_depth should be {val:d}")
def step_acms_default_depth(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["default_depth"] == val, f"Expected {val}, got {acms['default_depth']}"


@then("the stored ACMS config query_limit should be {val:d}")
def step_acms_query_limit(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["query_limit"] == val, f"Expected {val}, got {acms['query_limit']}"


@then("the stored ACMS config summary_max_tokens should be {val:d}")
def step_acms_summary_max_tokens(context: Any, val: int) -> None:
    acms = _read_stored_acms_config(context)
    assert acms["summary_max_tokens"] == val, (
        f"Expected {val}, got {acms['summary_max_tokens']}"
    )


# ------------------------------------------------------------------
# Then assertions — JSON output
# ------------------------------------------------------------------


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


@then('the wired JSON output should contain key "{key}"')
def step_wired_json_has_key(context: Any, key: str) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    assert key in data, f"Key '{key}' not found in JSON output: {list(data.keys())}"


@then("the wired JSON project_fragments total should be {val:d}")
def step_wired_json_fragments_total(context: Any, val: int) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    total = data.get("project_fragments", {}).get("total", 0)
    assert total == val, f"Expected {val} fragments, got {total}"


@then("the wired JSON total_tokens should be greater than 0")
def step_wired_json_total_tokens_positive(context: Any) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    total = data.get("total_tokens", 0)
    assert total > 0, f"Expected total_tokens > 0, got {total}"


@then("the wired JSON tier_metrics hot_count should be {val:d}")
def step_wired_json_tier_metrics_hot_count(context: Any, val: int) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    hot_count = data.get("tier_metrics", {}).get("hot_count", -1)
    assert hot_count == val, f"Expected tier_metrics.hot_count={val}, got {hot_count}"


@then("the wired JSON fragment_count should be {val:d}")
def step_wired_json_fragment_count(context: Any, val: int) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    count = data.get("fragment_count", -1)
    assert count == val, f"Expected fragment_count={val}, got {count}"


@then("the wired JSON total_tokens should be {val:d}")
def step_wired_json_total_tokens_exact(context: Any, val: int) -> None:
    data = getattr(context, "w_parsed_json", None)
    if data is None:
        data = json.loads(context.w_output.strip())
        context.w_parsed_json = data
    total = data.get("total_tokens", -1)
    assert total == val, f"Expected total_tokens={val}, got {total}"
