"""Step definitions for resource_dag.feature."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import Any

from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker

from cleveragents.domain.models.core.resource import (
    PhysVirt,
    Resource,
    ResourceCapabilities,
)
from cleveragents.domain.models.core.resource_type import (
    ResourceKind,
    ResourceTypeSpec,
    SandboxStrategy,
)
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    CycleDetectedError,
    DuplicateResourceLinkError,
    LinkNotFoundError,
    ResourceNotFoundRepoError,
    ResourceRepository,
    ResourceTypeRepository,
    TypeIncompatibleError,
)

# Crockford base32 alphabet for generating ULIDs
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
_DAG_CTR = 0


def _next_ulid() -> str:
    """Return a unique, valid ULID string."""
    global _DAG_CTR
    _DAG_CTR += 1
    n = _DAG_CTR
    suffix = ""
    for _ in range(16):
        suffix = _CB32[n % 32] + suffix
        n //= 32
    return f"01HDAGT0AD{suffix}"[:26]


def _make_type_spec(
    name: str,
    child_types: list[str] | None = None,
    auto_discovery: dict[str, Any] | None = None,
) -> ResourceTypeSpec:
    """Create a ResourceTypeSpec."""
    return ResourceTypeSpec(
        name=name,
        description=f"Test type {name}",
        resource_kind=ResourceKind.PHYSICAL,
        sandbox_strategy=SandboxStrategy.NONE,
        user_addable=True,
        cli_args=[],
        parent_types=[],
        child_types=child_types or [],
        auto_discovery=auto_discovery,
        equivalence=None,
        handler=None,
        capabilities={
            "read": True,
            "write": True,
            "sandbox": True,
            "checkpoint": False,
        },
        built_in=False,
    )


def _make_resource(
    resource_id: str,
    type_name: str,
) -> Resource:
    """Create a Resource domain object."""
    return Resource(
        resource_id=resource_id,
        name=None,
        resource_type_name=type_name,
        classification=PhysVirt.PHYSICAL,
        description=f"Test resource {resource_id}",
        properties={},
        location=None,
        content_hash=None,
        sandbox_strategy=None,
        capabilities=ResourceCapabilities(),
        created_at=datetime.now(tz=UTC),
        updated_at=datetime.now(tz=UTC),
    )


# ── Background ─────────────────────────────────────────


@given("a clean resource DAG database")
def step_clean_dag_db(context: Context) -> None:
    engine = create_engine("sqlite:///:memory:")

    @event.listens_for(engine, "connect")
    def _fk(conn: Any, _rec: Any) -> None:
        conn.cursor().execute("PRAGMA foreign_keys=ON")

    Base.metadata.create_all(engine)
    context.dag_engine = engine  # type: ignore[attr-defined]
    context.dag_factory = sessionmaker(  # type: ignore[attr-defined]
        bind=engine
    )
    context.dag_resources = {}  # type: ignore[attr-defined]
    context.dag_error = None  # type: ignore[attr-defined]
    context.dag_result = None  # type: ignore[attr-defined]


@given("a resource type repository for DAG tests")
def step_rt_repo(context: Context) -> None:
    context.dag_rt_repo = ResourceTypeRepository(  # type: ignore[attr-defined]
        context.dag_factory  # type: ignore[attr-defined]
    )


@given("a resource repository for DAG tests")
def step_res_repo(context: Context) -> None:
    context.dag_res_repo = ResourceRepository(  # type: ignore[attr-defined]
        context.dag_factory  # type: ignore[attr-defined]
    )


# ── Type setup ──────────────────────────────────────────


@given("a DAG parent type \"{name}\" allowing children '{child_json}'")
def step_dag_parent_type(context: Context, name: str, child_json: str) -> None:
    child_list: list[str] = json.loads(child_json)
    spec = _make_type_spec(name, child_types=child_list)
    context.dag_rt_repo.create(spec)  # type: ignore[attr-defined]


@given('a DAG child type "{name}"')
def step_dag_child_type(context: Context, name: str) -> None:
    spec = _make_type_spec(name)
    context.dag_rt_repo.create(spec)  # type: ignore[attr-defined]


@given('a DAG parent type "{name}" with auto-discovery for "{child_type}"')
def step_dag_parent_auto(context: Context, name: str, child_type: str) -> None:
    auto_disc = {
        "enabled": True,
        "rules": [{"type": child_type, "pattern": "*"}],
    }
    spec = _make_type_spec(
        name,
        child_types=[child_type],
        auto_discovery=auto_disc,
    )
    context.dag_rt_repo.create(spec)  # type: ignore[attr-defined]


@given('a DAG parent type "{name}" without auto-discovery')
def step_dag_parent_no_disc(context: Context, name: str) -> None:
    spec = _make_type_spec(name)
    context.dag_rt_repo.create(spec)  # type: ignore[attr-defined]


# ── Resource setup ──────────────────────────────────────


@given('a DAG resource "{label}" typed "{type_name}"')
def step_dag_resource(context: Context, label: str, type_name: str) -> None:
    rid = _next_ulid()
    res = _make_resource(rid, type_name)
    context.dag_res_repo.create(res)  # type: ignore[attr-defined]
    context.dag_resources[label] = rid  # type: ignore[attr-defined]


# ── Link setup ──────────────────────────────────────────


@given('DAG child "{child}" is already linked to parent "{parent}"')
def step_dag_already_linked(context: Context, child: str, parent: str) -> None:
    pid = context.dag_resources[parent]  # type: ignore[attr-defined]
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    context.dag_res_repo.link_child(pid, cid)  # type: ignore[attr-defined]


# ── When: link / unlink ────────────────────────────────


@when('DAG child "{child}" is linked to parent "{parent}"')
def step_dag_link_child(context: Context, child: str, parent: str) -> None:
    pid = context.dag_resources[parent]  # type: ignore[attr-defined]
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.link_child(pid, cid)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except (
        CycleDetectedError,
        TypeIncompatibleError,
        DuplicateResourceLinkError,
        ResourceNotFoundRepoError,
    ) as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


@when('DAG child "{child}" is linked to parent "{parent}" again')
def step_dag_link_again(context: Context, child: str, parent: str) -> None:
    pid = context.dag_resources[parent]  # type: ignore[attr-defined]
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.link_child(pid, cid)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except DuplicateResourceLinkError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


@when('DAG child "{child}" is unlinked from parent "{parent}"')
def step_dag_unlink(context: Context, child: str, parent: str) -> None:
    pid = context.dag_resources[parent]  # type: ignore[attr-defined]
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.unlink_child(pid, cid)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except (
        ResourceNotFoundRepoError,
        LinkNotFoundError,
    ) as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


@when('DAG linking child "{child}" to missing parent "{parent_id}"')
def step_dag_link_missing_parent(context: Context, child: str, parent_id: str) -> None:
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.link_child(parent_id, cid)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except ResourceNotFoundRepoError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


@when('DAG linking missing child "{child_id}" to parent "{parent}"')
def step_dag_link_missing_child(context: Context, child_id: str, parent: str) -> None:
    pid = context.dag_resources[parent]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.link_child(pid, child_id)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except ResourceNotFoundRepoError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


@when('DAG resource "{res}" is linked to itself')
def step_dag_self_link(context: Context, res: str) -> None:
    rid = context.dag_resources[res]  # type: ignore[attr-defined]
    try:
        context.dag_res_repo.link_child(rid, rid)  # type: ignore[attr-defined]
        context.dag_error = None  # type: ignore[attr-defined]
    except CycleDetectedError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]


# ── When: auto_discover ─────────────────────────────────


@when('DAG auto_discover_children is called for "{label}"')
def step_dag_auto_discover(context: Context, label: str) -> None:
    rid = context.dag_resources[label]  # type: ignore[attr-defined]
    try:
        context.dag_result = (  # type: ignore[attr-defined]
            context.dag_res_repo.auto_discover_children(  # type: ignore[attr-defined]
                rid
            )
        )
        context.dag_error = None  # type: ignore[attr-defined]
    except ResourceNotFoundRepoError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]
        context.dag_result = []  # type: ignore[attr-defined]


@when('DAG auto_discover_children is called for missing "{resource_id}"')
def step_dag_auto_discover_missing(context: Context, resource_id: str) -> None:
    try:
        context.dag_result = (  # type: ignore[attr-defined]
            context.dag_res_repo.auto_discover_children(  # type: ignore[attr-defined]
                resource_id
            )
        )
        context.dag_error = None  # type: ignore[attr-defined]
    except ResourceNotFoundRepoError as exc:
        context.dag_error = exc  # type: ignore[attr-defined]
        context.dag_result = []  # type: ignore[attr-defined]


# ── When: traversal ─────────────────────────────────────


@when('DAG children of "{label}" are retrieved')
def step_dag_get_children(context: Context, label: str) -> None:
    rid = context.dag_resources[label]  # type: ignore[attr-defined]
    context.dag_result = (  # type: ignore[attr-defined]
        context.dag_res_repo.get_children(rid)  # type: ignore[attr-defined]
    )


@when('DAG parents of "{label}" are retrieved')
def step_dag_get_parents(context: Context, label: str) -> None:
    rid = context.dag_resources[label]  # type: ignore[attr-defined]
    context.dag_result = (  # type: ignore[attr-defined]
        context.dag_res_repo.get_parents(rid)  # type: ignore[attr-defined]
    )


# ── Then: assertions ────────────────────────────────────


@then("the DAG link should succeed without error")
def step_dag_link_ok(context: Context) -> None:
    assert context.dag_error is None, (  # type: ignore[attr-defined]
        f"Expected no error, got: {context.dag_error}"  # type: ignore[attr-defined]
    )


@then("the DAG unlink should succeed without error")
def step_dag_unlink_ok(context: Context) -> None:
    assert context.dag_error is None, (  # type: ignore[attr-defined]
        f"Expected no error, got: {context.dag_error}"  # type: ignore[attr-defined]
    )


@then('DAG children of "{label}" should include "{child}"')
def step_dag_children_include(context: Context, label: str, child: str) -> None:
    pid = context.dag_resources[label]  # type: ignore[attr-defined]
    cid = context.dag_resources[child]  # type: ignore[attr-defined]
    children = context.dag_res_repo.get_children(pid)  # type: ignore[attr-defined]
    child_ids = [c.resource_id for c in children]
    assert cid in child_ids, f"Child {cid} not in children {child_ids}"


@then('DAG children of "{label}" should be empty')
def step_dag_children_empty(context: Context, label: str) -> None:
    pid = context.dag_resources[label]  # type: ignore[attr-defined]
    children = context.dag_res_repo.get_children(pid)  # type: ignore[attr-defined]
    assert len(children) == 0, f"Expected 0 children, got {len(children)}"


@then("a DAG ResourceNotFoundRepoError should be raised")
def step_dag_not_found(context: Context) -> None:
    assert isinstance(
        context.dag_error,  # type: ignore[attr-defined]
        ResourceNotFoundRepoError,
    ), (
        f"Expected ResourceNotFoundRepoError, got "
        f"{type(context.dag_error).__name__}: "  # type: ignore[attr-defined]
        f"{context.dag_error}"  # type: ignore[attr-defined]
    )


@then("a DAG CycleDetectedError should be raised")
def step_dag_cycle_error(context: Context) -> None:
    assert isinstance(
        context.dag_error,  # type: ignore[attr-defined]
        CycleDetectedError,
    ), (
        f"Expected CycleDetectedError, got "
        f"{type(context.dag_error).__name__}: "  # type: ignore[attr-defined]
        f"{context.dag_error}"  # type: ignore[attr-defined]
    )


@then("a DAG TypeIncompatibleError should be raised")
def step_dag_type_error(context: Context) -> None:
    assert isinstance(
        context.dag_error,  # type: ignore[attr-defined]
        TypeIncompatibleError,
    ), (
        f"Expected TypeIncompatibleError, got "
        f"{type(context.dag_error).__name__}: "  # type: ignore[attr-defined]
        f"{context.dag_error}"  # type: ignore[attr-defined]
    )


@then("a DAG DuplicateResourceLinkError should be raised")
def step_dag_dup_link(context: Context) -> None:
    assert isinstance(
        context.dag_error,  # type: ignore[attr-defined]
        DuplicateResourceLinkError,
    ), (
        f"Expected DuplicateResourceLinkError, got "
        f"{type(context.dag_error).__name__}: "  # type: ignore[attr-defined]
        f"{context.dag_error}"  # type: ignore[attr-defined]
    )


@then("at least {count:d} DAG child should be created")
def step_dag_at_least_n(context: Context, count: int) -> None:
    result = context.dag_result or []  # type: ignore[attr-defined]
    assert len(result) >= count, f"Expected >= {count} children, got {len(result)}"


@then("{count:d} DAG children should be created")
def step_dag_exact_n(context: Context, count: int) -> None:
    result = context.dag_result or []  # type: ignore[attr-defined]
    assert len(result) == count, f"Expected {count} children, got {len(result)}"


@then('DAG created children should be typed "{type_name}"')
def step_dag_children_typed(context: Context, type_name: str) -> None:
    result = context.dag_result or []  # type: ignore[attr-defined]
    for child in result:
        assert child.resource_type_name == type_name, (
            f"Expected type {type_name}, got {child.resource_type_name}"
        )


@then('DAG created children should be linked to "{label}"')
def step_dag_children_linked(context: Context, label: str) -> None:
    pid = context.dag_resources[label]  # type: ignore[attr-defined]
    children = context.dag_res_repo.get_children(pid)  # type: ignore[attr-defined]
    result = context.dag_result or []  # type: ignore[attr-defined]
    child_ids = {c.resource_id for c in children}
    for created in result:
        assert created.resource_id in child_ids, (
            f"Created child {created.resource_id} not linked to parent {pid}"
        )


@then("{count:d} DAG children should be returned")
def step_dag_n_children(context: Context, count: int) -> None:
    result = context.dag_result or []  # type: ignore[attr-defined]
    assert len(result) == count, f"Expected {count} children, got {len(result)}"


@then("{count:d} DAG parents should be returned")
def step_dag_n_parents(context: Context, count: int) -> None:
    result = context.dag_result or []  # type: ignore[attr-defined]
    assert len(result) == count, f"Expected {count} parents, got {len(result)}"
