"""Step definitions for namespaced_project_service.feature.

Tests the NamespacedProjectService application service which provides
a clean facade over the domain layer for the CLI layer, enforcing
Architectural Invariant #3: CLI → AppService → Domain.
"""

from __future__ import annotations

import inspect
from typing import Any

from behave import given, then, use_step_matcher, when
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

# ---------------------------------------------------------------------------
# Shared session wrapper (prevents premature session close)
# ---------------------------------------------------------------------------


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

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

    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 _make_nps_session_factory(context: Any) -> Any:
    """Create an in-memory SQLite database and return a session factory."""
    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 _factory


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


@given("a NamespacedProjectService with an in-memory database")
def step_init_nps(context: Any) -> None:
    from cleveragents.application.services.namespaced_project_service import (
        NamespacedProjectService,
    )
    from cleveragents.infrastructure.database.repositories import (
        NamespacedProjectRepository,
    )

    session_factory = _make_nps_session_factory(context)
    repo = NamespacedProjectRepository(session_factory=session_factory)
    context.nps = NamespacedProjectService(project_repo=repo)
    context.nps_repo = repo
    context.nps_parsed = None
    context.nps_project = None
    context.nps_project_list = []
    context.nps_dict = {}
    context.nps_delete_result = None
    context.nps_raised_exc = None


# ---------------------------------------------------------------------------
# Given helpers
# ---------------------------------------------------------------------------


@given('a project "{name}" already exists in the service')
def step_nps_project_exists(context: Any, name: str) -> None:
    context.nps.create_project(name=name)


# ---------------------------------------------------------------------------
# Parse / validate steps
# ---------------------------------------------------------------------------


