"""Step definitions for project_show_resource_name.feature.

Regression tests for bug #2943: agents project show must display resource
names instead of raw ULIDs for linked resources.

Uses the same monkey-patching pattern as project_cli_commands_steps.py to
exercise the actual CLI command functions under coverage.
"""

from __future__ import annotations

from io import StringIO
from typing import Any
from unittest.mock import MagicMock

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

# ---------------------------------------------------------------------------
# Database / service bootstrap
# ---------------------------------------------------------------------------


class _SharedSession:
    """Wraps a real SQLAlchemy Session but makes ``close()`` a no-op."""

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

    def close(self) -> None:
        """No-op so the shared session stays usable across calls."""

    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 _init_rn_db(context: Any) -> None:
    """Create a fresh in-memory database and wire up repos + service."""
    from cleveragents.application.services.resource_registry_service import (
        ResourceRegistryService,
    )
    from cleveragents.infrastructure.database.models import Base
    from cleveragents.infrastructure.database.repositories import (
        NamespacedProjectRepository,
        ProjectResourceLinkRepository,
    )

    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 = _SharedSession(real_session)

    def _shared_factory() -> Any:
        return wrapper

    context._rn_engine = engine
    context._rn_factory = _shared_factory
    context._rn_project_repo = NamespacedProjectRepository(
        session_factory=_shared_factory
    )
    context._rn_link_repo = ProjectResourceLinkRepository(
        session_factory=_shared_factory
    )
    context._rn_resource_svc = ResourceRegistryService(session_factory=_shared_factory)
    context._rn_resource_svc.bootstrap_builtin_types()
    context._rn_output = ""
    context._rn_failed = False
    context._rn_last_resource_id: str | None = None


# ---------------------------------------------------------------------------
# Monkey-patching helpers
# ---------------------------------------------------------------------------

_ORIG_FNS: dict[str, Any] = {}


def _patch_project_mod(context: Any, registry_override: Any = None) -> None:
    """Monkey-patch the DI look-up helpers in project module."""
    import cleveragents.cli.commands.project as project_mod

    _ORIG_FNS["repo"] = project_mod._get_namespaced_project_repo
    _ORIG_FNS["link"] = project_mod._get_resource_link_repo
    _ORIG_FNS["svc"] = project_mod._get_resource_registry_service

    project_mod._get_namespaced_project_repo = lambda: context._rn_project_repo
    project_mod._get_resource_link_repo = lambda: context._rn_link_repo
    if registry_override is not None:
        project_mod._get_resource_registry_service = lambda: registry_override
    else:
        project_mod._get_resource_registry_service = lambda: context._rn_resource_svc


def _unpatch_project_mod() -> None:
    """Restore original helpers."""
    import cleveragents.cli.commands.project as project_mod

    project_mod._get_namespaced_project_repo = _ORIG_FNS["repo"]
    project_mod._get_resource_link_repo = _ORIG_FNS["link"]
    project_mod._get_resource_registry_service = _ORIG_FNS["svc"]


def _capture_show(
    context: Any,
    project_name: str,
    output_format: str = "rich",
    registry_override: Any = None,
) -> None:
    """Invoke the show CLI function, capturing output."""
    import contextlib

    import typer

    import cleveragents.cli.commands.project as project_mod

    buf = StringIO()
    fake_console = Console(file=buf, width=200, no_color=True)

    orig_console = project_mod.console
    orig_err = project_mod.err_console
    project_mod.console = fake_console
    project_mod.err_console = fake_console

    _patch_project_mod(context, registry_override=registry_override)
    failed = False
    try:
        with contextlib.redirect_stdout(buf):
            project_mod.show(project=project_name, output_format=output_format)
    except (SystemExit, typer.Exit, typer.Abort):
        failed = True
    except Exception:
        failed = True
    finally:
        project_mod.console = orig_console
        project_mod.err_console = orig_err
        _unpatch_project_mod()

    context._rn_output = buf.getvalue()
    context._rn_failed = failed


# ---------------------------------------------------------------------------
# Reusable helpers
# ---------------------------------------------------------------------------


def _create_project(context: Any, name: str) -> Any:
    from cleveragents.domain.models.core.project import (
        NamespacedProject,
        parse_namespaced_name,
    )

    parsed = parse_namespaced_name(name)
    proj = NamespacedProject(
        name=parsed.name,
        namespace=parsed.namespace,
        server=parsed.server,
    )
    context._rn_project_repo.create(proj)
    return proj


def _register_named_resource(context: Any, name: str) -> str:
    """Register a resource with a namespaced name and return its resource_id."""
    res = context._rn_resource_svc.register_resource(
        type_name="git-checkout",
        name=name,
        location="/tmp/test-resource",
        description=f"Test resource {name}",
    )
    context._rn_last_resource_id = res.resource_id
    return res.resource_id


