"""Step definitions for resource_project_services.feature."""

from __future__ import annotations

import os
import tempfile
from typing import Any

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

from cleveragents.application.services.resource_registry_service import (
    ResourceRegistryService,
)
from cleveragents.domain.models.core.project import NamespacedProject
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    NamespacedProjectRepository,
    ProjectResourceLinkRepository,
)


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

    This lets repos and services that call ``session.close()`` in ``finally``
    blocks coexist when sharing a single in-memory SQLite session.  All other
    calls are proxied transparently.
    """

    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_session_factory(context: Any) -> tuple[Any, Any]:
    """Create an in-memory SQLite database and return (engine, session_factory).

    Returns a session factory that always returns the **same** unclosable
    session so that repositories using the session-factory pattern (each method
    opens its own session) share transactional state within a single in-memory
    database.  Without this, flush-only repos would lose data across calls
    since each new session gets an independent transaction on ``:memory:``.
    """
    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 resource service in-memory database is initialized")
def step_init_resource_svc_db(context: Any) -> None:
    engine, session_factory = _make_session_factory(context)
    context.svc_engine = engine
    context.svc_session_factory = session_factory
    context.resource_service = ResourceRegistryService(
        session_factory=session_factory,
    )
    context.project_repo = NamespacedProjectRepository(
        session_factory=session_factory,
    )
    context.link_repo = ProjectResourceLinkRepository(
        session_factory=session_factory,
    )
    context.svc_error = None
    context.svc_project_error = None
    context.last_resource = None
    context.last_type = None
    context.last_bootstrap_count = 0
    context.second_bootstrap_count = None
    context.resource_list = []
    context.type_list = []
    context.project_list = []
    context.linked_resources_list = []
    context.last_project = None
    context.bootstrap_names = []
    # temp dir for YAML files
    context.svc_tmpdir = tempfile.mkdtemp()


# ─── Bootstrap ──────────────────────────────────────────────


@when("I bootstrap built-in resource types")
def step_bootstrap_builtins(context: Any) -> None:
    result = context.resource_service.bootstrap_builtin_types()
    context.last_bootstrap_count = len(result)
    # Types may have been auto-bootstrapped in __init__; fall back to
    # querying the database so assertion steps see all registered types.
    if result:
        context.bootstrap_names = result
    else:
        context.bootstrap_names = [
            t.name for t in context.resource_service.list_types()
        ]


@then('the bootstrap should register "{name}"')
def step_bootstrap_registered(context: Any, name: str) -> None:
    assert name in context.bootstrap_names, (
        f"Expected '{name}' in {context.bootstrap_names}"
    )


@when("I bootstrap built-in resource types again")
def step_bootstrap_again(context: Any) -> None:
    result = context.resource_service.bootstrap_builtin_types()
    context.second_bootstrap_count = len(result)


@then("the second bootstrap should register {count:d} new types")
def step_second_bootstrap_count(context: Any, count: int) -> None:
    assert context.second_bootstrap_count == count, (
        f"Expected {count}, got {context.second_bootstrap_count}"
    )


@given("built-in resource types are bootstrapped")
def step_given_bootstrapped(context: Any) -> None:
    context.resource_service.bootstrap_builtin_types()


# ─── List / Show Resource Types ─────────────────────────────


@when("I list all resource types via the service")
def step_list_all_types(context: Any) -> None:
    context.type_list = context.resource_service.list_types()


@then("the type list should contain at least {count:d} types")
def step_type_list_at_least(context: Any, count: int) -> None:
    assert len(context.type_list) >= count, (
        f"Expected at least {count}, got {len(context.type_list)}"
    )


@then('the type list should include "{name}"')
def step_type_list_includes(context: Any, name: str) -> None:
    names = [t.name for t in context.type_list]
    assert name in names, f"Expected '{name}' in {names}"


@when('I show the resource type "{name}" via the service')
def step_show_type(context: Any, name: str) -> None:
    context.last_type = context.resource_service.show_type(name)


@when('I show the resource type "{name}" via the service expecting an error')
def step_show_type_error(context: Any, name: str) -> None:
    try:
        context.resource_service.show_type(name)
    except Exception as exc:
        context.svc_error = exc


@then('the shown type name should be "{name}"')
def step_shown_type_name(context: Any, name: str) -> None:
    assert context.last_type is not None
    assert context.last_type.name == name


@then('the shown type resource kind should be "{kind}"')
def step_shown_type_kind(context: Any, kind: str) -> None:
    assert context.last_type is not None
    assert context.last_type.resource_kind.value == kind


@then('the shown type sandbox strategy should be "{strategy}"')
def step_shown_type_strategy(context: Any, strategy: str) -> None:
    assert context.last_type is not None
    assert context.last_type.sandbox_strategy.value == strategy


@then('the service error should be "{error_type}"')
def step_svc_error_type(context: Any, error_type: str) -> None:
    assert context.svc_error is not None, "Expected an error but none was raised"
    # Check the actual class name and all parent class names (handles aliases
    # like NotFoundError -> ResourceNotFoundError)
    exc = context.svc_error
    type_names = [cls.__name__ for cls in type(exc).__mro__]
    assert error_type in type_names, (
        f"Expected {error_type}, got {type(exc).__name__} (mro: {type_names})"
    )


# ─── Custom Resource Type Registration ──────────────────────


@given('a custom resource type YAML file for "{name}"')
def step_custom_yaml(context: Any, name: str) -> None:
    yaml_content = f"""
