"""Step definitions for resource_type_virtual_core.feature.

Tests virtual core resource types: file, directory, commit, branch, tag, tree.
All step suffixes use 'for vcore' to avoid AmbiguousStep conflicts.

Context attributes are accessed via ``_ctx()`` typed wrapper to satisfy
Pyright without inline ``# type: ignore`` suppressions.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml
from behave import given, then, when
from pydantic import ValidationError

from cleveragents.application.services._resource_registry_data import (
    BUILTIN_TYPES,
)
from cleveragents.domain.models.core.resource_type import (
    ResourceKind,
    ResourceTypeSpec,
)
from cleveragents.resource.schema import ResourceTypeConfigSchema

_EXAMPLES_DIR = (
    Path(__file__).resolve().parent.parent.parent / "examples" / "resource-types"
)


# ---------------------------------------------------------------------------
# Typed context accessor — eliminates inline type: ignore suppressions
# ---------------------------------------------------------------------------


_SENTINEL = object()

# Attribute name mapping: property name -> context attribute name.
_ATTR_MAP: dict[str, str] = {
    "yaml_path": "vcore_yaml_path",
    "schema": "vcore_schema",
    "domain": "vcore_domain",
    "builtin_defs": "vcore_builtin_defs",
    "found_builtin": "vcore_found_builtin",
    "test_config": "vcore_test_config",
    "created_spec": "vcore_created_spec",
    "creation_error": "vcore_creation_error",
    "registry_service": "vcore_registry_service",
    "register_error": "vcore_register_error",
}


class _Ctx:
    """Typed proxy for Behave's dynamically-typed ``context`` object.

    Centralises all dynamic attribute access behind typed properties so
    step functions never need inline ``# type: ignore`` comments.
    Uses a dict-based lookup to satisfy both Pyright (typed properties)
    and ruff B009/B010 (no constant-string getattr/setattr).
    """

    __slots__ = ("_ctx",)

    def __init__(self, ctx: object) -> None:
        object.__setattr__(self, "_ctx", ctx)

    def _get(self, key: str, default: Any = _SENTINEL) -> Any:
        attr = _ATTR_MAP[key]
        if default is _SENTINEL:
            return getattr(self._ctx, attr)
        return getattr(self._ctx, attr, default)

    def _set(self, key: str, value: Any) -> None:
        setattr(self._ctx, _ATTR_MAP[key], value)

    @property
    def yaml_path(self) -> Path:
        return self._get("yaml_path")

    @yaml_path.setter
    def yaml_path(self, value: Path) -> None:
        self._set("yaml_path", value)

    @property
    def schema(self) -> ResourceTypeConfigSchema:
        return self._get("schema")

    @schema.setter
    def schema(self, value: ResourceTypeConfigSchema) -> None:
        self._set("schema", value)

    @property
    def domain(self) -> ResourceTypeSpec:
        return self._get("domain")

    @domain.setter
    def domain(self, value: ResourceTypeSpec) -> None:
        self._set("domain", value)

    @property
    def builtin_defs(self) -> list[dict[str, Any]]:
        return self._get("builtin_defs")

    @builtin_defs.setter
    def builtin_defs(self, value: list[dict[str, Any]]) -> None:
        self._set("builtin_defs", value)

    @property
    def found_builtin(self) -> dict[str, Any]:
        return self._get("found_builtin")

    @found_builtin.setter
    def found_builtin(self, value: dict[str, Any]) -> None:
        self._set("found_builtin", value)

    @property
    def test_config(self) -> dict[str, Any]:
        return self._get("test_config")

    @test_config.setter
    def test_config(self, value: dict[str, Any]) -> None:
        self._set("test_config", value)

    @property
    def created_spec(self) -> ResourceTypeSpec | None:
        return self._get("created_spec", None)

    @created_spec.setter
    def created_spec(self, value: ResourceTypeSpec | None) -> None:
        self._set("created_spec", value)

    @property
    def creation_error(self) -> Exception | None:
        return self._get("creation_error", None)

    @creation_error.setter
    def creation_error(self, value: Exception | None) -> None:
        self._set("creation_error", value)

    @property
    def registry_service(self) -> Any:
        return self._get("registry_service")

    @registry_service.setter
    def registry_service(self, value: Any) -> None:
        self._set("registry_service", value)

    @property
    def register_error(self) -> Exception | None:
        return self._get("register_error", None)

    @register_error.setter
    def register_error(self, value: Exception | None) -> None:
        self._set("register_error", value)


def _ctx(context: object) -> _Ctx:
    """Wrap Behave context with typed accessors."""
    return _Ctx(context)


# ── Given steps ──────────────────────────────────────────────


@given("the built-in {type_name} virtual YAML file for vcore")
def step_builtin_virtual_yaml(context: object, type_name: str) -> None:
    """Load path to a virtual core type YAML file."""
    path = _EXAMPLES_DIR / f"{type_name}.yaml"
    assert path.exists(), f"YAML file not found: {path}"
    _ctx(context).yaml_path = path


@given("the bootstrap builtin type definitions for vcore")
def step_bootstrap_builtin_defs(context: object) -> None:
    """Access the BUILTIN_TYPES list from the registry data module."""
    _ctx(context).builtin_defs = list(BUILTIN_TYPES)


# ── When steps ───────────────────────────────────────────────


@when("I load the virtual YAML via from_yaml_file for vcore")
def step_load_virtual_yaml(context: object) -> None:
    """Load virtual core YAML using the schema loader."""
    c = _ctx(context)
    c.schema = ResourceTypeConfigSchema.from_yaml_file(c.yaml_path)


@when("I load the virtual YAML as a ResourceTypeSpec for vcore")
def step_load_virtual_as_domain(context: object) -> None:
    """Load virtual core YAML as a domain model."""
    c = _ctx(context)
    raw: dict[str, Any] = yaml.safe_load(c.yaml_path.read_text(encoding="utf-8"))
    c.domain = ResourceTypeSpec.from_config(raw)


@when('I find the virtual type "{type_name}" in builtin definitions for vcore')
def step_find_virtual_in_builtins(context: object, type_name: str) -> None:
    """Find a specific virtual type entry in BUILTIN_TYPES."""
    c = _ctx(context)
    found = [d for d in c.builtin_defs if d["name"] == type_name]
    assert len(found) == 1, (
        f"Expected exactly 1 entry for '{type_name}', found {len(found)}"
    )
    c.found_builtin = found[0]


# ── Then steps: schema validation ────────────────────────────


@then("the vcore schema should be valid")
def step_vcore_schema_valid(context: object) -> None:
    schema = _ctx(context).schema
    assert schema is not None
    assert isinstance(schema, ResourceTypeConfigSchema)


@then('the vcore schema name should be "{name}"')
def step_vcore_schema_name(context: object, name: str) -> None:
    assert _ctx(context).schema.name == name


@then('the vcore schema resource_kind should be "{kind}"')
def step_vcore_schema_kind(context: object, kind: str) -> None:
    assert _ctx(context).schema.resource_kind == kind


@then('the vcore schema sandbox_strategy should be "{strategy}"')
def step_vcore_schema_sandbox(context: object, strategy: str) -> None:
    assert _ctx(context).schema.sandbox_strategy == strategy


@then("the vcore schema user_addable should be false")
def step_vcore_schema_not_addable(context: object) -> None:
    assert _ctx(context).schema.user_addable is False


@then("the vcore schema built_in should be true")
def step_vcore_schema_builtin(context: object) -> None:
    assert _ctx(context).schema.built_in is True


@then('the vcore schema name should not contain "/"')
def step_vcore_schema_no_slash(context: object) -> None:
    assert "/" not in _ctx(context).schema.name


@then("the vcore schema should have {count:d} cli_args")
def step_vcore_schema_cli_args_count(context: object, count: int) -> None:
    actual = len(_ctx(context).schema.cli_args)
    assert actual == count, f"Expected {count} cli_args, got {actual}"


@then("the vcore schema should have empty parent_types")
def step_vcore_schema_no_parents(context: object) -> None:
    parent_types = _ctx(context).schema.parent_types
    assert len(parent_types) == 0, f"Expected empty parent_types, got {parent_types}"


@then('the vcore schema child_types should contain "{child}"')
def step_vcore_schema_child(context: object, child: str) -> None:
    children = _ctx(context).schema.child_types
    assert child in children, f"'{child}' not in child_types: {children}"


@then("the vcore schema child_types should have exactly {count:d} entries")
def step_vcore_schema_child_count(context: object, count: int) -> None:
    actual = len(_ctx(context).schema.child_types)
    assert actual == count, f"Expected {count} child_types, got {actual}"


# ── Then steps: capabilities ─────────────────────────────────


@then("the vcore capabilities should have read {value}")
def step_vcore_caps_read(context: object, value: str) -> None:
    expected = value.lower() == "true"
    actual = _ctx(context).schema.capabilities["read"]
    assert actual is expected, f"read: expected {expected}, got {actual}"


@then("the vcore capabilities should have write {value}")
def step_vcore_caps_write(context: object, value: str) -> None:
    expected = value.lower() == "true"
    actual = _ctx(context).schema.capabilities["write"]
    assert actual is expected, f"write: expected {expected}, got {actual}"


@then("the vcore capabilities should have sandbox {value}")
def step_vcore_caps_sandbox(context: object, value: str) -> None:
    expected = value.lower() == "true"
    actual = _ctx(context).schema.capabilities["sandbox"]
    assert actual is expected, f"sandbox: expected {expected}, got {actual}"


@then("the vcore capabilities should have checkpoint {value}")
def step_vcore_caps_checkpoint(context: object, value: str) -> None:
    expected = value.lower() == "true"
    actual = _ctx(context).schema.capabilities["checkpoint"]
    assert actual is expected, f"checkpoint: expected {expected}, got {actual}"


# ── Then steps: equivalence ──────────────────────────────────


@then("the vcore schema equivalence should not be none")
def step_vcore_equivalence_not_none(context: object) -> None:
    equiv = _ctx(context).schema.equivalence
    assert equiv is not None, "equivalence should not be None for virtual types"


@then('the vcore equivalence criteria should contain "{criterion}"')
def step_vcore_equivalence_criterion(context: object, criterion: str) -> None:
    equiv = _ctx(context).schema.equivalence
    assert equiv is not None, "equivalence is None"
    criteria = equiv.get("criteria", [])
    assert criterion in criteria, f"'{criterion}' not in criteria: {criteria}"


# ── Then steps: auto-discovery and handler ───────────────────


@then("the vcore schema auto_discovery should be none")
def step_vcore_no_autodiscovery(context: object) -> None:
    auto = _ctx(context).schema.auto_discovery
    assert auto is None, f"Expected auto_discovery=None, got {auto}"


@then("the vcore schema handler should be none")
def step_vcore_no_handler(context: object) -> None:
    handler = _ctx(context).schema.handler
    assert handler is None, f"Expected handler=None, got {handler}"


# ── Then steps: domain model ────────────────────────────────


@then("the vcore domain model should be valid")
def step_vcore_domain_valid(context: object) -> None:
    domain = _ctx(context).domain
    assert domain is not None
    assert isinstance(domain, ResourceTypeSpec)


@then('the vcore domain model name should be "{name}"')
def step_vcore_domain_name(context: object, name: str) -> None:
    assert _ctx(context).domain.name == name


@then("the vcore domain model resource_kind should be virtual")
def step_vcore_domain_kind_virtual(context: object) -> None:
    domain = _ctx(context).domain
    assert domain.resource_kind == ResourceKind.VIRTUAL, (
        f"Expected VIRTUAL, got {domain.resource_kind}"
    )


# ── Then steps: bootstrap registration ──────────────────────


@then('the builtin definitions should include "{type_name}" for vcore')
def step_builtin_includes_type(context: object, type_name: str) -> None:
    defs: list[dict[str, Any]] = _ctx(context).builtin_defs
    names = [d["name"] for d in defs]
    assert type_name in names, f"'{type_name}' not found in builtin defs: {names}"


@then("the found builtin type user_addable should be false for vcore")
def step_found_builtin_not_addable(context: object) -> None:
    entry: dict[str, Any] = _ctx(context).found_builtin
    assert entry["user_addable"] is False, (
        f"Expected user_addable=False, got {entry['user_addable']}"
    )


@then('the found builtin type resource_kind should be "{kind}" for vcore')
def step_found_builtin_kind(context: object, kind: str) -> None:
    entry: dict[str, Any] = _ctx(context).found_builtin
    assert entry["resource_kind"] == kind, (
        f"Expected resource_kind='{kind}', got {entry['resource_kind']}"
    )


@then("the found builtin type should have equivalence for vcore")
def step_found_builtin_has_equivalence(context: object) -> None:
    entry: dict[str, Any] = _ctx(context).found_builtin
    assert "equivalence" in entry, f"No 'equivalence' key in entry: {entry.keys()}"
    assert entry["equivalence"] is not None, "equivalence is None"
    assert "criteria" in entry["equivalence"], (
        f"No 'criteria' in equivalence: {entry['equivalence']}"
    )


# ── Then steps: BUILTIN_NAMES ──────────────────────────────


@then('the ResourceTypeSpec BUILTIN_NAMES should contain "{name}" for vcore')
def step_builtin_names_contains(context: object, name: str) -> None:
    assert name in ResourceTypeSpec.BUILTIN_NAMES, (
        f"'{name}' not in BUILTIN_NAMES: {ResourceTypeSpec.BUILTIN_NAMES}"
    )


# ── Given/When/Then steps: negative validation tests ────────


@given("a virtual type config missing equivalence for vcore")
def step_virtual_config_missing_equiv(context: object) -> None:
    """Create a virtual type config dict without equivalence."""
    _ctx(context).test_config = {
        "name": "test-virtual-no-equiv",
        "resource_kind": "virtual",
        "sandbox_strategy": "none",
        "user_addable": False,
        "built_in": True,
        "cli_args": [],
        "parent_types": [],
        "child_types": [],
        "capabilities": {
            "read": False,
            "write": False,
            "sandbox": False,
            "checkpoint": False,
        },
    }


@given("a virtual type config with empty equivalence criteria for vcore")
def step_virtual_config_empty_criteria(context: object) -> None:
    """Create a virtual type config with equivalence but empty criteria."""
    _ctx(context).test_config = {
        "name": "test-virtual-empty-criteria",
        "resource_kind": "virtual",
        "sandbox_strategy": "none",
        "user_addable": False,
        "built_in": True,
        "cli_args": [],
        "parent_types": [],
        "child_types": [],
        "capabilities": {
            "read": False,
            "write": False,
            "sandbox": False,
            "checkpoint": False,
        },
        "equivalence": {
            "criteria": [],
            "description": "Empty criteria test",
        },
    }


@when("I attempt to create a ResourceTypeSpec from it for vcore")
def step_attempt_create_spec(context: object) -> None:
    """Try to create a ResourceTypeSpec and capture any error."""
    c = _ctx(context)
    try:
        c.created_spec = ResourceTypeSpec.from_config(c.test_config)
        c.creation_error = None
    except (ValueError, ValidationError) as exc:
        c.created_spec = None
        c.creation_error = exc


@then("the vcore creation should fail with a validation error")
def step_creation_should_fail(context: object) -> None:
    assert _ctx(context).creation_error is not None, (
        "Expected a validation error but creation succeeded"
    )


@then('the vcore error message should mention "{keyword}"')
def step_error_mentions(context: object, keyword: str) -> None:
    err = _ctx(context).creation_error
    assert err is not None, "No error captured"
    assert keyword.lower() in str(err).lower(), (
        f"Error message does not mention '{keyword}': {err}"
    )


@then("the vcore creation should succeed")
def step_creation_should_succeed(context: object) -> None:
    c = _ctx(context)
    assert c.creation_error is None, (
        f"Expected success but got error: {c.creation_error}"
    )
    assert c.created_spec is not None


@then("the vcore created spec equivalence criteria should be empty")
def step_created_spec_empty_criteria(context: object) -> None:
    spec = _ctx(context).created_spec
    assert spec is not None
    assert spec.equivalence is not None
    assert spec.equivalence["criteria"] == [], (
        f"Expected empty criteria, got {spec.equivalence['criteria']}"
    )


@then("the vcore equivalence criteria count should be {count:d}")
def step_equiv_criteria_count(context: object, count: int) -> None:
    schema = _ctx(context).schema
    actual = len(schema.equivalence["criteria"])
    assert actual == count, f"Expected {count} criteria, got {actual}"


@then("the vcore domain model should have equivalence")
def step_domain_has_equivalence(context: object) -> None:
    domain = _ctx(context).domain
    assert domain.equivalence is not None, (
        f"Domain model {domain.name} has no equivalence"
    )


@then("the YAML and registry should have matching fields for vcore")
def step_yaml_registry_match(context: object) -> None:
    """Assert the YAML-loaded domain model matches the registry dict."""
    domain = _ctx(context).domain
    found: dict[str, Any] = _ctx(context).found_builtin
    assert domain.name == found["name"], (
        f"name mismatch: {domain.name} != {found['name']}"
    )
    assert domain.resource_kind.value == found["resource_kind"], (
        f"resource_kind mismatch: {domain.resource_kind} != {found['resource_kind']}"
    )
    assert domain.sandbox_strategy.value == found["sandbox_strategy"], (
        "sandbox_strategy mismatch"
    )
    assert domain.user_addable == found["user_addable"], "user_addable mismatch"
    assert sorted(domain.child_types) == sorted(found["child_types"]), (
        f"child_types mismatch: {domain.child_types} != {found['child_types']}"
    )
    yaml_criteria = domain.equivalence["criteria"] if domain.equivalence else []
    reg_criteria = found.get("equivalence", {}).get("criteria", [])
    assert yaml_criteria == reg_criteria, (
        f"equivalence criteria mismatch: {yaml_criteria} != {reg_criteria}"
    )


# ── Given/When/Then: user-add rejection ──────────────────


@given("a bootstrapped resource registry for vcore")
def step_bootstrapped_registry(context: object) -> None:
    """Create a ResourceRegistryService and bootstrap built-in types."""
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker

    from cleveragents.application.services.resource_registry_service import (
        ResourceRegistryService,
    )
    from cleveragents.infrastructure.database.models import Base

    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine)
    service = ResourceRegistryService(session_factory=factory)
    service.bootstrap_builtin_types()
    _ctx(context).registry_service = service


@when('I attempt to register a resource of virtual type "{type_name}" for vcore')
def step_attempt_register_virtual(context: object, type_name: str) -> None:
    """Try to register a resource instance of a virtual type."""
    c = _ctx(context)
    try:
        c.registry_service.register_resource(type_name, name=f"test-{type_name}")
        c.register_error = None
    except Exception as exc:
        c.register_error = exc


@then('the vcore resource registration should fail with "{message}"')
def step_register_should_fail(context: object, message: str) -> None:
    err = _ctx(context).register_error
    assert err is not None, "Expected registration error but succeeded"
    assert message.lower() in str(err).lower(), (
        f"Error does not mention '{message}': {err}"
    )
