"""Step definitions for resource_type_deferred_virtual.feature.

Tests for deferred virtual resource types: remote, submodule, symlink (#331).
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml
from behave import given, then, when

from cleveragents.domain.models.core.resource_type import ResourceTypeSpec
from cleveragents.resource.schema import ResourceTypeConfigSchema

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


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


@given("the built-in remote YAML file for virt_deferred")
def step_builtin_remote_yaml_virt_deferred(context: object) -> None:
    """Load the remote YAML file path."""
    context.virt_deferred_yaml_path = _EXAMPLES_DIR / "remote.yaml"  # type: ignore[attr-defined]


@given("the built-in submodule YAML file for virt_deferred")
def step_builtin_submodule_yaml_virt_deferred(context: object) -> None:
    """Load the submodule YAML file path."""
    context.virt_deferred_yaml_path = _EXAMPLES_DIR / "submodule.yaml"  # type: ignore[attr-defined]


@given("the built-in symlink YAML file for virt_deferred")
def step_builtin_symlink_yaml_virt_deferred(context: object) -> None:
    """Load the symlink YAML file path."""
    context.virt_deferred_yaml_path = _EXAMPLES_DIR / "symlink.yaml"  # type: ignore[attr-defined]


@given("the registry builtin types list for virt_deferred")
def step_registry_builtin_types_virt_deferred(context: object) -> None:
    """Load the BUILTIN_TYPES list from the registry data module."""
    from cleveragents.application.services._resource_registry_data import (
        BUILTIN_TYPES,
    )

    context.virt_deferred_builtin_types = {  # type: ignore[attr-defined]
        t["name"]: t for t in BUILTIN_TYPES
    }


@given("the ResourceTypeSpec BUILTIN_NAMES set for virt_deferred")
def step_builtin_names_set_virt_deferred(context: object) -> None:
    """Load the BUILTIN_NAMES frozenset."""
    context.virt_deferred_builtin_names = ResourceTypeSpec.BUILTIN_NAMES  # type: ignore[attr-defined]


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


@when("I load the YAML via schema for virt_deferred")
def step_load_yaml_schema_virt_deferred(context: object) -> None:
    """Load the YAML file via the schema loader."""
    path: Path = context.virt_deferred_yaml_path  # type: ignore[attr-defined]
    context.virt_deferred_schema = ResourceTypeConfigSchema.from_yaml_file(path)  # type: ignore[attr-defined]


@when("I load the YAML as a ResourceTypeSpec for virt_deferred")
def step_load_yaml_domain_virt_deferred(context: object) -> None:
    """Load the YAML file as a domain model."""
    path: Path = context.virt_deferred_yaml_path  # type: ignore[attr-defined]
    raw: dict[str, Any] = yaml.safe_load(path.read_text(encoding="utf-8"))
    context.virt_deferred_domain = ResourceTypeSpec.from_config(raw)  # type: ignore[attr-defined]


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


@then("the virt_deferred schema should be valid")
def step_schema_valid_virt_deferred(context: object) -> None:
    """Assert the schema loaded successfully with required fields populated."""
    schema = context.virt_deferred_schema  # type: ignore[attr-defined]
    assert schema is not None, "Schema should not be None"
    assert hasattr(schema, "name") and schema.name, "Schema must have a non-empty name"
    assert hasattr(schema, "resource_kind") and schema.resource_kind, (
        "Schema must have a resource_kind"
    )
    assert hasattr(schema, "sandbox_strategy"), "Schema must have sandbox_strategy"


@then('the virt_deferred schema name should be "{name}"')
def step_schema_name_virt_deferred(context: object, name: str) -> None:
    """Assert the schema name matches."""
    actual = context.virt_deferred_schema.name  # type: ignore[attr-defined]
    assert actual == name, f"Expected schema name '{name}', got '{actual}'"


@then('the virt_deferred schema resource_kind should be "{kind}"')
def step_schema_kind_virt_deferred(context: object, kind: str) -> None:
    """Assert the resource kind matches."""
    actual = context.virt_deferred_schema.resource_kind  # type: ignore[attr-defined]
    assert actual == kind, f"Expected resource_kind '{kind}', got '{actual}'"


@then("the virt_deferred schema user_addable should be false")
def step_schema_not_user_addable_virt_deferred(context: object) -> None:
    """Assert the type is not user-addable."""
    actual = context.virt_deferred_schema.user_addable  # type: ignore[attr-defined]
    assert actual is False, f"Expected user_addable False, got {actual}"


@then("the virt_deferred schema built_in should be true")
def step_schema_builtin_virt_deferred(context: object) -> None:
    """Assert the type is built-in."""
    actual = context.virt_deferred_schema.built_in  # type: ignore[attr-defined]
    assert actual is True, f"Expected built_in True, got {actual}"


@then('the virt_deferred schema name should not contain "/"')
def step_schema_no_slash_virt_deferred(context: object) -> None:
    """Assert the name is unnamespaced."""
    name = context.virt_deferred_schema.name  # type: ignore[attr-defined]
    assert "/" not in name, f"Built-in name '{name}' should not contain '/'"


@then('the virt_deferred schema sandbox_strategy should be "{strategy}"')
def step_schema_sandbox_virt_deferred(context: object, strategy: str) -> None:
    """Assert the sandbox strategy matches."""
    actual = context.virt_deferred_schema.sandbox_strategy  # type: ignore[attr-defined]
    assert actual == strategy, f"Expected sandbox_strategy '{strategy}', got '{actual}'"


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


@then("the virt_deferred capabilities should have read {value}")
def step_caps_read_virt_deferred(context: object, value: str) -> None:
    """Assert the read capability."""
    expected = value.lower() == "true"
    actual = context.virt_deferred_schema.capabilities["read"]  # type: ignore[attr-defined]
    assert actual == expected, f"Expected read={expected}, got {actual}"


@then("the virt_deferred capabilities should have write {value}")
def step_caps_write_virt_deferred(context: object, value: str) -> None:
    """Assert the write capability."""
    expected = value.lower() == "true"
    actual = context.virt_deferred_schema.capabilities["write"]  # type: ignore[attr-defined]
    assert actual == expected, f"Expected write={expected}, got {actual}"


@then("the virt_deferred capabilities should have sandbox {value}")
def step_caps_sandbox_virt_deferred(context: object, value: str) -> None:
    """Assert the sandbox capability."""
    expected = value.lower() == "true"
    actual = context.virt_deferred_schema.capabilities["sandbox"]  # type: ignore[attr-defined]
    assert actual == expected, f"Expected sandbox={expected}, got {actual}"


@then("the virt_deferred capabilities should have checkpoint {value}")
def step_caps_checkpoint_virt_deferred(context: object, value: str) -> None:
    """Assert the checkpoint capability."""
    expected = value.lower() == "true"
    actual = context.virt_deferred_schema.capabilities["checkpoint"]  # type: ignore[attr-defined]
    assert actual == expected, f"Expected checkpoint={expected}, got {actual}"


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


@then("the virt_deferred schema equivalence should be present")
def step_equivalence_present_virt_deferred(context: object) -> None:
    """Assert equivalence metadata is present."""
    equiv = context.virt_deferred_schema.equivalence  # type: ignore[attr-defined]
    assert equiv is not None, "Equivalence metadata should not be None"
    assert "criteria" in equiv, (
        f"Equivalence must contain 'criteria', got keys: {list(equiv)}"
    )


@then('the virt_deferred equivalence criteria should contain "{field}"')
def step_equivalence_criteria_virt_deferred(context: object, field: str) -> None:
    """Assert the equivalence criteria contains the given field."""
    equiv = context.virt_deferred_schema.equivalence  # type: ignore[attr-defined]
    assert field in equiv["criteria"], (
        f"Expected '{field}' in criteria {equiv['criteria']}"
    )


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


@then("the virt_deferred domain model should be valid")
def step_domain_valid_virt_deferred(context: object) -> None:
    """Assert the domain model loaded successfully with required fields."""
    domain = context.virt_deferred_domain  # type: ignore[attr-defined]
    assert domain is not None, "Domain model should not be None"
    assert domain.name, "Domain model must have a non-empty name"
    assert domain.resource_kind is not None, "Domain model must have a resource_kind"
    assert domain.equivalence is not None, (
        f"Virtual domain model '{domain.name}' must have equivalence"
    )


@then('the virt_deferred domain model name should be "{name}"')
def step_domain_name_virt_deferred(context: object, name: str) -> None:
    """Assert the domain model name matches."""
    actual = context.virt_deferred_domain.name  # type: ignore[attr-defined]
    assert actual == name, f"Expected domain name '{name}', got '{actual}'"


@then('the virt_deferred domain model resource_kind should be "{kind}"')
def step_domain_kind_virt_deferred(context: object, kind: str) -> None:
    """Assert the domain model resource kind matches."""
    actual = context.virt_deferred_domain.resource_kind  # type: ignore[attr-defined]
    assert str(actual) == kind or actual.value == kind, (
        f"Expected resource_kind '{kind}', got '{actual}'"
    )


# ── Then steps: child/parent types ──────────────────────────


@then('the virt_deferred schema child_types should contain "{child}"')
def step_schema_child_virt_deferred(context: object, child: str) -> None:
    """Assert the schema child_types contains the given type."""
    types = context.virt_deferred_schema.child_types  # type: ignore[attr-defined]
    assert child in types, f"Expected '{child}' in child_types {types}"


@then("the virt_deferred schema should have empty parent_types")
def step_schema_no_parents_virt_deferred(context: object) -> None:
    """Assert the schema has no parent types."""
    parents = context.virt_deferred_schema.parent_types  # type: ignore[attr-defined]
    assert len(parents) == 0, f"Expected empty parent_types, got {parents}"


@then("the virt_deferred schema should have {count:d} cli_args")
def step_schema_cli_args_count_virt_deferred(context: object, count: int) -> None:
    """Assert the schema has the expected number of CLI args."""
    actual = len(context.virt_deferred_schema.cli_args)  # type: ignore[attr-defined]
    assert actual == count, f"Expected {count} cli_args, got {actual}"


@then("the virt_deferred schema handler should be none")
def step_schema_handler_none_virt_deferred(context: object) -> None:
    """Assert the schema has no handler."""
    handler = context.virt_deferred_schema.handler  # type: ignore[attr-defined]
    assert handler is None, f"Expected handler None, got '{handler}'"


# ── Then steps: registry ─────────────────────────────────────


@then('the registry should contain type "{name}" for virt_deferred')
def step_registry_contains_virt_deferred(context: object, name: str) -> None:
    """Assert the registry contains the named type."""
    types: dict[str, Any] = context.virt_deferred_builtin_types  # type: ignore[attr-defined]
    assert name in types, f"Type '{name}' not found in _BUILTIN_TYPES"


@then('the registry type "{name}" should not be user_addable for virt_deferred')
def step_registry_not_user_addable_virt_deferred(context: object, name: str) -> None:
    """Assert the named type in the registry is not user-addable."""
    types: dict[str, Any] = context.virt_deferred_builtin_types  # type: ignore[attr-defined]
    assert types[name]["user_addable"] is False, (
        f"Type '{name}' should not be user_addable"
    )


@then('the registry type "{name}" should be virtual for virt_deferred')
def step_registry_virtual_virt_deferred(context: object, name: str) -> None:
    """Assert the named type in the registry is virtual."""
    types: dict[str, Any] = context.virt_deferred_builtin_types  # type: ignore[attr-defined]
    assert types[name]["resource_kind"] == "virtual", f"Type '{name}' should be virtual"


@then('the registry type "{name}" should have equivalence for virt_deferred')
def step_registry_equivalence_virt_deferred(context: object, name: str) -> None:
    """Assert the named type in the registry has equivalence metadata."""
    types: dict[str, Any] = context.virt_deferred_builtin_types  # type: ignore[attr-defined]
    assert types[name].get("equivalence") is not None, (
        f"Type '{name}' should have equivalence metadata"
    )


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


@then('BUILTIN_NAMES should contain "{name}" for virt_deferred')
def step_builtin_names_contains_virt_deferred(context: object, name: str) -> None:
    """Assert BUILTIN_NAMES contains the given name."""
    names: frozenset[str] = context.virt_deferred_builtin_names  # type: ignore[attr-defined]
    assert name in names, f"'{name}' not in BUILTIN_NAMES: {sorted(names)}"


# ── Given/When/Then steps: DB roundtrip via bootstrap ────────


@given("a virt_deferred fresh in-memory resource registry with bootstrap")
def step_fresh_registry_virt_deferred(context: object) -> None:
    """Create an in-memory SQLite DB, bootstrap all 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:", echo=False)
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    context.virt_deferred_service = ResourceRegistryService(  # type: ignore[attr-defined]
        session_factory=factory,
    )