name: "{name}"
resource_kind: physical
sandbox_strategy: none
user_addable: true
description: "Test custom type"
"""
    path = os.path.join(context.svc_tmpdir, f"{name.replace('/', '_')}.yaml")
    with open(path, "w") as f:
        f.write(yaml_content)
    context.custom_yaml_path = path
    context.custom_type_name = name


@given("I register the custom resource type via the service")
def step_given_register_custom_type(context: Any) -> None:
    context.last_type = context.resource_service.register_type(context.custom_yaml_path)


@when("I register the custom resource type via the service")
def step_register_custom_type(context: Any) -> None:
    context.last_type = context.resource_service.register_type(context.custom_yaml_path)


@then('the registered type name should be "{name}"')
def step_registered_type_name(context: Any, name: str) -> None:
    assert context.last_type is not None
    assert context.last_type.name == name


@when("I register the same custom type again expecting an error")
def step_register_dup_type(context: Any) -> None:
    try:
        context.resource_service.register_type(context.custom_yaml_path)
    except Exception as exc:
        context.svc_error = exc


@when('I register a type from path "{path}" expecting an error')
def step_register_nonexistent_path(context: Any, path: str) -> None:
    try:
        context.resource_service.register_type(path)
    except Exception as exc:
        context.svc_error = exc


# ─── Resource Registration ──────────────────────────────────


@when(
    'I register a resource of type "{type_name}" named "{name}" with location "{location}"'
)
def step_register_resource(
    context: Any, type_name: str, name: str, location: str
) -> None:
    context.last_resource = context.resource_service.register_resource(
        type_name=type_name, name=name, location=location
    )


@when('I register a resource of type "{type_name}" named "{name}" expecting an error')
def step_register_resource_error(context: Any, type_name: str, name: str) -> None:
    try:
        context.resource_service.register_resource(type_name=type_name, name=name)
    except Exception as exc:
        context.svc_error = exc


@then('the registered resource name should be "{name}"')
def step_registered_resource_name(context: Any, name: str) -> None:
    assert context.last_resource is not None
    assert context.last_resource.name == name


@then('the registered resource type should be "{type_name}"')
def step_registered_resource_type(context: Any, type_name: str) -> None:
    assert context.last_resource is not None
    assert context.last_resource.resource_type_name == type_name


@then('the registered resource classification should be "{kind}"')
def step_registered_resource_classification(context: Any, kind: str) -> None:
    assert context.last_resource is not None
    assert context.last_resource.classification == kind


@given('a resource of type "{type_name}" named "{name}" is registered')
def step_given_resource_registered(context: Any, type_name: str, name: str) -> None:
    context.last_resource = context.resource_service.register_resource(
        type_name=type_name, name=name, location="/tmp/test"
    )
    # Commit so the row survives any rollback in later error-path steps.
    context.svc_session_factory().commit()


@when("I list all resources via the service")
def step_list_all_resources(context: Any) -> None:
    context.resource_list = context.resource_service.list_resources()


@when('I list resources of type "{type_name}" via the service')
def step_list_resources_by_type(context: Any, type_name: str) -> None:
    context.resource_list = context.resource_service.list_resources(type_name=type_name)


@then("the resource list should contain {count:d} resources")
def step_resource_list_count(context: Any, count: int) -> None:
    assert len(context.resource_list) == count, (
        f"Expected {count}, got {len(context.resource_list)}"
    )


@when('I show the resource "{name}" via the service')
def step_show_resource(context: Any, name: str) -> None:
    context.last_resource = context.resource_service.show_resource(name)


@when("I show the resource by its ULID via the service")
def step_show_resource_by_ulid(context: Any) -> None:
    assert context.last_resource is not None
    ulid = context.last_resource.resource_id
    context.last_resource = context.resource_service.show_resource(ulid)


@when('I show the resource "{name}" via the service expecting an error')
def step_show_resource_error(context: Any, name: str) -> None:
    try:
        context.resource_service.show_resource(name)
    except Exception as exc:
        context.svc_error = exc


@then('the shown resource name should be "{name}"')
def step_shown_resource_name(context: Any, name: str) -> None:
    assert context.last_resource is not None
    assert context.last_resource.name == name


# ─── Project Service ────────────────────────────────────────


@when('I create a namespaced project "{ns_name}" via the project service')
def step_create_ns_project(context: Any, ns_name: str) -> None:
    parts = ns_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    name = parts[1] if len(parts) == 2 else parts[0]
    project = NamespacedProject(name=name, namespace=namespace)
    context.last_project = context.project_repo.create(project)


@then('the service project "{ns_name}" should exist')
def step_project_exists(context: Any, ns_name: str) -> None:
    project = context.project_repo.get(ns_name)
    assert project is not None


@given('a namespaced project "{ns_name}" exists via the service')
def step_given_ns_project_exists(context: Any, ns_name: str) -> None:
    parts = ns_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    name = parts[1] if len(parts) == 2 else parts[0]
    project = NamespacedProject(name=name, namespace=namespace)
    context.project_repo.create(project)
    # Commit so the row survives any rollback in later error-path steps.
    context.svc_session_factory().commit()


@when(
    'I create a namespaced project "{ns_name}" via the project service expecting an error'
)
def step_create_dup_project(context: Any, ns_name: str) -> None:
    parts = ns_name.split("/", 1)
    namespace = parts[0] if len(parts) == 2 else "local"
    name = parts[1] if len(parts) == 2 else parts[0]
    project = NamespacedProject(name=name, namespace=namespace)
    try:
        context.project_repo.create(project)
    except Exception as exc:
        context.svc_project_error = exc


@then("the project service error should be a database error")
def step_project_svc_db_error(context: Any) -> None:
    assert context.svc_project_error is not None, (
        "Expected a database error but none was raised"
    )


@when("I list all projects via the project service")
def step_list_all_projects(context: Any) -> None:
    context.project_list = context.project_repo.list_projects()


@then("the project service list should contain {count:d} projects")
def step_project_list_count(context: Any, count: int) -> None:
    assert len(context.project_list) == count, (
        f"Expected {count}, got {len(context.project_list)}"
    )


@when('I get the project "{ns_name}" via the project service')
def step_get_project(context: Any, ns_name: str) -> None:
    context.last_project = context.project_repo.get(ns_name)


@then('the project service returned name should be "{name}"')
def step_project_returned_name(context: Any, name: str) -> None:
    assert context.last_project is not None
    assert context.last_project.name == name


@when('I delete the project "{ns_name}" via the project service')
def step_delete_project(context: Any, ns_name: str) -> None:
    context.project_repo.delete(ns_name)


@then('the project "{ns_name}" should not exist via the service')
def step_project_not_exist(context: Any, ns_name: str) -> None:
    try:
        context.project_repo.get(ns_name)
        raise AssertionError(f"Project {ns_name} should not exist")
    except Exception:
        pass


# ─── Link / Unlink Resources ───────────────────────────────


@when(
    'I link the resource "{resource_name}" to project "{project_name}" via the service'
)
def step_link_resource(context: Any, resource_name: str, project_name: str) -> None:
    resource = context.resource_service.show_resource(resource_name)
    context.link_repo.create_link(
        project_name=project_name,
        resource_id=resource.resource_id,
    )


@given('the resource "{resource_name}" is linked to project "{project_name}"')
def step_given_resource_linked(
    context: Any, resource_name: str, project_name: str
) -> None:
    resource = context.resource_service.show_resource(resource_name)
    context.link_repo.create_link(
        project_name=project_name,
        resource_id=resource.resource_id,
    )
    # Commit so the row survives any rollback in later error-path steps.
    context.svc_session_factory().commit()
    context.last_link_resource_id = resource.resource_id


@then('the project "{project_name}" should have {count:d} linked resource')
def step_project_link_count_singular(
    context: Any, project_name: str, count: int
) -> None:
    links = context.link_repo.list_links(project_name)
    assert len(links) == count, f"Expected {count}, got {len(links)}"


@then('the project "{project_name}" should have {count:d} linked resources')
def step_project_link_count(context: Any, project_name: str, count: int) -> None:
    links = context.link_repo.list_links(project_name)
    assert len(links) == count, f"Expected {count}, got {len(links)}"


@when('I unlink the resource from project "{project_name}" via the service')
def step_unlink_resource(context: Any, project_name: str) -> None:
    links = context.link_repo.list_links(project_name)
    if links:
        link_id: str = str(links[0].link_id)  # type: ignore[union-attr]
        context.link_repo.remove_link(link_id)


@when('I list linked resources for project "{project_name}" via the service')
def step_list_linked_resources(context: Any, project_name: str) -> None:
    context.linked_resources_list = context.link_repo.list_links(project_name)


@then("the linked resources list should contain {count:d} items")
def step_linked_resources_count(context: Any, count: int) -> None:
    assert len(context.linked_resources_list) == count, (
        f"Expected {count}, got {len(context.linked_resources_list)}"
    )


# ─── Additional Coverage Steps ──────────────────────────────


@when('I list resource types with namespace "{namespace}" via the service')
def step_list_types_by_namespace(context: Any, namespace: str) -> None:
    context.type_list = context.resource_service.list_types(namespace=namespace)


@when(
    'I register a resource of type "{type_name}" named "{name}"'
    ' with location "{location}" and properties'
)
def step_register_resource_with_props(
    context: Any, type_name: str, name: str, location: str
) -> None:
    context.last_resource = context.resource_service.register_resource(
        type_name=type_name,
        name=name,
        location=location,
        properties={"branch": "main", "depth": 1},
    )


@when(
    'I register a resource of type "{type_name}" with no name but location "{location}"'
)
def step_register_resource_no_name(context: Any, type_name: str, location: str) -> None:
    context.last_resource = context.resource_service.register_resource(
        type_name=type_name, name=None, location=location
    )


@then("the registered resource should have a ULID id")
def step_resource_has_ulid(context: Any) -> None:
    assert context.last_resource is not None
    assert context.last_resource.resource_id is not None
    assert len(context.last_resource.resource_id) == 26


# ─── DI Container ──────────────────────────────────────────


@when("I get the resource registry service from the DI container")
def step_get_di_resource_service(context: Any) -> None:
    from cleveragents.application.container import Container

    container = Container()
    context.di_resource_service = container.resource_registry_service()


@then("the resource registry service should not be None")
def step_di_resource_service_not_none(context: Any) -> None:
    assert context.di_resource_service is not None


@when("I get the namespaced project service from the DI container")
def step_get_di_project_service(context: Any) -> None:
    from cleveragents.application.container import Container

    container = Container()
    context.di_project_service = container.namespaced_project_repo()


@then("the namespaced project service should not be None")
def step_di_project_service_not_none(context: Any) -> None:
    assert context.di_project_service is not None