def _register_unnamed_resource(context: Any) -> str:
    """Register a resource without a name (auto-discovered) and return its resource_id."""
    res = context._rn_resource_svc.register_resource(
        type_name="git-checkout",
        location="/tmp/unnamed-resource",
        description="Unnamed test resource",
    )
    context._rn_last_resource_id = res.resource_id
    return res.resource_id


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


@given("a fresh project-show-resource-name database is initialised")
def step_init_rn_db(context: Any) -> None:
    _init_rn_db(context)


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


@given('a project "{name}" exists in the resource-name DB')
def step_project_exists_rn(context: Any, name: str) -> None:
    _create_project(context, name)


@given(
    'a named resource "{res_name}" is linked to project "{proj_name}" in the resource-name DB'
)
def step_named_resource_linked_rn(context: Any, res_name: str, proj_name: str) -> None:
    rid = _register_named_resource(context, res_name)
    context._rn_link_repo.create_link(
        project_name=proj_name,
        resource_id=rid,
    )


@given('an unnamed resource is linked to project "{proj_name}" in the resource-name DB')
def step_unnamed_resource_linked_rn(context: Any, proj_name: str) -> None:
    rid = _register_unnamed_resource(context)
    context._rn_link_repo.create_link(
        project_name=proj_name,
        resource_id=rid,
    )


@given(
    'a named read-only resource "{res_name}" is linked to project "{proj_name}" in the resource-name DB'
)
def step_named_ro_resource_linked_rn(
    context: Any, res_name: str, proj_name: str
) -> None:
    rid = _register_named_resource(context, res_name)
    context._rn_link_repo.create_link(
        project_name=proj_name,
        resource_id=rid,
        read_only=True,
    )


@given(
    'resource "{res_name}" with alias "{alias}" is linked to project "{proj_name}" in the resource-name DB'
)
def step_named_aliased_resource_linked_rn(
    context: Any, res_name: str, alias: str, proj_name: str
) -> None:
    rid = _register_named_resource(context, res_name)
    context._rn_link_repo.create_link(
        project_name=proj_name,
        resource_id=rid,
        alias=alias,
    )


@given('a resource is linked to project "{proj_name}" in the resource-name DB')
def step_resource_linked_rn(context: Any, proj_name: str) -> None:
    """Link a resource (named or unnamed) to the project for fallback tests."""
    safe = proj_name.replace("/", "-")
    rid = _register_named_resource(context, f"local/res-{safe}")
    context._rn_link_repo.create_link(
        project_name=proj_name,
        resource_id=rid,
    )


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


@when('I show project "{name}" via the resource-name CLI')
def step_show_project_rn(context: Any, name: str) -> None:
    _capture_show(context, name)


@when('I show project "{name}" as JSON via the resource-name CLI')
def step_show_project_json_rn(context: Any, name: str) -> None:
    _capture_show(context, name, output_format="json")


@when('I show project "{name}" as YAML via the resource-name CLI')
def step_show_project_yaml_rn(context: Any, name: str) -> None:
    _capture_show(context, name, output_format="yaml")


@when('I show project "{name}" with registry unavailable via the resource-name CLI')
def step_show_project_registry_unavailable_rn(context: Any, name: str) -> None:
    """Simulate registry unavailability by using a mock that raises on every call."""
    broken_registry = MagicMock()
    broken_registry.show_resource.side_effect = Exception("Registry unavailable")
    _capture_show(context, name, registry_override=broken_registry)


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


@then('the resource-name show output should contain "{text}"')
def step_rn_output_contains(context: Any, text: str) -> None:
    assert text.lower() in context._rn_output.lower(), (
        f"Expected output to contain '{text}', got:\n{context._rn_output}"
    )


@then("the resource-name show output should contain the resource ULID")
def step_rn_output_contains_ulid(context: Any) -> None:
    rid = context._rn_last_resource_id
    assert rid is not None, "No resource ULID stored in context"
    assert rid in context._rn_output, (
        f"Expected output to contain ULID '{rid}', got:\n{context._rn_output}"
    )


@then("the resource-name show exit code should be 0")
def step_rn_exit_code_zero(context: Any) -> None:
    assert not context._rn_failed, (
        f"Expected command to succeed but it failed. Output:\n{context._rn_output}"
    )


@then("the resource-name show exit code should not be 0")
def step_rn_exit_code_nonzero(context: Any) -> None:
    assert context._rn_failed, (
        f"Expected command to fail but it succeeded. Output:\n{context._rn_output}"
    )