@when('I query the virt_deferred registry for type "{name}"')
def step_query_registry_virt_deferred(context: object, name: str) -> None:
    """Query the bootstrapped registry for a specific type."""
    from cleveragents.core.exceptions import NotFoundError

    service = context.virt_deferred_service  # type: ignore[attr-defined]
    try:
        context.virt_deferred_db_spec = service.show_type(name)  # type: ignore[attr-defined]
        context.virt_deferred_db_found = True  # type: ignore[attr-defined]
    except NotFoundError:
        context.virt_deferred_db_spec = None  # type: ignore[attr-defined]
        context.virt_deferred_db_found = False  # type: ignore[attr-defined]


@when("I list all virt_deferred registry types")
def step_list_types_virt_deferred(context: object) -> None:
    """List all types from the bootstrapped registry."""
    service = context.virt_deferred_service  # type: ignore[attr-defined]
    context.virt_deferred_type_list = [  # type: ignore[attr-defined]
        t.name for t in service.list_types()
    ]


@then('the virt_deferred db type "{name}" should exist')
def step_db_type_exists_virt_deferred(context: object, name: str) -> None:
    """Assert the type was found in the DB."""
    assert context.virt_deferred_db_found is True, (  # type: ignore[attr-defined]
        f"Type '{name}' not found after bootstrap"
    )


