"""Step definitions for repo_indexing_utils_coverage.feature.

These steps target specific uncovered lines in repo_indexing_utils.py:
- Line 118: detect_language Dockerfile special-case return
- Line 175: _glob_match with '**' or '/' patterns (PurePosixPath.match)
- Lines 266, 268, 269: directory filtering in walk_and_index
- Line 274: hidden file skip
- Line 281: symlink skip
- Lines 285-286: ValueError from relative_to
- Lines 292-296: path > 1024 chars skip
- Lines 306-307: stat() OSError skip
- Line 312: non-regular file (FIFO) skip
- Lines 338-342: max_file_count cutoff
"""

import os
import tempfile
from pathlib import Path
from unittest.mock import patch

from behave import given, then, when

from cleveragents.application.services.repo_indexing_utils import (
    detect_language,
    matches_policy,
    walk_and_index,
)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the repo indexing utils module is imported")
def step_module_imported(context):
    """Ensure the module is importable."""
    assert detect_language is not None
    assert matches_policy is not None
    assert walk_and_index is not None


# ---------------------------------------------------------------------------
# detect_language: Dockerfile (line 118)
# ---------------------------------------------------------------------------
@when('I detect the language for path "{path}"')
def step_detect_language(context, path):
    context.detected_language = detect_language(path)


@then('the detected language should be "{expected}"')
def step_verify_language(context, expected):
    assert context.detected_language == expected, (
        f"Expected '{expected}', got '{context.detected_language}'"
    )


# ---------------------------------------------------------------------------
# matches_policy: recursive / path globs (line 175)
# ---------------------------------------------------------------------------
@when('I check matches_policy for "{rel_path}" with include glob "{pattern}"')
def step_check_matches_policy(context, rel_path, pattern):
    context.policy_result = matches_policy(
        rel_path,
        include_globs=(pattern,),
        exclude_globs=(),
    )


@then("the path should match the policy")
def step_path_matches(context):
    assert context.policy_result is True, "Expected path to match policy"


@then("the path should not match the policy")
def step_path_not_matches(context):
    assert context.policy_result is False, "Expected path NOT to match policy"