@when('I parse the project name "{name}"')
def step_nps_parse_name(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_parsed = context.nps.parse_project_name(name)
    except Exception as exc:
        context.nps_raised_exc = exc


@when('I parse the invalid project name "{name}"')
def step_nps_parse_invalid_name(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_parsed = context.nps.parse_project_name(name)
    except ValueError as exc:
        context.nps_raised_exc = exc


@when('I validate the project name "{name}"')
def step_nps_validate_name(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_parsed = context.nps.validate_project_name(name)
    except Exception as exc:
        context.nps_raised_exc = exc


@when('I validate the invalid project name "{name}"')
def step_nps_validate_invalid_name(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_parsed = context.nps.validate_project_name(name)
    except ValueError as exc:
        context.nps_raised_exc = exc


# ---------------------------------------------------------------------------
# Create steps
# ---------------------------------------------------------------------------


use_step_matcher("re")


@when(r'I create a project named "(?P<name>[^"]+)" via the service')
def step_nps_create_project(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.create_project(name=name)
    except Exception as exc:
        context.nps_raised_exc = exc


@when(
    r'I create a project named "(?P<name>[^"]+)"'
    r' with description "(?P<desc>[^"]+)" via the service'
)
def step_nps_create_project_with_desc(context: Any, name: str, desc: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.create_project(name=name, description=desc)
    except Exception as exc:
        context.nps_raised_exc = exc


@when(r'I attempt to create a project named "(?P<name>[^"]+)" via the service')
def step_nps_attempt_create_project(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.create_project(name=name)
    except Exception as exc:
        context.nps_raised_exc = exc


@when(
    r'I attempt to create a duplicate project named "(?P<name>[^"]+)" via the service'
)
def step_nps_attempt_create_duplicate(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.create_project(name=name)
    except Exception as exc:
        context.nps_raised_exc = exc


use_step_matcher("parse")


# ---------------------------------------------------------------------------
# Get steps
# ---------------------------------------------------------------------------


@when('I get the project "{name}" via the service')
def step_nps_get_project(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.get_project(name)
    except Exception as exc:
        context.nps_raised_exc = exc


@when('I attempt to get the project "{name}" via the service')
def step_nps_attempt_get_project(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project = context.nps.get_project(name)
    except Exception as exc:
        context.nps_raised_exc = exc


# ---------------------------------------------------------------------------
# List steps
# ---------------------------------------------------------------------------


@when("I list all projects via the service")
def step_nps_list_all_projects(context: Any) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project_list = context.nps.list_projects()
    except Exception as exc:
        context.nps_raised_exc = exc


@when('I list projects with namespace "{ns}" via the service')
def step_nps_list_projects_ns(context: Any, ns: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_project_list = context.nps.list_projects(namespace=ns)
    except Exception as exc:
        context.nps_raised_exc = exc


# ---------------------------------------------------------------------------
# Delete steps
# ---------------------------------------------------------------------------


@when('I delete the project "{name}" via the service')
def step_nps_delete_project(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        context.nps_delete_result = context.nps.delete_project(name)
    except Exception as exc:
        context.nps_raised_exc = exc


# ---------------------------------------------------------------------------
# project_to_dict steps
# ---------------------------------------------------------------------------


@when('I convert the project "{name}" to a dict via the service')
def step_nps_project_to_dict(context: Any, name: str) -> None:
    context.nps_raised_exc = None
    try:
        project = context.nps.get_project(name)
        context.nps_dict = context.nps.project_to_dict(project)
    except Exception as exc:
        context.nps_raised_exc = exc


# ---------------------------------------------------------------------------
# Architectural invariant step
# ---------------------------------------------------------------------------


@when("I inspect the project CLI create command source")
def step_nps_inspect_cli_source(context: Any) -> None:
    import cleveragents.cli.commands.project as project_module

    context.nps_cli_source = inspect.getsource(project_module)


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


@then('the NPS parsed namespace should be "{ns}"')
def step_nps_assert_parsed_ns(context: Any, ns: str) -> None:
    assert context.nps_parsed is not None, "No parsed result available"
    assert context.nps_parsed.namespace == ns, (
        f"Expected namespace '{ns}', got '{context.nps_parsed.namespace}'"
    )


@then('the NPS parsed name should be "{name}"')
def step_nps_assert_parsed_name(context: Any, name: str) -> None:
    assert context.nps_parsed is not None, "No parsed result available"
    assert context.nps_parsed.name == name, (
        f"Expected name '{name}', got '{context.nps_parsed.name}'"
    )


@then("the NPS parsed server should be None")
def step_nps_assert_parsed_server_none(context: Any) -> None:
    assert context.nps_parsed is not None, "No parsed result available"
    assert context.nps_parsed.server is None, (
        f"Expected server to be None, got '{context.nps_parsed.server}'"
    )


@then('the NPS parsed server should be "{server}"')
def step_nps_assert_parsed_server(context: Any, server: str) -> None:
    assert context.nps_parsed is not None, "No parsed result available"
    assert context.nps_parsed.server == server, (
        f"Expected server '{server}', got '{context.nps_parsed.server}'"
    )


@then("the NPS should raise a ValueError")
def step_nps_assert_value_error(context: Any) -> None:
    assert context.nps_raised_exc is not None, (
        "Expected a ValueError but none was raised"
    )
    assert isinstance(context.nps_raised_exc, ValueError), (
        f"Expected ValueError, got {type(context.nps_raised_exc).__name__}: "
        f"{context.nps_raised_exc}"
    )


@then("a database error should be raised")
def step_nps_assert_db_error(context: Any) -> None:
    assert context.nps_raised_exc is not None, (
        "Expected a database error but none was raised"
    )


@then("a NotFoundError should be raised")
def step_nps_assert_not_found_error(context: Any) -> None:
    from cleveragents.core.exceptions import NotFoundError

    assert context.nps_raised_exc is not None, (
        "Expected a NotFoundError but none was raised"
    )
    assert isinstance(context.nps_raised_exc, NotFoundError), (
        f"Expected NotFoundError, got {type(context.nps_raised_exc).__name__}: "
        f"{context.nps_raised_exc}"
    )


@then("the validation should succeed")
def step_nps_assert_validation_success(context: Any) -> None:
    assert context.nps_raised_exc is None, (
        f"Expected validation to succeed but got: {context.nps_raised_exc}"
    )
    assert context.nps_parsed is not None, "Expected a parsed result"


@then('the service should return a project with namespaced name "{name}"')
def step_nps_assert_project_namespaced_name(context: Any, name: str) -> None:
    assert context.nps_project is not None, "No project returned from service"
    assert context.nps_project.namespaced_name == name, (
        f"Expected namespaced_name '{name}', "
        f"got '{context.nps_project.namespaced_name}'"
    )


@then("the project should be persisted in the database")
def step_nps_assert_project_persisted(context: Any) -> None:
    assert context.nps_project is not None, "No project to check"
    fetched = context.nps_repo.get(context.nps_project.namespaced_name)
    assert fetched is not None, (
        f"Project '{context.nps_project.namespaced_name}' not found in database"
    )


@then('the NPS project description should be "{desc}"')
def step_nps_assert_project_desc(context: Any, desc: str) -> None:
    assert context.nps_project is not None, "No project returned from service"
    assert context.nps_project.description == desc, (
        f"Expected description '{desc}', got '{context.nps_project.description}'"
    )


@then('the service project list should contain "{name}"')
def step_nps_assert_list_contains(context: Any, name: str) -> None:
    names = [p.namespaced_name for p in context.nps_project_list]
    assert name in names, f"Expected project list to contain '{name}', got: {names}"


@then('the service project list should not contain "{name}"')
def step_nps_assert_list_not_contains(context: Any, name: str) -> None:
    names = [p.namespaced_name for p in context.nps_project_list]
    assert name not in names, (
        f"Expected project list NOT to contain '{name}', got: {names}"
    )


@then("the service project list should be empty")
def step_nps_assert_list_empty(context: Any) -> None:
    assert len(context.nps_project_list) == 0, (
        f"Expected empty project list, got: {context.nps_project_list}"
    )


@then("the delete should return True")
def step_nps_assert_delete_true(context: Any) -> None:
    assert context.nps_delete_result is True, (
        f"Expected delete to return True, got: {context.nps_delete_result}"
    )


@then("the delete should return False")
def step_nps_assert_delete_false(context: Any) -> None:
    assert context.nps_delete_result is False, (
        f"Expected delete to return False, got: {context.nps_delete_result}"
    )


@then('the project "{name}" should not exist in the service')
def step_nps_assert_project_not_exists(context: Any, name: str) -> None:
    from cleveragents.core.exceptions import NotFoundError

    try:
        context.nps.get_project(name)
        raise AssertionError(f"Project '{name}' should not exist but was found")
    except NotFoundError:
        pass


@then('the dict should have key "{key}"')
def step_nps_assert_dict_has_key(context: Any, key: str) -> None:
    assert key in context.nps_dict, (
        f"Expected dict to have key '{key}', keys: {list(context.nps_dict.keys())}"
    )


@then('the dict value for "{key}" should be "{value}"')
def step_nps_assert_dict_value(context: Any, key: str, value: str) -> None:
    assert key in context.nps_dict, f"Key '{key}' not found in dict"
    assert str(context.nps_dict[key]) == value, (
        f"Expected dict['{key}'] == '{value}', got '{context.nps_dict[key]}'"
    )


@then('it should not contain a direct import of "{module_path}"')
def step_nps_assert_no_direct_import(context: Any, module_path: str) -> None:
    source = context.nps_cli_source
    # Check for direct import patterns like:
    # "from cleveragents.domain.models.core.project import"
    import_pattern = f"from {module_path} import"
    assert import_pattern not in source, (
        f"CLI source still contains direct domain import: '{import_pattern}'"
    )