@then('the virt_deferred db type "{name}" should have kind "{kind}"')
def step_db_type_kind_virt_deferred(context: object, name: str, kind: str) -> None:
    """Assert the DB type has the expected resource kind."""
    spec = context.virt_deferred_db_spec  # type: ignore[attr-defined]
    actual = (
        str(spec.resource_kind.value)
        if hasattr(spec.resource_kind, "value")
        else str(spec.resource_kind)
    )
    assert actual == kind, f"Expected '{name}' kind '{kind}', got '{actual}'"


@then('the virt_deferred db type "{name}" should not be user_addable')
def step_db_type_not_user_addable_virt_deferred(context: object, name: str) -> None:
    """Assert the DB type is not user-addable."""
    spec = context.virt_deferred_db_spec  # type: ignore[attr-defined]
    assert spec.user_addable is False, (
        f"Type '{name}' should not be user_addable after roundtrip"
    )


@then(
    'the virt_deferred db type "{name}" should have equivalence with criteria "{criteria_csv}"'
)
def step_db_type_equivalence_virt_deferred(
    context: object,
    name: str,
    criteria_csv: str,
) -> None:
    """Assert the DB type has equivalence with expected criteria after roundtrip."""
    spec = context.virt_deferred_db_spec  # type: ignore[attr-defined]
    assert spec.equivalence is not None, (
        f"Type '{name}' should have equivalence after roundtrip"
    )
    expected = [c.strip() for c in criteria_csv.split(",")]
    actual = spec.equivalence.get("criteria", [])
    assert actual == expected, (
        f"Type '{name}' equivalence criteria: expected {expected}, got {actual}"
    )