# ---------------------------------------------------------------------------
# walk_and_index: hidden / special directories filtered (lines 266-269)
# ---------------------------------------------------------------------------
@given("a temporary directory with hidden and special subdirectories")
def step_create_dir_with_hidden_and_special(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir = tmpdir

    # Hidden directory — should be skipped
    os.makedirs(os.path.join(tmpdir, ".git"))
    with open(os.path.join(tmpdir, ".git", "config"), "w") as f:
        f.write("gitconfig")

    # __pycache__ — should be skipped
    os.makedirs(os.path.join(tmpdir, "__pycache__"))
    with open(os.path.join(tmpdir, "__pycache__", "mod.pyc"), "w") as f:
        f.write("bytecode")

    # node_modules — should be skipped
    os.makedirs(os.path.join(tmpdir, "node_modules"))
    with open(os.path.join(tmpdir, "node_modules", "pkg.js"), "w") as f:
        f.write("js")

    # venv — should be skipped
    os.makedirs(os.path.join(tmpdir, "venv"))
    with open(os.path.join(tmpdir, "venv", "activate.py"), "w") as f:
        f.write("activate")

    # Visible directory with a real file — should be indexed
    os.makedirs(os.path.join(tmpdir, "src"))
    with open(os.path.join(tmpdir, "src", "main.py"), "w") as f:
        f.write("print('hello')")

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index on the temporary directory")
def step_walk_and_index_tmpdir(context):
    context.walk_results = walk_and_index(
        Path(context.tmpdir),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
    )


@then("only files from visible non-special directories should be indexed")
def step_verify_only_visible_dirs(context):
    paths = [r.path for r in context.walk_results]
    # The only file should be src/main.py
    assert len(paths) == 1, f"Expected 1 file, got {len(paths)}: {paths}"
    assert paths[0] == os.path.join("src", "main.py"), (
        f"Expected 'src/main.py', got '{paths[0]}'"
    )


# ---------------------------------------------------------------------------
# walk_and_index: hidden files skipped (line 274)
# ---------------------------------------------------------------------------
@given("a temporary directory with hidden files")
def step_create_dir_with_hidden_files(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_hidden = tmpdir

    # Hidden file — should be skipped
    with open(os.path.join(tmpdir, ".hidden_config"), "w") as f:
        f.write("secret")

    # Normal file — should be indexed
    with open(os.path.join(tmpdir, "visible.py"), "w") as f:
        f.write("x = 1")

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index on the directory with hidden files")
def step_walk_hidden_files(context):
    context.walk_results = walk_and_index(
        Path(context.tmpdir_hidden),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
    )


@then("hidden files should not appear in the results")
def step_verify_no_hidden_files(context):
    paths = [r.path for r in context.walk_results]
    hidden = [p for p in paths if os.path.basename(p).startswith(".")]
    assert len(hidden) == 0, f"Hidden files found: {hidden}"
    assert "visible.py" in paths, f"Expected visible.py in {paths}"


# ---------------------------------------------------------------------------
# walk_and_index: symlinks skipped (line 281)
# ---------------------------------------------------------------------------
@given("a temporary directory with a symlink to a file")
def step_create_dir_with_symlink(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_symlink = tmpdir

    # Real file
    real_file = os.path.join(tmpdir, "real.py")
    with open(real_file, "w") as f:
        f.write("real content")

    # Symlink pointing to the real file
    link_path = os.path.join(tmpdir, "link_to_real.py")
    os.symlink(real_file, link_path)

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index on the directory with symlinks")
def step_walk_symlinks(context):
    context.walk_results = walk_and_index(
        Path(context.tmpdir_symlink),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
    )


@then("symlinked files should not appear in the results")
def step_verify_no_symlinks(context):
    paths = [r.path for r in context.walk_results]
    assert "link_to_real.py" not in paths, (
        f"Symlink should be excluded but found in: {paths}"
    )
    assert "real.py" in paths, f"Expected real.py in {paths}"


# ---------------------------------------------------------------------------
# walk_and_index: ValueError from relative_to (lines 285-286)
# ---------------------------------------------------------------------------
@given("a temporary directory with a file that will cause a relative_to ValueError")
def step_create_dir_for_relative_to_error(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_relto = tmpdir

    with open(os.path.join(tmpdir, "normal.py"), "w") as f:
        f.write("x = 1")

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index with mocked relative_to failure")
def step_walk_with_relative_to_mock(context):
    """Patch Path.relative_to so the first call raises ValueError,
    but subsequent calls (or the resolve() call) work normally."""
    original_relative_to = Path.relative_to
    call_count = {"n": 0}

    def patched_relative_to(self, other):
        call_count["n"] += 1
        # Fail for the first real file encountered to exercise lines 285-286
        if call_count["n"] == 1:
            raise ValueError("mocked relative_to failure")
        return original_relative_to(self, other)

    with patch.object(Path, "relative_to", patched_relative_to):
        context.walk_results = walk_and_index(
            Path(context.tmpdir_relto),
            include_globs=(),
            exclude_globs=(),
            max_file_size=None,
            max_total_size=None,
        )


@then("the file causing ValueError should be skipped gracefully")
def step_verify_relative_to_skip(context):
    # The function should complete without raising; the problematic file
    # is simply skipped.  Result may be empty (the only file was skipped).
    assert isinstance(context.walk_results, list)


# ---------------------------------------------------------------------------
# walk_and_index: path > 1024 chars (lines 292-296)
# ---------------------------------------------------------------------------
@given("a temporary directory with a deeply nested file exceeding 1024 char path")
def step_create_deeply_nested(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_deep = tmpdir

    # Build a path whose relative portion > 1024 chars.
    # Use 60-char directory names nested ~20 levels deep → ~1200 chars.
    nested = tmpdir
    for i in range(20):
        segment = f"d{'x' * 58}{i:01d}"  # 60 chars each
        nested = os.path.join(nested, segment)
    os.makedirs(nested, exist_ok=True)

    deep_file = os.path.join(nested, "deep_file.py")
    with open(deep_file, "w") as f:
        f.write("deep = True")

    # Also add a normal file at root level
    with open(os.path.join(tmpdir, "shallow.py"), "w") as f:
        f.write("shallow = True")

    context.deep_file_rel = str(Path(deep_file).relative_to(Path(tmpdir).resolve()))

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index on the deeply nested directory")
def step_walk_deeply_nested(context):
    context.walk_results = walk_and_index(
        Path(context.tmpdir_deep),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
    )


@then("the deeply nested file should be skipped")
def step_verify_deep_file_skipped(context):
    paths = [r.path for r in context.walk_results]
    # The deep file path exceeds 1024 chars so should be excluded
    long_paths = [p for p in paths if len(p) > 1024]
    assert len(long_paths) == 0, f"Long paths should be excluded: {long_paths}"
    # shallow.py should still be there
    assert "shallow.py" in paths, f"Expected shallow.py in {paths}"


# ---------------------------------------------------------------------------
# walk_and_index: stat() OSError (lines 306-307)
# ---------------------------------------------------------------------------
@given("a temporary directory with a file that will cause a stat OSError")
def step_create_dir_for_stat_error(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_stat = tmpdir

    with open(os.path.join(tmpdir, "goodfile.py"), "w") as f:
        f.write("good = True")

    with open(os.path.join(tmpdir, "badfile.py"), "w") as f:
        f.write("bad = True")

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index with mocked stat failure")
def step_walk_with_stat_mock(context):
    """Patch Path.stat so it raises OSError for badfile.py.

    Only intercept calls with follow_symlinks=True (the default),
    so that is_symlink() → lstat() → stat(follow_symlinks=False)
    still works normally.
    """
    original_stat = Path.stat

    def patched_stat(self, *args, **kwargs):
        follow = kwargs.get("follow_symlinks", True)
        if self.name == "badfile.py" and follow:
            raise OSError("mocked stat failure")
        return original_stat(self, *args, **kwargs)

    with patch.object(Path, "stat", patched_stat):
        context.walk_results = walk_and_index(
            Path(context.tmpdir_stat),
            include_globs=(),
            exclude_globs=(),
            max_file_size=None,
            max_total_size=None,
        )


@then("the file causing stat OSError should be skipped gracefully")
def step_verify_stat_error_skip(context):
    paths = [r.path for r in context.walk_results]
    assert "badfile.py" not in paths, (
        f"badfile.py should be skipped due to OSError, got: {paths}"
    )
    assert "goodfile.py" in paths, f"Expected goodfile.py in {paths}"


# ---------------------------------------------------------------------------
# walk_and_index: non-regular file / FIFO (line 312)
# ---------------------------------------------------------------------------
@given("a temporary directory with a FIFO named pipe")
def step_create_dir_with_fifo(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_fifo = tmpdir

    # Regular file
    with open(os.path.join(tmpdir, "regular.py"), "w") as f:
        f.write("regular = True")

    # FIFO (named pipe) — should be skipped by S_ISREG check
    fifo_path = os.path.join(tmpdir, "myfifo")
    os.mkfifo(fifo_path)

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index on the directory with the FIFO")
def step_walk_fifo(context):
    context.walk_results = walk_and_index(
        Path(context.tmpdir_fifo),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
    )


@then("the FIFO should not appear in the results")
def step_verify_no_fifo(context):
    paths = [r.path for r in context.walk_results]
    assert "myfifo" not in paths, f"FIFO should be excluded, got: {paths}"
    assert "regular.py" in paths, f"Expected regular.py in {paths}"


# ---------------------------------------------------------------------------
# walk_and_index: max_file_count cutoff (lines 338-342)
# ---------------------------------------------------------------------------
@given("a temporary directory with five regular files")
def step_create_dir_with_five_files(context):
    tmpdir = tempfile.mkdtemp()
    context.tmpdir_count = tmpdir

    for i in range(5):
        with open(os.path.join(tmpdir, f"file_{i:02d}.py"), "w") as f:
            f.write(f"x = {i}")

    def cleanup():
        import shutil

        shutil.rmtree(tmpdir, ignore_errors=True)

    context.add_cleanup(cleanup)


@when("I run walk_and_index with max_file_count of {count:d}")
def step_walk_with_max_count(context, count):
    context.walk_results = walk_and_index(
        Path(context.tmpdir_count),
        include_globs=(),
        exclude_globs=(),
        max_file_size=None,
        max_total_size=None,
        max_file_count=count,
    )


@then("only {count:d} file records should be returned")
def step_verify_file_count(context, count):
    actual = len(context.walk_results)
    assert actual == count, (
        f"Expected {count} records, got {actual}: "
        f"{[r.path for r in context.walk_results]}"
    )
