"""Additional step implementations for decomposition coverage scenarios.

Exercises edge cases and code paths not covered by the primary
scenarios: config validation bounds, token estimation errors,
token_map parameter, closure trimming/clearing, service wrappers,
property access, empty-list helpers, and short-path directory keys.
"""

from __future__ import annotations

from typing import Any

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

from cleveragents.application.services.decomposition_graph import (
    DependencyClosureComputer,
)
from cleveragents.application.services.decomposition_models import (
    DecompositionConfig,
    DependencyEdge,
    DependencyGraph,
    DependencyType,
)

# ---------------------------------------------------------------------------
# Additional config validation
# ---------------------------------------------------------------------------


@when("I create a config with max_files_per_subplan {n:d}")
def step_when_create_bad_max_files(context: Any, n: int) -> None:
    try:
        DecompositionConfig(max_files_per_subplan=n)
    except ValueError as exc:
        context.error = exc


@when("I create a config with max_tokens_per_subplan {n:d}")
def step_when_create_bad_max_tokens(context: Any, n: int) -> None:
    try:
        DecompositionConfig(max_tokens_per_subplan=n)
    except ValueError as exc:
        context.error = exc


@when("I create a config with min_files_per_subplan {n:d}")
def step_when_create_bad_min_files(context: Any, n: int) -> None:
    try:
        DecompositionConfig(min_files_per_subplan=n)
    except ValueError as exc:
        context.error = exc


# ---------------------------------------------------------------------------
# estimate_tokens edge cases
# ---------------------------------------------------------------------------


@when("I estimate tokens for a nonexistent file")
def step_when_estimate_nonexistent(context: Any) -> None:
    from cleveragents.application.services.decomposition_clustering import (
        estimate_tokens_for_path,
    )

    context.token_estimate = estimate_tokens_for_path("/no/such/file.py")


@then("the token estimate should be {n:d}")
def step_then_token_estimate(context: Any, n: int) -> None:
    assert context.token_estimate == n


# ---------------------------------------------------------------------------
# cluster_by_size with token_map
# ---------------------------------------------------------------------------


@given("a set of files with known token counts")
def step_given_files_with_tokens(context: Any) -> None:
    context.sized_files = ["a.py", "b.py", "c.py", "d.py"]
    context.token_map = {"a.py": 40, "b.py": 60, "c.py": 30, "d.py": 70}


@when("I cluster by size with a token_map and max_tokens {n:d}")
def step_when_cluster_by_size_tokenmap(context: Any, n: int) -> None:
    from cleveragents.application.services.decomposition_clustering import (
        ClusteringStrategy,
    )

    context.sized_clusters = ClusteringStrategy.cluster_by_size(
        context.sized_files, n, token_map=context.token_map
    )


@then("the clusters should respect the token_map values")
def step_then_clusters_respect_tokenmap(context: Any) -> None:
    assert len(context.sized_clusters) >= 2
    for cluster in context.sized_clusters:
        total = sum(context.token_map[f] for f in cluster)
        assert total <= 100 + 70  # max_tokens + single large file


# ---------------------------------------------------------------------------
# closure trimming with multiple roots
# ---------------------------------------------------------------------------


@given("a dependency graph with two roots each reaching 3 nodes")
def step_given_two_root_graph(context: Any) -> None:
    g = DependencyGraph()
    for n in ("R1", "R1a", "R1b", "R2", "R2a", "R2b"):
        g.add_node(n)
    g.add_edge(DependencyEdge("R1", "R1a", DependencyType.IMPORT))
    g.add_edge(DependencyEdge("R1", "R1b", DependencyType.IMPORT))
    g.add_edge(DependencyEdge("R2", "R2a", DependencyType.IMPORT))
    g.add_edge(DependencyEdge("R2", "R2b", DependencyType.IMPORT))
    context.graph = g


@when("I compute closure from both roots with cutoff {n:d}")
def step_when_closure_both_roots_cutoff(context: Any, n: int) -> None:
    computer = DependencyClosureComputer()
    context.closure = computer.compute_closure(context.graph, ["R1", "R2"], cutoff=n)


# ---------------------------------------------------------------------------
# clear cache
# ---------------------------------------------------------------------------


@when("I compute closure from A and then clear the cache")
def step_when_closure_and_clear(context: Any) -> None:
    computer = DependencyClosureComputer()
    context.closure_before = computer.compute_closure(context.graph, ["A"])
    computer.clear_cache()
    context.closure_after = computer.compute_closure(context.graph, ["A"])


@then("computing closure from A again should still work")
def step_then_closure_after_clear(context: Any) -> None:
    assert context.closure_before == context.closure_after


# ---------------------------------------------------------------------------
# service wrapper methods
# ---------------------------------------------------------------------------


@when("I compute dependency order through the service")
def step_when_service_topo(context: Any) -> None:
    context.service_order = context.svc.compute_dependency_order(context.graph)


@then("A should appear before B and C in the service order")
def step_then_service_a_before_bc(context: Any) -> None:
    order = context.service_order
    assert order.index("A") < order.index("B")
    assert order.index("A") < order.index("C")


@then("B and C should appear before D in the service order")
def step_then_service_bc_before_d(context: Any) -> None:
    order = context.service_order
    assert order.index("B") < order.index("D")
    assert order.index("C") < order.index("D")


@when("I compute closure through the service from A")
def step_when_service_closure(context: Any) -> None:
    context.service_closure = context.svc.compute_closure(context.graph, ["A"])


@then("the service closure should contain A, B, C, D")
def step_then_service_closure_abcd(context: Any) -> None:
    assert {"A", "B", "C", "D"} <= context.service_closure


# ---------------------------------------------------------------------------
# decision_service property
# ---------------------------------------------------------------------------


@then("the decision service property should return the mock")
def step_then_ds_property_mock(context: Any) -> None:
    assert context.svc.decision_service is context.mock_ds


@then("the decision service property should return None")
def step_then_ds_property_none(context: Any) -> None:
    assert context.svc.decision_service is None


# ---------------------------------------------------------------------------
# empty file edge cases
# ---------------------------------------------------------------------------


@when("I compute dominant extension of an empty list")
def step_when_dominant_ext_empty(context: Any) -> None:
    from cleveragents.application.services.decomposition_service import (
        _dominant_extension,
    )

    context.dominant_ext = _dominant_extension([])


@then("the dominant extension should be empty")
def step_then_dominant_ext_empty(context: Any) -> None:
    assert context.dominant_ext == ""


@when("I compute common prefix of an empty list")
def step_when_common_prefix_empty(context: Any) -> None:
    from cleveragents.application.services.decomposition_service import (
        _common_prefix,
    )

    context.common_pfx = _common_prefix([])


@then("the common prefix should be empty")
def step_then_common_prefix_empty(context: Any) -> None:
    assert context.common_pfx == ""


# ---------------------------------------------------------------------------
# directory key short path
# ---------------------------------------------------------------------------


@when("I compute directory key for a single-component path")
def step_when_dir_key_short(context: Any) -> None:
    from cleveragents.application.services.decomposition_clustering import (
        _directory_key,
    )

    context.dir_key = _directory_key("file.py", depth=2)


@then("the directory key should be empty string")
def step_then_dir_key_empty(context: Any) -> None:
    assert context.dir_key == ""
