"""Step definitions for tdd_actors_loader_lock_filter.feature (Issue #8660).

These steps exercise ``ActorLoader.list_actors`` with namespace filtering and verify
thread-safety under concurrent cache mutation.
"""

from __future__ import annotations

import tempfile
import threading
import time
from pathlib import Path
from typing import Any

from behave import given, then, when  # type: ignore[attr-defined]

from cleveragents.actor.loader import ActorLoader


def _make_actor_yaml(name: str, provider: str = "openai", model: str = "gpt-4") -> str:
    return f"""name: {name}
type: llm
description: Test actor {name}
provider: {provider}
model: {model}
tools: []
"""


# ---------------------------------------------------------------------------
# Fixture-like setup for the background scenario
# ---------------------------------------------------------------------------


def _setup_actor_dir() -> tuple[str, Path]:
    """Create a temp directory seeded with actors from two namespaces."""
    tmp = tempfile.mkdtemp()
    root = Path(tmp) / "actors"
    root.mkdir(parents=True)

    for fname, content in {
        "local_alpha.yaml": _make_actor_yaml("local/alpha"),
        "local_beta.yaml": _make_actor_yaml("local/beta"),
        "remote_gamma.yaml": _make_actor_yaml("remote/gamma", provider="anthropic"),
        "remote_delta.yaml": _make_actor_yaml("remote/delta", provider="google"),
    }.items():
        (root / fname).write_text(content)

    return tmp, root


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


@given("a temporary directory with multiple namespace groups of actor YAML files")
def step_given_actor_dir(context: Any) -> None:
    """Set up temp directory with actors from two namespaces."""
    context._actor_tmp, context._actor_root = _setup_actor_dir()  # type: ignore[attr-defined]


@given("the loader discovers all actors")
@when("the loader discovers all actors")
def step_when_discover_all(context: Any) -> None:
    loader: ActorLoader = context.actor_loader  # type: ignore[attr-defined]
    loader.discover()


@given("I create an ActorLoader with initial actors from multiple namespaces")
@when("I create an ActorLoader with initial actors from multiple namespaces")
def step_given_loader_with_initial(context: Any) -> None:
    """Set up a loader and seed it with actors."""
    loader = ActorLoader(search_roots=[context._actor_root])  # type: ignore[attr-defined]
    loader.discover()
    context.actor_loader = loader  # type: ignore[attr-defined]


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


@when("I create an ActorLoader from that directory")
def step_when_create_loader(context: Any) -> None:
    root: Path = context._actor_root  # type: ignore[attr-defined]
    loader = ActorLoader(search_roots=[root])
    loader.discover()
    context.actor_loader = loader  # type: ignore[attr-defined]


@when(
    "another thread concurrently adds and removes YAML files "
    "(triggering ``discover()``)"
)
def step_when_concurrent_modifications(context: Any) -> None:
    """Define a background thread that mutates actor files."""
    root: Path = context._actor_root  # type: ignore[attr-defined]

    def _concurrent_worker() -> None:
        """Repeatedly add and remove YAML files."""
        try:
            for i in range(10):
                tmp_fpath = root / f"conc_{i}.yaml"
                tmp_fpath.write_text(_make_actor_yaml(f"conc/concurrent_{i}"))
                loader = ActorLoader(search_roots=[root])
                loader.discover()
                time.sleep(0.01)
                if tmp_fpath.exists():
                    tmp_fpath.unlink(missing_ok=True)
        except Exception as exc:
            errors = getattr(context, "_concurrent_errors", [])  # type: ignore[attr-defined]
            errors.append(exc)
            context._concurrent_errors = errors  # type: ignore[attr-defined]

    context._worker_thread = threading.Thread(  # type: ignore[attr-defined]
        target=_concurrent_worker, daemon=True
    )


@when("a worker repeatedly calls ``list_actors(namespace=...)`` in parallel")
def step_when_parallel_list(context: Any) -> None:
    """Run parallel list_actors calls while a background thread mutates."""
    loader: ActorLoader = context.actor_loader  # type: ignore[attr-defined]

    def _list_worker() -> None:
        try:
            for _ in range(20):
                loader.list_actors(namespace="local")
                loader.list_actors(namespace="remote")
                time.sleep(0.01)
        except Exception as exc:
            errors = getattr(context, "_list_errors", [])  # type: ignore[attr-defined]
            errors.append(exc)
            context._list_errors = errors  # type: ignore[attr-defined]

    workers: list[threading.Thread] = []
    start_thread = getattr(context, "_worker_thread", None)  # type: ignore[attr-defined]

    for _ in range(5):
        t = threading.Thread(target=_list_worker, daemon=True)
        workers.append(t)
        t.start()

    if start_thread is not None:
        start_thread.start()

    for t in workers:
        t.join(timeout=10)
    if start_thread is not None:
        start_thread.join(timeout=10)

    collected_errors: list[Exception] = [
        *getattr(context, "_list_errors", []),  # type: ignore[attr-defined]
        *getattr(context, "_concurrent_errors", []),  # type: ignore[attr-defined]
    ]
    context._final_concurrent_errors = collected_errors  # type: ignore[attr-defined]


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


@then('filtering by namespace "local" should return only local actors')
def step_then_local_only(context: Any) -> None:
    result = context.actor_loader.list_actors(namespace="local")  # type: ignore[attr-defined]
    assert len(result) == 2, (
        f"Expected 2 local actors, got {len(result)}: {[a.name for a in result]}"
    )
    names = {a.name for a in result}
    assert names == {"local/alpha", "local/beta"}


@then('filtering by namespace "remote" should return only remote actors')
def step_then_remote_only(context: Any) -> None:
    result = context.actor_loader.list_actors(namespace="remote")  # type: ignore[attr-defined]
    assert len(result) == 2, (
        f"Expected 2 remote actors, got {len(result)}: {[a.name for a in result]}"
    )
    names = {a.name for a in result}
    assert names == {"remote/gamma", "remote/delta"}


@then("calling ``list_actors()`` without a filter should return every actor")
def step_then_all_actors(context: Any) -> None:
    result = context.actor_loader.list_actors()  # type: ignore[attr-defined]
    assert len(result) == 4, f"Expected 4 total actors, got {len(result)}"


@then("no exceptions should be raised")
def step_then_no_errors(context: Any) -> None:
    errors = getattr(context, "_final_concurrent_errors", [])  # type: ignore[attr-defined]
    assert len(errors) == 0, f"Concurrent errors occurred: {errors}"


@then("every returned result should be internally consistent")
def step_then_consistent(context: Any) -> None:
    """Every thread-safe list_actors call returns a stable snapshot."""
    loader: ActorLoader = context.actor_loader  # type: ignore[attr-defined]
    result = loader.list_actors(namespace="local")
    for actor in result:
        assert hasattr(actor, "name"), f"Actor missing 'name' attribute: {actor}"
        assert "/" in actor.name, (
            f"Namespaced actor name must contain '/': {actor.name}"
        )


@then("filtering by a non-existent namespace should return an empty list")
def step_then_empty_for_missing_ns(context: Any) -> None:
    result = context.actor_loader.list_actors(namespace="nonexistent")  # type: ignore[attr-defined]
    assert result == []