@then('the virt_deferred type list should include "{name}"')
def step_type_list_includes_virt_deferred(context: object, name: str) -> None:
    """Assert the type name appears in the list_types result."""
    names: list[str] = context.virt_deferred_type_list  # type: ignore[attr-defined]
    assert name in names, f"'{name}' not in list_types result: {names}"


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


@when("I create a virt_deferred virtual type config without equivalence")
def step_create_virtual_no_equiv_virt_deferred(context: object) -> None:
    """Attempt to create a virtual ResourceTypeSpec without equivalence."""
    from pydantic import ValidationError

    context.virt_deferred_error = None  # type: ignore[attr-defined]
    try:
        ResourceTypeSpec.from_config(
            {
                "name": "test-virtual-no-equiv",
                "resource_kind": "virtual",
                "sandbox_strategy": "none",
                "built_in": True,
            }
        )
    except (ValueError, ValidationError) as exc:
        context.virt_deferred_error = exc  # type: ignore[attr-defined]


@when("I create a virt_deferred config without name")
def step_create_no_name_virt_deferred(context: object) -> None:
    """Attempt to create a ResourceTypeSpec without name."""
    context.virt_deferred_error = None  # type: ignore[attr-defined]
    try:
        ResourceTypeSpec.from_config(
            {
                "resource_kind": "virtual",
                "sandbox_strategy": "none",
            }
        )
    except ValueError as exc:
        context.virt_deferred_error = exc  # type: ignore[attr-defined]


