"""Step definitions for ``project_context_cov3.feature``.

Targets uncovered lines 156, 175, 350, 674-675, 678, 864-869 in
``src/cleveragents/cli/commands/project_context.py``.

Uses the ``pccov3`` prefix on all step text to avoid Behave
AmbiguousStep collisions with existing step files.
"""

from __future__ import annotations

import json as _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, text
from sqlalchemy.orm import Session, sessionmaker

# ------------------------------------------------------------------
# Session wrapper - prevents close() from destroying the shared
# in-memory session; rollback() resets dirty state between calls.
# ------------------------------------------------------------------


class _SafeSession:
    """Thin wrapper: ``close()`` → ``rollback()`` instead of destroy."""

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

    def close(self) -> None:
        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 + runner
# ------------------------------------------------------------------


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

    mc = MagicMock()
    mc.namespaced_project_repo.return_value = context.pccov3_project_repo
    mc.session_factory.return_value = context.pccov3_session_factory
    if not hasattr(context, "pccov3_tier_service"):
        context.pccov3_tier_service = ContextTierService()
    mc.context_tier_service.return_value = context.pccov3_tier_service
    return mc


def _run(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

    mc = _mock_container(context)
    buf = StringIO()
    test_console = RichConsole(
        file=buf,
        no_color=True,
        highlight=False,
        force_terminal=False,
        width=500,
        soft_wrap=True,
    )

    def _test_format_output(data: Any, format_type: Any) -> Any:
        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=mc,
        ),
        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.pccov3_exit_code = 0
        except typer.Exit as exc:
            context.pccov3_exit_code = exc.exit_code
        except typer.Abort:
            context.pccov3_exit_code = 1

    context.pccov3_output = buf.getvalue()


# ------------------------------------------------------------------
# Helper: seed raw policy blobs into the DB
# ------------------------------------------------------------------


def _seed_policy_blob(context: Any, ns: str, blob: dict) -> None:
    """Write a raw JSON blob into ns_projects.context_policy_json."""
    session = context.pccov3_session_factory()
    session.execute(
        text(
            "UPDATE ns_projects SET context_policy_json = :blob "
            "WHERE namespaced_name = :ns"
        ),
        {"blob": _json.dumps(blob), "ns": ns},
    )
    session.commit()


# ------------------------------------------------------------------
# Background steps
# ------------------------------------------------------------------


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

    engine, sf = _make_session_factory(context)
    context.pccov3_engine = engine
    context.pccov3_session_factory = sf
    context.pccov3_project_repo = NamespacedProjectRepository(session_factory=sf)
    context.pccov3_output = ""
    context.pccov3_exit_code = 0
    context.pccov3_error = None


@given('a project "{name}" exists for pccov3')
def step_pccov3_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.pccov3_project_repo.create(proj)


# ------------------------------------------------------------------
# Given steps
# ------------------------------------------------------------------


@given('the pccov3 tier service has a fragment with long content for "{project}"')
def step_pccov3_seed_long_content_fragment(context: Any, project: str) -> None:
    """Seed the tier service with a fragment whose content exceeds 100 chars.

    This ensures line 350 (content truncation) is exercised in
    ``_simulate_context_assembly``.
    """
    from cleveragents.application.services.context_tiers import ContextTierService
    from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment

    svc = ContextTierService()
    svc.store(
        TieredFragment(
            fragment_id="pccov3-long-frag",
            content="X" * 200,  # 200 chars, well over the 100-char threshold
            tier=ContextTier.HOT,
            resource_id="res:pccov3-long",
            project_name=project,
            token_count=50,
            access_count=3,
        )
    )
    context.pccov3_tier_service = svc


