"""Step implementations for decomposition_clustering_coverage_boost.feature.

Targets uncovered lines in decomposition_clustering.py:
  - Lines 122-131: ClusteringStrategy.cluster_by_language()
  - Line 160: estimate_tokens_for_path() fallback in cluster_by_size()
"""

from __future__ import annotations

import os
import tempfile
from typing import Any

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

from cleveragents.application.services.decomposition_clustering import (
    ClusteringStrategy,
    _extension_of,
)

# ---------------------------------------------------------------------------
# Givens — file lists
# ---------------------------------------------------------------------------


@given("a list of mixed-language file paths")
def step_given_mixed_language_files(context: Any) -> None:
    context.file_paths = [
        "src/app/main.py",
        "src/app/utils.py",
        "src/app/helpers.py",
        "src/web/index.ts",
        "src/web/router.ts",
        "src/web/views.ts",
    ]


@given("a list of {count:d} Python file paths")
def step_given_n_python_files(context: Any, count: int) -> None:
    context.file_paths = [f"src/mod_{i:03d}.py" for i in range(count)]


@given("an empty list of file paths")
def step_given_empty_file_list(context: Any) -> None:
    context.file_paths = []


@given("an unsorted list of mixed-language file paths")
def step_given_unsorted_mixed_files(context: Any) -> None:
    context.file_paths = [
        "z_last.py",
        "a_first.ts",
        "m_middle.py",
        "b_second.ts",
    ]


@given("a list of files with js py and ts extensions")
def step_given_js_py_ts_files(context: Any) -> None:
    context.file_paths = [
        "app.js",
        "main.py",
        "index.ts",
    ]


# ---------------------------------------------------------------------------
# Givens — temporary files with known sizes (for line 160 fallback)
# ---------------------------------------------------------------------------


@given("a set of temporary files with known byte sizes")
def step_given_temp_files_with_sizes(context: Any) -> None:
    context.clustering_tmpdir = tempfile.mkdtemp(prefix="clust-cov-")
    # Create files with specific byte sizes.
    # _BYTES_PER_TOKEN = 4, so tokens = max(size // 4, 1)
    # 200 bytes -> 50 tokens; 80 bytes -> 20 tokens; 40 bytes -> 10 tokens
    context.temp_files = []
    sizes = [200, 80, 40, 200, 80]
    for i, size in enumerate(sizes):
        fpath = os.path.join(context.clustering_tmpdir, f"file_{i}.py")
        with open(fpath, "w") as fh:
            fh.write("x" * size)
        context.temp_files.append(fpath)


@given("a partial token_map covering only some files")
def step_given_partial_token_map(context: Any) -> None:
    # Only map the first two files; the rest fall through to
    # estimate_tokens_for_path (line 160).
    context.partial_token_map = {
        context.temp_files[0]: 50,
        context.temp_files[1]: 20,
    }


# ---------------------------------------------------------------------------
# Whens — cluster_by_language
# ---------------------------------------------------------------------------


@when("I cluster by language with max_per_cluster {n:d}")
def step_when_cluster_by_language(context: Any, n: int) -> None:
    context.lang_clusters = ClusteringStrategy.cluster_by_language(
        context.file_paths, n
    )


# ---------------------------------------------------------------------------
# Whens — cluster_by_size fallback (line 160)
# ---------------------------------------------------------------------------


@when("I cluster by size with max_tokens {n:d} and no token_map")
def step_when_cluster_by_size_no_map(context: Any, n: int) -> None:
    context.size_clusters = ClusteringStrategy.cluster_by_size(
        context.temp_files, n, token_map=None
    )


@when("I cluster by size with max_tokens {n:d} and the partial token_map")
def step_when_cluster_by_size_partial_map(context: Any, n: int) -> None:
    context.size_clusters = ClusteringStrategy.cluster_by_size(
        context.temp_files, n, token_map=context.partial_token_map
    )


# ---------------------------------------------------------------------------
# Thens — cluster_by_language assertions
# ---------------------------------------------------------------------------


@then("the language clusters should separate Python and TypeScript files")
def step_then_clusters_separate_py_ts(context: Any) -> None:
    clusters = context.lang_clusters
    assert len(clusters) >= 2, f"expected >=2 clusters, got {len(clusters)}"
    extensions_per_cluster = [
        {_extension_of(f) for f in cluster} for cluster in clusters
    ]
    # Each cluster should be homogeneous (single extension)
    for exts in extensions_per_cluster:
        assert len(exts) == 1, f"cluster has mixed extensions: {exts}"
    # There should be both .py and .ts across clusters
    all_exts = {ext for exts in extensions_per_cluster for ext in exts}
    assert ".py" in all_exts
    assert ".ts" in all_exts


@then("each language cluster should contain only one extension")
def step_then_each_cluster_single_ext(context: Any) -> None:
    for cluster in context.lang_clusters:
        exts = {_extension_of(f) for f in cluster}
        assert len(exts) == 1, f"cluster has mixed extensions: {exts}"


@then("the language clusters should contain at least {n:d} clusters")
def step_then_at_least_n_clusters(context: Any, n: int) -> None:
    assert len(context.lang_clusters) >= n, (
        f"expected >= {n} clusters, got {len(context.lang_clusters)}"
    )


@then("no language cluster should exceed {n:d} files")
def step_then_no_cluster_exceeds(context: Any, n: int) -> None:
    for cluster in context.lang_clusters:
        assert len(cluster) <= n, f"cluster has {len(cluster)} files, max allowed {n}"


@then("exactly {n:d} language cluster should be returned")
def step_then_exactly_n_clusters(context: Any, n: int) -> None:
    assert len(context.lang_clusters) == n, (
        f"expected {n} clusters, got {len(context.lang_clusters)}"
    )


@then("no language clusters should be returned")
def step_then_no_clusters(context: Any) -> None:
    assert len(context.lang_clusters) == 0, (
        f"expected 0 clusters, got {len(context.lang_clusters)}"
    )


@then("files within each language cluster should be in sorted order")
def step_then_files_sorted_within_clusters(context: Any) -> None:
    for cluster in context.lang_clusters:
        assert cluster == sorted(cluster), f"cluster not sorted: {cluster}"


@then("the language clusters should be ordered by extension alphabetically")
def step_then_clusters_ordered_by_extension(context: Any) -> None:
    cluster_exts = []
    for cluster in context.lang_clusters:
        exts = {_extension_of(f) for f in cluster}
        assert len(exts) == 1
        cluster_exts.append(exts.pop())
    assert cluster_exts == sorted(cluster_exts), (
        f"cluster extensions not in order: {cluster_exts}"
    )


# ---------------------------------------------------------------------------
# Thens — cluster_by_size fallback assertions
# ---------------------------------------------------------------------------


@then("the size clusters should respect the estimated token limits")
def step_then_size_clusters_respect_limits(context: Any) -> None:
    clusters = context.size_clusters
    assert len(clusters) >= 1, "expected at least 1 cluster"
    # All input files must appear exactly once across clusters
    all_files = [f for cluster in clusters for f in cluster]
    assert sorted(all_files) == sorted(context.temp_files)
    # Verify that we got more than one cluster (token budget is tight)
    assert len(clusters) >= 2, (
        f"expected >= 2 clusters with tight budget, got {len(clusters)}"
    )


@then("the size clusters should include all files")
def step_then_size_clusters_include_all(context: Any) -> None:
    all_files = [f for cluster in context.size_clusters for f in cluster]
    assert sorted(all_files) == sorted(context.temp_files), (
        "not all files present in clusters"
    )