@when("I create a virt_deferred config without resource_kind")
def step_create_no_kind_virt_deferred(context: object) -> None:
    """Attempt to create a ResourceTypeSpec without resource_kind."""
    context.virt_deferred_error = None  # type: ignore[attr-defined]
    try:
        ResourceTypeSpec.from_config(
            {
                "name": "test-missing-kind",
                "sandbox_strategy": "none",
            }
        )
    except ValueError as exc:
        context.virt_deferred_error = exc  # type: ignore[attr-defined]


@then("the virt_deferred config creation should raise a validation error")
def step_should_raise_validation_virt_deferred(context: object) -> None:
    """Assert that a validation error was raised."""
    err = context.virt_deferred_error  # type: ignore[attr-defined]
    assert err is not None, "Expected a validation error but none was raised"


@then("the virt_deferred config creation should raise a ValueError")
def step_should_raise_value_error_virt_deferred(context: object) -> None:
    """Assert that a ValueError was raised."""
    err = context.virt_deferred_error  # type: ignore[attr-defined]
    assert err is not None, "Expected a ValueError but none was raised"
    assert isinstance(err, ValueError), (
        f"Expected ValueError, got {type(err).__name__}: {err}"
    )


# ── When/Then steps: manual add rejection (F-001 / F-003) ───


@when(
    'I attempt to manually register a resource of type "{type_name}" for virt_deferred'
)
def step_attempt_manual_register_virt_deferred(context: object, type_name: str) -> None:
    """Attempt to register a resource instance of a non-user-addable type."""
    service = context.virt_deferred_service  # type: ignore[attr-defined]
    context.virt_deferred_register_error = None  # type: ignore[attr-defined]
    try:
        service.register_resource(
            type_name=type_name,
            name=f"test-manual-{type_name}",
        )
    except Exception as exc:
        context.virt_deferred_register_error = exc  # type: ignore[attr-defined]


@then("the virt_deferred manual registration should be rejected")
def step_manual_registration_rejected_virt_deferred(context: object) -> None:
    """Assert that manual registration was rejected."""
    err = context.virt_deferred_register_error  # type: ignore[attr-defined]
    assert err is not None, "Expected registration to be rejected but it succeeded"


@then('the virt_deferred rejection message should mention "{text}"')
def step_rejection_message_mentions_virt_deferred(context: object, text: str) -> None:
    """Assert the rejection error message contains expected text."""
    err = context.virt_deferred_register_error  # type: ignore[attr-defined]
    assert text in str(err), f"Expected '{text}' in error message, got: {err}"


# ── When steps: criteria validation (F-004) ──────────────────


@when("I create a virt_deferred virtual type config with criteria containing 123")
def step_create_criteria_non_string_virt_deferred(context: object) -> None:
    """Attempt to create a virtual type with non-string criteria element."""
    from pydantic import ValidationError

    context.virt_deferred_error = None  # type: ignore[attr-defined]
    try:
        ResourceTypeSpec.from_config(
            {
                "name": "test-bad-criteria-int",
                "resource_kind": "virtual",
                "sandbox_strategy": "none",
                "built_in": True,
                "equivalence": {"criteria": [123], "description": "bad"},
            }
        )
    except (ValueError, ValidationError) as exc:
        context.virt_deferred_error = exc  # type: ignore[attr-defined]


@when('I create a virt_deferred virtual type config with criteria containing ""')
def step_create_criteria_empty_string_virt_deferred(context: object) -> None:
    """Attempt to create a virtual type with empty-string criteria element."""
    from pydantic import ValidationError

    context.virt_deferred_error = None  # type: ignore[attr-defined]
    try:
        ResourceTypeSpec.from_config(
            {
                "name": "test-bad-criteria-empty",
                "resource_kind": "virtual",
                "sandbox_strategy": "none",
                "built_in": True,
                "equivalence": {"criteria": [""], "description": "bad"},
            }
        )
    except (ValueError, ValidationError) as exc:
        context.virt_deferred_error = exc  # type: ignore[attr-defined]