@given(
    'I pccov3 seed execution environment "{env}" and priority "{priority}" for "{project}"'
)
def step_pccov3_seed_ee_and_eep(
    context: Any, env: str, priority: str, project: str
) -> None:
    """Seed a policy blob with both execution_environment and execution_env_priority.

    This ensures lines 864-869 are exercised when context_show renders
    the rich output with both ee and eep present.
    """
    from cleveragents.cli.commands.project_context import _default_acms_config

    blob = {
        "default_view": {
            "include_resources": [],
            "exclude_resources": [],
            "include_paths": [],
            "exclude_paths": [],
            "max_file_size": None,
            "max_total_size": None,
        },
        "acms_config": _default_acms_config(),
        "execution_environment": env,
        "execution_env_priority": priority,
    }
    _seed_policy_blob(context, project, blob)


@given('I pccov3 seed execution environment "{env}" without priority for "{project}"')
def step_pccov3_seed_ee_only(context: Any, env: str, project: str) -> None:
    """Seed a policy blob with execution_environment only (no priority)."""
    from cleveragents.cli.commands.project_context import _default_acms_config

    blob = {
        "default_view": {
            "include_resources": [],
            "exclude_resources": [],
            "include_paths": [],
            "exclude_paths": [],
            "max_file_size": None,
            "max_total_size": None,
        },
        "acms_config": _default_acms_config(),
        "execution_environment": env,
    }
    _seed_policy_blob(context, project, blob)


@given(
    'I pccov3 seed priority "{priority}" without execution environment for "{project}"'
)
def step_pccov3_seed_eep_only(context: Any, priority: str, project: str) -> None:
    """Seed a policy blob with execution_env_priority only (no ee)."""
    from cleveragents.cli.commands.project_context import _default_acms_config

    blob = {
        "default_view": {
            "include_resources": [],
            "exclude_resources": [],
            "include_paths": [],
            "exclude_paths": [],
            "max_file_size": None,
            "max_total_size": None,
        },
        "acms_config": _default_acms_config(),
        "execution_env_priority": priority,
    }
    _seed_policy_blob(context, project, blob)


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


@when('I pccov3 read the policy for "{project}"')
def step_pccov3_read_policy(context: Any, project: str) -> None:
    """Directly call _read_policy to exercise line 156."""
    from cleveragents.cli.commands.project_context import _read_policy

    context.pccov3_policy = _read_policy(context.pccov3_session_factory, project)


@when('I pccov3 read the ACMS config for "{project}"')
def step_pccov3_read_acms(context: Any, project: str) -> None:
    """Directly call _read_acms_config to exercise line 175."""
    from cleveragents.cli.commands.project_context import _read_acms_config

    context.pccov3_acms = _read_acms_config(context.pccov3_session_factory, project)


@when('I pccov3 run simulate_context_assembly for "{project}"')
def step_pccov3_simulate_assembly(context: Any, project: str) -> None:
    """Run _simulate_context_assembly to exercise line 350 (content truncation)."""
    from cleveragents.cli.commands.project_context import (
        _default_acms_config,
        _simulate_context_assembly,
    )

    # Patch _get_context_tier_service to return our test tier service
    patcher = patch(
        "cleveragents.cli.commands.project_context._get_context_tier_service",
        return_value=context.pccov3_tier_service,
    )
    patcher.start()
    context.add_cleanup(patcher.stop)

    try:
        context.pccov3_assembled = _simulate_context_assembly(
            project_name=project,
            acms_config=_default_acms_config(),
            budget_tokens=10000,  # large budget so fragment is included
        )
    except Exception as exc:
        context.pccov3_error = exc


@when('I pccov3 run context set on "{project}" with depth-gradient "{gradient}"')
def step_pccov3_set_depth_gradient(context: Any, project: str, gradient: str) -> None:
    """Run context_set with a depth-gradient to exercise lines 674-678."""
    from cleveragents.cli.commands.project_context import context_set

    _run(
        context,
        context_set,
        project=project,
        view="default",
        depth_gradient=[gradient],
    )


