"""Step definitions for lsp_discovery_coverage.feature.

Targets uncovered lines in src/cleveragents/lsp/discovery.py:
  - Line 142:     _detect_from_shebang returns None (no shebang prefix)
  - Line 146:     _detect_from_shebang returns None (empty after #!)
  - Line 157:     _detect_from_shebang returns None (unknown interpreter)
  - Line 201:     detect_file_language cache hit
  - Lines 219-231: detect_directory_languages body + OSError branch
  - Line 234:     return sorted(languages) from detect_directory_languages
  - Line 246:     invalidate_all clears cache
  - Line 257:     Makefile basename detection
  - Line 259:     CMakeLists.txt basename detection
  - Line 276:     UKO classification layer
  - Lines 298-301: get_servers_for_language list comprehension
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import patch

from behave import given, then, when
from behave.runner import Context

from cleveragents.lsp.discovery import LanguageDiscovery, _detect_from_shebang

# ── helpers ──────────────────────────────────────────────────────────


def _make_config(languages: list[str]) -> SimpleNamespace:
    """Create a lightweight object with a ``languages`` attribute."""
    return SimpleNamespace(languages=languages)


# ── shebang givens ───────────────────────────────────────────────────


@given('ldcov a first line "{line}"')
def step_ldcov_given_first_line(context: Context, line: str) -> None:
    context.ldcov_first_line = line


# ── shebang whens ────────────────────────────────────────────────────


@when("ldcov I detect from shebang")
def step_ldcov_when_detect_shebang(context: Context) -> None:
    context.ldcov_shebang_result = _detect_from_shebang(context.ldcov_first_line)


# ── shebang thens ────────────────────────────────────────────────────


@then("ldcov the shebang result is None")
def step_ldcov_then_shebang_none(context: Context) -> None:
    assert context.ldcov_shebang_result is None, (
        f"Expected None, got {context.ldcov_shebang_result!r}"
    )


# ── LanguageDiscovery instance ───────────────────────────────────────


@given("ldcov a LanguageDiscovery instance")
def step_ldcov_given_discovery(context: Context) -> None:
    context.ldcov_discovery = LanguageDiscovery()


@given('ldcov a LanguageDiscovery instance with UKO mapping "{path}" to "{lang}"')
def step_ldcov_given_discovery_with_uko(context: Context, path: str, lang: str) -> None:
    context.ldcov_discovery = LanguageDiscovery(uko_classifications={path: lang})


# ── cache manipulation ───────────────────────────────────────────────


@given('ldcov the cache already contains "{path}" mapped to "{lang}"')
def step_ldcov_given_cache_entry(context: Context, path: str, lang: str) -> None:
    context.ldcov_discovery._cache[path] = lang


# ── detect_file_language ─────────────────────────────────────────────


@when('ldcov I detect file language for "{path}"')
def step_ldcov_when_detect_file(context: Context, path: str) -> None:
    context.ldcov_result = context.ldcov_discovery.detect_file_language(path)


# ── detect via _detect_uncached (avoids real file I/O for shebang) ──


@when('ldcov I detect uncached language for "{path}"')
def step_ldcov_when_detect_uncached(context: Context, path: str) -> None:
    # Mock open so Layer-2 (shebang) doesn't try to read a real file.
    patcher = patch("builtins.open", side_effect=OSError("mocked"))
    patcher.start()
    context.add_cleanup(patcher.stop)
    context.ldcov_result = context.ldcov_discovery._detect_uncached(path)


# ── invalidate_all ───────────────────────────────────────────────────


@when("ldcov I call invalidate_all")
def step_ldcov_when_invalidate_all(context: Context) -> None:
    context.ldcov_discovery.invalidate_all()


@then("ldcov the cache is empty")
def step_ldcov_then_cache_empty(context: Context) -> None:
    assert len(context.ldcov_discovery._cache) == 0, (
        f"Cache still has {len(context.ldcov_discovery._cache)} entries"
    )


# ── detect_directory_languages (happy path) ──────────────────────────


@given('ldcov a mock directory tree with files "{file_list}"')
def step_ldcov_given_mock_dir(context: Context, file_list: str) -> None:
    filenames = [f.strip() for f in file_list.split(",")]
    mock_root = "/fake/project"
    context.ldcov_mock_dir = mock_root

    # Mock os.walk to yield our fake file list
    walk_result = [(mock_root, [], filenames)]
    patcher_walk = patch(
        "cleveragents.lsp.discovery.os.walk", return_value=iter(walk_result)
    )
    patcher_walk.start()
    context.add_cleanup(patcher_walk.stop)

    # Mock open so shebang analysis doesn't hit the real filesystem.
    patcher_open = patch("builtins.open", side_effect=OSError("mocked"))
    patcher_open.start()
    context.add_cleanup(patcher_open.stop)


@when("ldcov I detect directory languages")
def step_ldcov_when_detect_dir(context: Context) -> None:
    context.ldcov_dir_result = context.ldcov_discovery.detect_directory_languages(
        context.ldcov_mock_dir,
    )


@then('ldcov the directory languages include "{lang}"')
def step_ldcov_then_dir_includes(context: Context, lang: str) -> None:
    assert lang in context.ldcov_dir_result, (
        f"Expected {lang!r} in {context.ldcov_dir_result}"
    )


# ── detect_directory_languages (OSError branch) ─────────────────────


@given("ldcov os.walk will raise an OSError")
def step_ldcov_given_walk_oserror(context: Context) -> None:
    patcher = patch(
        "cleveragents.lsp.discovery.os.walk",
        side_effect=OSError("permission denied"),
    )
    patcher.start()
    context.add_cleanup(patcher.stop)


@when('ldcov I detect directory languages for "{directory}"')
def step_ldcov_when_detect_dir_for(context: Context, directory: str) -> None:
    context.ldcov_dir_result = context.ldcov_discovery.detect_directory_languages(
        directory,
    )


@then("ldcov the directory languages result is an empty list")
def step_ldcov_then_dir_empty(context: Context) -> None:
    assert context.ldcov_dir_result == [], (
        f"Expected empty list, got {context.ldcov_dir_result!r}"
    )


# ── get_servers_for_language ─────────────────────────────────────────


@given('ldcov server configs with languages "{langs_a}" and "{langs_b}"')
def step_ldcov_given_server_configs(
    context: Context, langs_a: str, langs_b: str
) -> None:
    config_a = _make_config([lang.strip() for lang in langs_a.split(",")])
    config_b = _make_config([lang.strip() for lang in langs_b.split(",")])
    context.ldcov_configs = [config_a, config_b]


@when('ldcov I get servers for language "{lang}"')
def step_ldcov_when_get_servers(context: Context, lang: str) -> None:
    context.ldcov_matched = context.ldcov_discovery.get_servers_for_language(
        lang,
        context.ldcov_configs,
    )


@then("ldcov the matched server count is {count:d}")
def step_ldcov_then_matched_count(context: Context, count: int) -> None:
    assert len(context.ldcov_matched) == count, (
        f"Expected {count} matched configs, got {len(context.ldcov_matched)}"
    )


# ── generic language assertion ───────────────────────────────────────


@then('ldcov the detected language is "{expected}"')
def step_ldcov_then_detected_lang(context: Context, expected: str) -> None:
    assert context.ldcov_result == expected, (
        f"Expected {expected!r}, got {context.ldcov_result!r}"
    )
