"""Step definitions for actor configuration coverage."""

from __future__ import annotations

import ast
import json
import os
import shutil
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any

from behave import given, then, when

import cleveragents.actor.config as actor_config
from cleveragents.actor.config import ActorConfiguration


def _add_cleanup(context, handler: Callable[[], None]) -> None:
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(handler)


def _ensure_workspace(context) -> Path:
    if getattr(context, "config_workspace", None) is None:
        workspace = Path(tempfile.mkdtemp(prefix="actor-config-"))
        context.config_workspace = workspace

        def cleanup_workspace() -> None:
            shutil.rmtree(workspace, ignore_errors=True)

        _add_cleanup(context, cleanup_workspace)
    return context.config_workspace


def _resolve_path(obj: Any, path: str) -> Any:
    current: Any = obj
    for segment in path.split("."):
        if isinstance(current, list):
            current = current[int(segment)]
            continue
        if isinstance(current, dict):
            assert segment in current, f"{segment} missing from {current}"
            current = current[segment]
            continue
        raise AssertionError(f"Cannot traverse through {type(current).__name__}")
    return current


@given("an isolated actor config workspace")
def step_isolated_workspace(context) -> None:
    _ensure_workspace(context)


@given('an actor config file "{filename}" with content:')
def step_actor_config_file_with_content(context, filename: str) -> None:
    workspace = _ensure_workspace(context)
    path = workspace / filename
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(context.text or "")
    context.last_config_path = path


@given("PyYAML parsing is unavailable for actor config")
def step_disable_yaml(context) -> None:
    context._original_yaml_module = actor_config.yaml
    actor_config.yaml = None

    def restore_yaml() -> None:
        actor_config.yaml = context._original_yaml_module

    _add_cleanup(context, restore_yaml)


@given('the actor config environment variable "{name}" is unset')
def step_unset_env_var(context, name: str) -> None:
    original = os.environ.get(name)

    def restore() -> None:
        if original is None:
            os.environ.pop(name, None)
        else:
            os.environ[name] = original

    _add_cleanup(context, restore)
    os.environ.pop(name, None)


@given('the actor config environment variable "{name}" is set to "{value}"')
def step_set_env_var(context, name: str, value: str) -> None:
    original = os.environ.get(name)

    def restore() -> None:
        if original is None:
            os.environ.pop(name, None)
        else:
            os.environ[name] = original

    _add_cleanup(context, restore)
    os.environ[name] = value


@when('I load the actor config blob from "{filename}"')
def step_load_actor_config_blob(context, filename: str) -> None:
    workspace = _ensure_workspace(context)
    path = workspace / filename
    try:
        context.load_result = ActorConfiguration.load_blob_from_file(path)
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.load_result = None
        context.last_error = exc


@when('I parse the actor configuration from file "{filename}" with overrides:')
def step_parse_actor_config_from_file(context, filename: str) -> None:
    workspace = _ensure_workspace(context)
    overrides: dict[str, Any] = json.loads(context.text or "{}")
    try:
        context.actor_config_result = ActorConfiguration.from_file(
            path=workspace / filename,
            name=overrides.get("name"),
            provider=overrides.get("provider"),
            model=overrides.get("model"),
            graph_descriptor=overrides.get("graph_descriptor"),
            unsafe=bool(overrides.get("unsafe", False)),
        )
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.actor_config_result = None
        context.last_error = exc


@when("I build an actor configuration from blob {blob_literal}")
def step_build_actor_config_from_blob(context, blob_literal: str) -> None:
    blob = ast.literal_eval(blob_literal)
    try:
        context.actor_config_result = ActorConfiguration.from_blob(blob=blob)
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.actor_config_result = None
        context.last_error = exc


@when(
    "I build an actor configuration from structured blob with defaults and overrides:"
)
def step_build_actor_config_with_defaults(context) -> None:
    payload = json.loads(context.text or "{}")
    blob = payload.get("blob")
    default_options = payload.get("default_options")
    option_overrides = payload.get("option_overrides")
    try:
        context.actor_config_result = ActorConfiguration.from_blob(
            blob=blob,
            default_options=default_options,
            option_overrides=option_overrides,
        )
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.actor_config_result = None
        context.last_error = exc


@then('a ValueError should be raised containing "{message}"')
def step_value_error_with_message(context, message: str) -> None:
    assert isinstance(context.last_error, ValueError), type(context.last_error)
    assert message in str(context.last_error)


@then("the loaded actor config blob should equal {expected}")
def step_loaded_blob_equals(context, expected: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.load_result == ast.literal_eval(expected)


@then('the loaded actor config value at "{path_expr}" should equal {expected}')
def step_loaded_blob_value_at_path(context, path_expr: str, expected: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.load_result is not None
    actual = _resolve_path(context.load_result, path_expr)
    assert actual == ast.literal_eval(expected)


@then('the actor configuration should have provider "{provider}" and model "{model}"')
def step_actor_config_provider_model(context, provider: str, model: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.provider == provider
    assert context.actor_config_result.model == model


@then('the actor configuration name should be "{name}"')
def step_actor_config_name(context, name: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.name == name


@then("the actor configuration graph descriptor should equal {expected}")
def step_actor_config_graph_descriptor(context, expected: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.graph_descriptor == ast.literal_eval(expected)


@then('the actor configuration graph descriptor should include key "{key}"')
def step_actor_config_graph_descriptor_has_key(context, key: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    graph_descriptor = context.actor_config_result.graph_descriptor
    assert graph_descriptor is not None, "graph_descriptor missing"
    assert key in graph_descriptor, f"{key} missing from graph_descriptor"


@then("the actor configuration options should equal {expected}")
def step_actor_config_options(context, expected: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.options == ast.literal_eval(expected)


@then('the loaded actor config value at "{path_expr}" should contain "{substring}"')
def step_loaded_blob_value_contains(context, path_expr: str, substring: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.load_result is not None
    actual = _resolve_path(context.load_result, path_expr)
    assert isinstance(actual, str), f"Expected str, got {type(actual).__name__}"
    assert substring in actual, f"'{substring}' not found in '{actual}'"


@then('the loaded actor config nested value at "{path_expr}" should equal {expected}')
def step_loaded_blob_nested_value(context, path_expr: str, expected: str) -> None:
    assert context.last_error is None, context.last_error
    assert context.load_result is not None
    actual = _resolve_path(context.load_result, path_expr)
    assert actual == ast.literal_eval(expected), f"Expected {expected}, got {actual!r}"


@when("I build actor config from None blob with provider and model overrides")
def step_build_actor_config_none_blob(context) -> None:
    try:
        context.actor_config_result = ActorConfiguration.from_blob(
            blob=None,
            provider="override-provider",
            model="override-model",
        )
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.actor_config_result = None
        context.last_error = exc


@when("I build actor config from blob using graph key alias")
def step_build_actor_config_graph_alias(context) -> None:
    try:
        context.actor_config_result = ActorConfiguration.from_blob(
            blob={
                "provider": "p",
                "model": "m",
                "graph": {"step": "one"},
            },
        )
        context.last_error = None
    except Exception as exc:  # pragma: no cover - exercised in tests
        context.actor_config_result = None
        context.last_error = exc


@then("the actor configuration graph descriptor should be None")
def step_actor_config_graph_descriptor_none(context) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.graph_descriptor is None


@then("the actor configuration unsafe flag should be true")
def step_actor_config_unsafe_true(context) -> None:
    assert context.last_error is None, context.last_error
    assert context.actor_config_result is not None
    assert context.actor_config_result.unsafe is True