@when('I pccov3 run context show on "{project}" without view')
def step_pccov3_show_no_view(context: Any, project: str) -> None:
    """Run context_show without a view to exercise lines 864-869."""
    from cleveragents.cli.commands.project_context import context_show

    _run(context, context_show, project=project)


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


@then("the pccov3 policy should be a default ProjectContextPolicy")
def step_pccov3_assert_default_policy(context: Any) -> None:
    from cleveragents.domain.models.core.context_policy import ProjectContextPolicy

    default = ProjectContextPolicy()
    assert context.pccov3_policy.default_view == default.default_view, (
        f"Expected default view {default.default_view}, "
        f"got {context.pccov3_policy.default_view}"
    )


@then("the pccov3 policy strategize_view should be None")
def step_pccov3_assert_strat_none(context: Any) -> None:
    assert context.pccov3_policy.strategize_view is None, (
        f"Expected None, got {context.pccov3_policy.strategize_view}"
    )


@then("the pccov3 policy execute_view should be None")
def step_pccov3_assert_exec_none(context: Any) -> None:
    assert context.pccov3_policy.execute_view is None, (
        f"Expected None, got {context.pccov3_policy.execute_view}"
    )


@then("the pccov3 policy apply_view should be None")
def step_pccov3_assert_apply_none(context: Any) -> None:
    assert context.pccov3_policy.apply_view is None, (
        f"Expected None, got {context.pccov3_policy.apply_view}"
    )


@then("the pccov3 ACMS config should equal the default ACMS config")
def step_pccov3_assert_default_acms(context: Any) -> None:
    from cleveragents.cli.commands.project_context import _default_acms_config

    expected = _default_acms_config()
    assert context.pccov3_acms == expected, (
        f"Expected {expected}, got {context.pccov3_acms}"
    )


@then('the pccov3 assembled fragments should have truncated content ending with "..."')
def step_pccov3_assert_truncated_content(context: Any) -> None:
    assert context.pccov3_error is None, f"Unexpected error: {context.pccov3_error}"
    assembled = context.pccov3_assembled
    assert len(assembled.fragments) > 0, "No fragments in assembled context"

    # The fragment content is stored in the ContextFragment's content field.
    # In the simulation, content > 100 chars gets truncated to [:100] + "..."
    frag = assembled.fragments[0]
    assert frag.content.endswith("..."), (
        f"Expected content to end with '...', got: {frag.content!r}"
    )
    # The truncated content should be 103 chars (100 + "...")
    assert len(frag.content) == 103, f"Expected 103 chars, got {len(frag.content)}"


@then("the pccov3 command should have exit code {code:d}")
def step_pccov3_assert_exit_code(context: Any, code: int) -> None:
    assert context.pccov3_exit_code == code, (
        f"Expected exit code {code}, got {context.pccov3_exit_code}. "
        f"Output: {context.pccov3_output}"
    )


@then("the pccov3 command should succeed")
def step_pccov3_assert_success(context: Any) -> None:
    assert context.pccov3_exit_code == 0, (
        f"Expected exit 0, got {context.pccov3_exit_code}. "
        f"Output: {context.pccov3_output}"
    )


@then('the pccov3 output should contain "{text}"')
def step_pccov3_assert_output_contains(context: Any, text: str) -> None:
    assert text in context.pccov3_output, (
        f"Expected '{text}' in output, got: {context.pccov3_output!r}"
    )


@then("the pccov3 ACMS config skeleton_ratio should be {expected:g}")
def step_pccov3_assert_skeleton_ratio(context: Any, expected: float) -> None:
    from cleveragents.cli.commands.project_context import _DEFAULT_SKELETON_RATIO

    assert context.pccov3_acms["skeleton_ratio"] == expected, (
        f"Expected skeleton_ratio {expected}, got {context.pccov3_acms['skeleton_ratio']}"
    )
    assert expected == _DEFAULT_SKELETON_RATIO, (
        f"_DEFAULT_SKELETON_RATIO is {_DEFAULT_SKELETON_RATIO}, expected {expected}"
    )
