"""Tests for file hint extraction."""

from little_loops.parallel.file_hints import (
    COMMON_FILES_EXCLUDE,
    MIN_DIRECTORY_DEPTH,
    MIN_OVERLAP_FILES,
    OVERLAP_RATIO_THRESHOLD,
    FileHints,
    _directories_overlap,
    _extract_write_target_files,
    _file_in_directory,
    _is_common_file,
    _is_valid_path,
    extract_file_hints,
)


class TestFileHintExtraction:
    """Tests for extract_file_hints function."""

    def test_extracts_python_files(self) -> None:
        """Should extract .py file paths from write-target sections."""
        content = """
### Files to Modify
- `scripts/little_loops/cli.py` — fix the bug
"""
        hints = extract_file_hints(content)
        assert "scripts/little_loops/cli.py" in hints.files

    def test_extracts_typescript_files(self) -> None:
        """Should extract .ts and .tsx file paths from write-target sections."""
        content = """
### Files to Modify
- `src/components/Button.tsx`
- `utils/helpers.ts`
"""
        hints = extract_file_hints(content)
        assert "src/components/Button.tsx" in hints.files
        assert "utils/helpers.ts" in hints.files

    def test_extracts_multiple_files_from_content(self) -> None:
        """Should extract multiple file references from write-target sections."""
        content = """
### Files to Modify
- `scripts/little_loops/cli.py`
- `scripts/little_loops/config.py`
- `tests/test_cli.py`
"""
        hints = extract_file_hints(content)
        assert "scripts/little_loops/cli.py" in hints.files
        assert "scripts/little_loops/config.py" in hints.files
        assert "tests/test_cli.py" in hints.files

    def test_extracts_directories(self) -> None:
        """Should extract directory paths."""
        content = "Changes to scripts/little_loops/ directory"
        hints = extract_file_hints(content)
        assert "scripts/little_loops/" in hints.directories

    def test_extracts_scopes(self) -> None:
        """Should extract scope identifiers."""
        content = "scope: sidebar\nComponent: auth-flow"
        hints = extract_file_hints(content)
        assert "sidebar" in hints.scopes

    def test_extracts_module_scope(self) -> None:
        """Should extract module identifiers."""
        content = "module: parallel-processing"
        hints = extract_file_hints(content)
        assert "parallel-processing" in hints.scopes

    def test_scope_boundaries_header_not_extracted(self) -> None:
        """Section header '## Scope Boundaries' must not extract 'boundaries' as a scope."""
        content = "## Scope Boundaries\n- In scope: add tests\n- Out of scope: impl changes"
        hints = extract_file_hints(content)
        assert "boundaries" not in hints.scopes

    def test_module_prose_not_extracted(self) -> None:
        """Natural prose 'module boundaries' must not extract 'boundaries' as a scope."""
        content = "This change stays within module boundaries and does not affect the API."
        hints = extract_file_hints(content)
        assert "boundaries" not in hints.scopes

    def test_scope_keyword_no_colon_not_extracted(self) -> None:
        """Keywords followed by space but no colon must not match."""
        content = "Narrowed scope to single missing test: test_maintain_route_event_emitted"
        hints = extract_file_hints(content)
        assert "to" not in hints.scopes
        assert "single" not in hints.scopes

    def test_filters_urls(self) -> None:
        """Should filter out URLs."""
        content = "See https://example.com/path/file.py for details"
        hints = extract_file_hints(content)
        assert not any("example.com" in f for f in hints.files)

    def test_filters_short_paths(self) -> None:
        """Should filter out paths that are too short."""
        # Note: The regex still extracts short paths, but _is_valid_path filters them
        # "a.py" is 4 chars which passes the length check
        # We should test with paths shorter than 3 chars
        content = "Change `x` to `y`"  # Single character paths
        hints = extract_file_hints(content)
        # These are too short (< 3 chars)
        assert "x" not in hints.files
        assert "y" not in hints.files

    def test_stores_issue_id(self) -> None:
        """Should store the issue ID."""
        hints = extract_file_hints("content", "ENH-143")
        assert hints.issue_id == "ENH-143"

    def test_extracts_from_code_blocks_in_write_section(self) -> None:
        """Should extract file paths from code blocks inside write-target sections."""
        content = """
### Files to Modify
```bash
# Edit the file
vim scripts/little_loops/cli.py
```
"""
        hints = extract_file_hints(content)
        assert "scripts/little_loops/cli.py" in hints.files

    def test_json_and_yaml_files(self) -> None:
        """Should extract config file types from write-target sections."""
        content = """
### Files to Modify
- `config.json`
- `settings.yaml`
"""
        hints = extract_file_hints(content)
        assert "config.json" in hints.files
        assert "settings.yaml" in hints.files

    def test_files_outside_write_sections_not_extracted(self) -> None:
        """Files mentioned outside write-target sections should not be extracted."""
        content = "Fix the bug in `scripts/little_loops/cli.py`"
        hints = extract_file_hints(content)
        assert "scripts/little_loops/cli.py" not in hints.files


class TestExtractWriteTargetFiles:
    """Tests for _extract_write_target_files — section-scoped extraction."""

    def test_extracts_from_files_to_modify(self) -> None:
        """Should extract file paths from '### Files to Modify' section."""
        content = """
## Integration Map

### Files to Modify
- `scripts/little_loops/parallel/file_hints.py` — add helper
- `scripts/little_loops/cli/sprint/manage.py` — update call

### Related Key Documentation
- `docs/generalized-fsm-loop.md` — reference only
"""
        files = _extract_write_target_files(content)
        assert "scripts/little_loops/parallel/file_hints.py" in files
        assert "scripts/little_loops/cli/sprint/manage.py" in files
        assert "docs/generalized-fsm-loop.md" not in files

    def test_extracts_from_files_changed(self) -> None:
        """Should extract file paths from '### Files Changed' section."""
        content = """
### Files Changed
- `scripts/little_loops/executor.py`

### Dependent Files (Callers/Importers)
- `scripts/little_loops/orchestrator.py`
"""
        files = _extract_write_target_files(content)
        assert "scripts/little_loops/executor.py" in files
        assert "scripts/little_loops/orchestrator.py" not in files

    def test_ignores_reference_docs_section(self) -> None:
        """Files in 'Related Key Documentation' should be excluded."""
        content = """
### Files to Modify
- `scripts/little_loops/parallel/file_hints.py`

### Related Key Documentation
| Document | Relevance |
|---|---|
| `docs/generalized-fsm-loop.md` | Example reference |
| `docs/guides/LOOPS_GUIDE.md` | Context |
"""
        files = _extract_write_target_files(content)
        assert "docs/generalized-fsm-loop.md" not in files
        assert "docs/guides/LOOPS_GUIDE.md" not in files

    def test_returns_empty_when_no_write_sections(self) -> None:
        """Should return empty set when no write-target sections exist."""
        content = """
## Summary
Some issue description mentioning `scripts/cli.py` in passing.

### Related Key Documentation
- `docs/ARCHITECTURE.md`
"""
        files = _extract_write_target_files(content)
        assert files == set()

    def test_both_sections_combined(self) -> None:
        """Should collect files from both 'Files to Modify' and 'Files Changed'."""
        content = """
### Files to Modify
- `scripts/little_loops/executor.py`

### Files Changed
- `scripts/little_loops/validation.py`
"""
        files = _extract_write_target_files(content)
        assert "scripts/little_loops/executor.py" in files
        assert "scripts/little_loops/validation.py" in files


class TestSectionAwareOverlapDetection:
    """Integration tests: extract_file_hints + overlap using section-aware extraction."""

    def test_shared_reference_doc_does_not_cause_overlap(self) -> None:
        """Two issues sharing a file only in reference sections should not overlap."""
        issue_a = """
### Files to Modify
- `scripts/little_loops/parallel/executor.py`

### Related Key Documentation
| Document | Relevance |
|---|---|
| `docs/generalized-fsm-loop.md` | Reference |
"""
        issue_b = """
### Files to Modify
- `scripts/little_loops/parallel/validation.py`

### Related Key Documentation
| Document | Relevance |
|---|---|
| `docs/generalized-fsm-loop.md` | Reference |
"""
        hints_a = extract_file_hints(issue_a, "BUG-001")
        hints_b = extract_file_hints(issue_b, "BUG-002")
        assert not hints_a.overlaps_with(hints_b)

    def test_shared_write_target_still_causes_overlap(self) -> None:
        """Two issues sharing a file in '### Files to Modify' should still overlap."""
        issue_a = """
### Files to Modify
- `scripts/little_loops/parallel/executor.py`
- `scripts/little_loops/parallel/overlap_detector.py`
"""
        issue_b = """
### Files to Modify
- `scripts/little_loops/parallel/executor.py`
- `scripts/little_loops/parallel/file_hints.py`
"""
        hints_a = extract_file_hints(issue_a, "ENH-001")
        hints_b = extract_file_hints(issue_b, "ENH-002")
        assert hints_a.overlaps_with(hints_b)


class TestFileHintsOverlap:
    """Tests for FileHints.overlaps_with method."""

    def test_multiple_file_matches(self) -> None:
        """Should detect overlap when multiple files match (meets MIN_OVERLAP_FILES)."""
        hints1 = FileHints(files={"src/cli.py", "src/config.py"})
        hints2 = FileHints(files={"src/cli.py", "src/config.py", "src/other.py"})
        assert hints1.overlaps_with(hints2)

    def test_single_file_in_small_set_meets_ratio(self) -> None:
        """Single file match meets ratio threshold when set is small enough."""
        # 1 file in a set of 1 = 100% ratio >= 25% threshold
        hints1 = FileHints(files={"src/cli.py"})
        hints2 = FileHints(files={"src/cli.py", "src/other.py"})
        assert hints1.overlaps_with(hints2)

    def test_single_file_below_ratio_threshold(self) -> None:
        """Single file match below ratio threshold should not overlap."""
        # 1 shared file out of 5 = 20% < 25% threshold, and 1 < MIN_OVERLAP_FILES
        hints1 = FileHints(files={"src/a.py", "src/b.py", "src/c.py", "src/d.py", "src/cli.py"})
        hints2 = FileHints(files={"src/e.py", "src/f.py", "src/g.py", "src/h.py", "src/cli.py"})
        assert not hints1.overlaps_with(hints2)

    def test_no_file_overlap(self) -> None:
        """Should return False for non-overlapping files."""
        hints1 = FileHints(files={"src/cli.py"})
        hints2 = FileHints(files={"src/other.py"})
        assert not hints1.overlaps_with(hints2)

    def test_deep_directory_contains_file(self) -> None:
        """Should detect when a deep directory contains a file."""
        hints1 = FileHints(directories={"src/components/"})
        hints2 = FileHints(files={"src/components/Button.tsx"})
        assert hints1.overlaps_with(hints2)

    def test_shallow_directory_no_overlap(self) -> None:
        """Shallow directory (depth < MIN_DIRECTORY_DEPTH) should not trigger overlap."""
        hints1 = FileHints(directories={"src/"})
        hints2 = FileHints(files={"src/cli.py"})
        assert not hints1.overlaps_with(hints2)

    def test_shallow_directory_reverse_no_overlap(self) -> None:
        """Shallow directory should not trigger overlap in reverse direction."""
        hints1 = FileHints(files={"src/cli.py"})
        hints2 = FileHints(directories={"src/"})
        assert not hints1.overlaps_with(hints2)

    def test_deep_nested_directories(self) -> None:
        """Should detect nested directory overlap when depth is sufficient."""
        hints1 = FileHints(directories={"src/components/"})
        hints2 = FileHints(directories={"src/components/forms/"})
        assert hints1.overlaps_with(hints2)

    def test_shallow_nested_directories_no_overlap(self) -> None:
        """Shallow nested directories should not overlap."""
        hints1 = FileHints(directories={"src/"})
        hints2 = FileHints(directories={"src/components/"})
        assert not hints1.overlaps_with(hints2)

    def test_scope_only_no_file_overlap(self) -> None:
        """overlaps_with() should NOT match on scope alone (ENH-746).

        Scope names are too coarse for sprint scheduling — all FSM/loop issues
        share the same scope, which was causing every pair to serialize.
        Use contends_with() when scope matching is required.
        """
        hints1 = FileHints(scopes={"sidebar"})
        hints2 = FileHints(scopes={"sidebar", "auth"})
        assert not hints1.overlaps_with(hints2)

    def test_scope_no_match(self) -> None:
        """Should not match different scopes via overlaps_with()."""
        hints1 = FileHints(scopes={"sidebar"})
        hints2 = FileHints(scopes={"auth"})
        assert not hints1.overlaps_with(hints2)


class TestFileHintsContendsWithScope:
    """Tests for FileHints.contends_with — scope matching for worktree safety."""

    def test_scope_match_detected(self) -> None:
        """contends_with() detects shared scope names."""
        hints1 = FileHints(scopes={"sidebar"})
        hints2 = FileHints(scopes={"sidebar", "auth"})
        assert hints1.contends_with(hints2)

    def test_scope_no_match(self) -> None:
        """contends_with() does not match unrelated scopes."""
        hints1 = FileHints(scopes={"sidebar"})
        hints2 = FileHints(scopes={"auth"})
        assert not hints1.contends_with(hints2)

    def test_file_overlap_still_detected(self) -> None:
        """contends_with() also detects file overlaps (delegates to overlaps_with)."""
        hints1 = FileHints(files={"src/cli.py", "src/config.py"})
        hints2 = FileHints(files={"src/cli.py", "src/config.py"})
        assert hints1.contends_with(hints2)

    def test_empty_hints_no_contention(self) -> None:
        """Empty hints don't contend."""
        hints1 = FileHints()
        hints2 = FileHints(scopes={"fsm"})
        assert not hints1.contends_with(hints2)

    def test_empty_hints_no_overlap(self) -> None:
        """Empty hints should not overlap."""
        hints1 = FileHints()
        hints2 = FileHints(files={"src/cli.py"})
        assert not hints1.overlaps_with(hints2)

    def test_both_empty_no_overlap(self) -> None:
        """Two empty hints should not overlap."""
        hints1 = FileHints()
        hints2 = FileHints()
        assert not hints1.overlaps_with(hints2)

    def test_all_paths_property(self) -> None:
        """all_paths should combine files and directories."""
        hints = FileHints(files={"a.py", "b.py"}, directories={"src/", "tests/"})
        assert hints.all_paths == {"a.py", "b.py", "src/", "tests/"}

    def test_is_empty_property(self) -> None:
        """is_empty should return True only when no hints."""
        assert FileHints().is_empty
        assert not FileHints(files={"a.py"}).is_empty
        assert not FileHints(directories={"src/"}).is_empty
        assert not FileHints(scopes={"auth"}).is_empty

    def test_common_file_excluded(self) -> None:
        """Common infrastructure files should be excluded from overlap checks."""
        hints1 = FileHints(files={"scripts/little_loops/__init__.py"})
        hints2 = FileHints(files={"scripts/little_loops/__init__.py", "src/other.py"})
        assert not hints1.overlaps_with(hints2)

    def test_common_file_pyproject_excluded(self) -> None:
        """pyproject.toml should be excluded from overlap checks."""
        hints1 = FileHints(files={"pyproject.toml"})
        hints2 = FileHints(files={"pyproject.toml", "src/main.py"})
        assert not hints1.overlaps_with(hints2)

    def test_common_files_plus_real_overlap(self) -> None:
        """Common files don't prevent detection of real overlaps."""
        hints1 = FileHints(files={"scripts/__init__.py", "scripts/cli.py", "scripts/config.py"})
        hints2 = FileHints(files={"scripts/__init__.py", "scripts/cli.py", "scripts/config.py"})
        # __init__.py excluded, but cli.py + config.py = 2 files >= MIN_OVERLAP_FILES
        assert hints1.overlaps_with(hints2)

    def test_ratio_threshold_edge(self) -> None:
        """Edge case: exactly at the ratio threshold should overlap."""
        # 1 shared file out of 4 = 25% == OVERLAP_RATIO_THRESHOLD
        hints1 = FileHints(files={"src/a.py", "src/b.py", "src/c.py", "src/shared.py"})
        hints2 = FileHints(files={"src/shared.py"})
        # 1/1 = 100% ratio (smaller set is 1) - meets threshold
        assert hints1.overlaps_with(hints2)

    def test_deep_file_in_directory(self) -> None:
        """File in a deep directory should trigger overlap."""
        hints1 = FileHints(files={"scripts/little_loops/parallel/file_hints.py"})
        hints2 = FileHints(directories={"scripts/little_loops/"})
        assert hints1.overlaps_with(hints2)


class TestGetOverlappingPaths:
    """Tests for FileHints.get_overlapping_paths method."""

    def test_multiple_file_matches(self) -> None:
        """Should return shared file paths when threshold met."""
        hints1 = FileHints(files={"src/cli.py", "src/config.py"})
        hints2 = FileHints(files={"src/cli.py", "src/config.py", "src/other.py"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == {"src/cli.py", "src/config.py"}

    def test_single_file_below_threshold_returns_empty(self) -> None:
        """Single shared file below threshold should return empty set."""
        hints1 = FileHints(files={"src/a.py", "src/b.py", "src/c.py", "src/d.py", "src/cli.py"})
        hints2 = FileHints(files={"src/e.py", "src/f.py", "src/g.py", "src/h.py", "src/cli.py"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()

    def test_deep_directory_overlap(self) -> None:
        """Should return the shorter/parent directory when deep enough."""
        hints1 = FileHints(directories={"src/components/"})
        hints2 = FileHints(directories={"src/components/forms/"})
        result = hints1.get_overlapping_paths(hints2)
        assert "src/components/" in result

    def test_shallow_directory_no_overlap(self) -> None:
        """Should return empty for shallow directory overlap."""
        hints1 = FileHints(directories={"src/"})
        hints2 = FileHints(directories={"src/components/"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()

    def test_file_in_deep_directory(self) -> None:
        """Should return the file path when it's in a deep directory."""
        hints1 = FileHints(files={"src/components/Button.tsx"})
        hints2 = FileHints(directories={"src/components/"})
        result = hints1.get_overlapping_paths(hints2)
        assert "src/components/Button.tsx" in result

    def test_file_in_shallow_directory_no_overlap(self) -> None:
        """Should return empty when file is in a shallow directory."""
        hints1 = FileHints(files={"src/cli.py"})
        hints2 = FileHints(directories={"src/"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()

    def test_no_overlap(self) -> None:
        """Should return empty set when no overlap."""
        hints1 = FileHints(files={"src/cli.py"})
        hints2 = FileHints(files={"tests/test.py"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()

    def test_empty_hints(self) -> None:
        """Should return empty set for empty hints."""
        hints1 = FileHints()
        hints2 = FileHints(files={"src/cli.py"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()

    def test_both_empty(self) -> None:
        """Should return empty set when both empty."""
        result = FileHints().get_overlapping_paths(FileHints())
        assert result == set()

    def test_multiple_overlaps(self) -> None:
        """Should return all overlapping paths when threshold met."""
        hints1 = FileHints(files={"src/a.py", "src/b.py"})
        hints2 = FileHints(files={"src/a.py", "src/b.py", "src/c.py"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == {"src/a.py", "src/b.py"}

    def test_common_files_excluded(self) -> None:
        """Common infrastructure files should be excluded from results."""
        hints1 = FileHints(files={"scripts/__init__.py", "pyproject.toml"})
        hints2 = FileHints(files={"scripts/__init__.py", "pyproject.toml"})
        result = hints1.get_overlapping_paths(hints2)
        assert result == set()


class TestDirectoriesOverlap:
    """Tests for _directories_overlap helper."""

    def test_same_deep_directory(self) -> None:
        """Same deep directory should overlap."""
        assert _directories_overlap("src/components/", "src/components/")

    def test_deep_parent_child(self) -> None:
        """Deep parent and child directories should overlap."""
        assert _directories_overlap("src/components/", "src/components/forms/")
        assert _directories_overlap("src/components/forms/", "src/components/")

    def test_siblings(self) -> None:
        """Sibling directories should not overlap."""
        assert not _directories_overlap("src/", "tests/")

    def test_shallow_directory_no_overlap(self) -> None:
        """Shallow directories (depth < MIN_DIRECTORY_DEPTH) should not overlap."""
        assert not _directories_overlap("src/", "src/")
        assert not _directories_overlap("src/", "src/components/")

    def test_trailing_slash_handling(self) -> None:
        """Should handle inconsistent trailing slashes."""
        assert _directories_overlap("src/components", "src/components/")
        assert _directories_overlap("src/components/", "src/components")

    def test_min_depth_boundary(self) -> None:
        """Directories at exactly MIN_DIRECTORY_DEPTH segments should overlap."""
        # "src/components" has 2 segments → meets MIN_DIRECTORY_DEPTH=2
        assert _directories_overlap("src/components/", "src/components/forms/")
        # "src" has 1 segment → does NOT meet threshold
        assert not _directories_overlap("src/", "src/components/")


class TestFileInDirectory:
    """Tests for _file_in_directory helper."""

    def test_file_in_deep_dir(self) -> None:
        """Should detect file in deep directory."""
        assert _file_in_directory("src/components/Button.tsx", "src/components/")

    def test_file_in_shallow_dir(self) -> None:
        """Shallow directory should not match."""
        assert not _file_in_directory("src/cli.py", "src/")
        assert not _file_in_directory("src/components/Button.tsx", "src/")

    def test_file_not_in_dir(self) -> None:
        """Should return False for file not in directory."""
        assert not _file_in_directory("tests/test.py", "src/")

    def test_handles_trailing_slash(self) -> None:
        """Should handle trailing slash on directory."""
        assert _file_in_directory("src/components/cli.py", "src/components")
        assert _file_in_directory("src/components/cli.py", "src/components/")


class TestIsValidPath:
    """Tests for _is_valid_path filter."""

    def test_valid_paths(self) -> None:
        """Should accept valid file paths."""
        assert _is_valid_path("src/cli.py")
        assert _is_valid_path("tests/test_cli.py")
        assert _is_valid_path("config.json")

    def test_filters_urls(self) -> None:
        """Should reject URLs."""
        assert not _is_valid_path("https://example.com/file.py")
        assert not _is_valid_path("http://example.com/file.py")
        assert not _is_valid_path("//network/share/file.py")

    def test_filters_short_paths(self) -> None:
        """Should reject very short paths."""
        assert not _is_valid_path("a")
        assert not _is_valid_path("ab")

    def test_filters_extension_only(self) -> None:
        """Should reject paths that are just extensions."""
        assert not _is_valid_path(".py")
        assert not _is_valid_path(".ts")

    def test_filters_common_false_positives(self) -> None:
        """Should reject common false positives."""
        assert not _is_valid_path("e.g.")
        assert not _is_valid_path("i.e.")


class TestIsCommonFile:
    """Tests for _is_common_file helper."""

    def test_init_py_excluded(self) -> None:
        """__init__.py should be a common file."""
        assert _is_common_file("__init__.py")
        assert _is_common_file("scripts/little_loops/__init__.py")
        assert _is_common_file("deep/path/to/__init__.py")

    def test_pyproject_excluded(self) -> None:
        """pyproject.toml should be a common file."""
        assert _is_common_file("pyproject.toml")

    def test_setup_excluded(self) -> None:
        """setup.py and setup.cfg should be common files."""
        assert _is_common_file("setup.py")
        assert _is_common_file("setup.cfg")

    def test_regular_file_not_excluded(self) -> None:
        """Regular source files should not be common files."""
        assert not _is_common_file("src/cli.py")
        assert not _is_common_file("scripts/little_loops/config.py")
        assert not _is_common_file("tests/test_cli.py")

    def test_all_common_files(self) -> None:
        """All files in COMMON_FILES_EXCLUDE should be common."""
        for f in COMMON_FILES_EXCLUDE:
            assert _is_common_file(f)


class TestThresholdConstants:
    """Tests to document threshold constant values."""

    def test_min_overlap_files(self) -> None:
        """MIN_OVERLAP_FILES should be 2."""
        assert MIN_OVERLAP_FILES == 2

    def test_overlap_ratio_threshold(self) -> None:
        """OVERLAP_RATIO_THRESHOLD should be 0.25."""
        assert OVERLAP_RATIO_THRESHOLD == 0.25

    def test_min_directory_depth(self) -> None:
        """MIN_DIRECTORY_DEPTH should be 2."""
        assert MIN_DIRECTORY_DEPTH == 2


class TestConfigurableThresholds:
    """Tests for configurable threshold support via DependencyMappingConfig."""

    def _make_config(self, **kwargs):  # type: ignore[no-untyped-def]
        """Helper to create a DependencyMappingConfig with overrides."""
        from little_loops.config import DependencyMappingConfig

        return DependencyMappingConfig.from_dict(kwargs)

    def test_overlaps_with_stricter_min_files(self) -> None:
        """Raising min_files and ratio should prevent overlap when default would trigger."""
        # 2 shared files out of 3 each → count=2 meets default min_files=2
        h1 = FileHints(files={"src/a.py", "src/b.py", "src/x.py"}, issue_id="A")
        h2 = FileHints(files={"src/a.py", "src/b.py", "src/c.py"}, issue_id="B")
        assert h1.overlaps_with(h2) is True

        # Raising min_files=3 and ratio=0.7 prevents overlap:
        # count=2 < 3, ratio=2/3=0.667 < 0.7 → both branches of OR fail
        config = self._make_config(overlap_min_files=3, overlap_min_ratio=0.7)
        assert h1.overlaps_with(h2, config=config) is False

    def test_overlaps_with_stricter_ratio(self) -> None:
        """Raising ratio threshold should prevent overlap from ratio trigger."""
        # 1 shared out of 4 each: count=1 < min_files=2, ratio=1/4=0.25 >= 0.25 → True
        h1 = FileHints(files={"src/a.py", "src/b.py", "src/c.py", "src/d.py"}, issue_id="A")
        h2 = FileHints(files={"src/a.py", "src/e.py", "src/f.py", "src/g.py"}, issue_id="B")
        assert h1.overlaps_with(h2) is True

        # Raising ratio to 0.5: count=1 < 2, ratio=0.25 < 0.5 → both fail → False
        config = self._make_config(overlap_min_ratio=0.5)
        assert h1.overlaps_with(h2, config=config) is False

    def test_overlaps_with_custom_exclude(self) -> None:
        """Custom exclude list should filter different files."""
        h1 = FileHints(files={"src/a.py", "src/b.py"}, issue_id="A")
        h2 = FileHints(files={"src/a.py", "src/b.py"}, issue_id="B")

        # With default, a.py and b.py both match
        assert h1.overlaps_with(h2) is True

        # If we exclude a.py and b.py, nothing matches
        config = self._make_config(exclude_common_files=["a.py", "b.py"])
        assert h1.overlaps_with(h2, config=config) is False

    def test_overlaps_with_lower_directory_depth(self) -> None:
        """Lowering min depth should detect overlap on shallow directories."""
        h1 = FileHints(directories={"scripts/"}, issue_id="A")
        h2 = FileHints(directories={"scripts/"}, issue_id="B")

        # Default min_depth=2 prevents depth-1 directory overlap
        assert h1.overlaps_with(h2) is False

        # Lowering to depth 1 should allow it
        config = self._make_config(min_directory_depth=1)
        assert h1.overlaps_with(h2, config=config) is True

    def test_get_overlapping_paths_with_config(self) -> None:
        """get_overlapping_paths should respect config thresholds."""
        # 2 shared out of 3 each: count=2 >= min_files=2 → overlaps
        h1 = FileHints(files={"src/a.py", "src/b.py", "src/x.py"}, issue_id="A")
        h2 = FileHints(files={"src/a.py", "src/b.py", "src/y.py"}, issue_id="B")

        paths = h1.get_overlapping_paths(h2)
        assert paths == {"src/a.py", "src/b.py"}

        # min_files=3 and ratio=0.7: count=2 < 3, ratio=2/3=0.667 < 0.7 → no overlap
        config = self._make_config(overlap_min_files=3, overlap_min_ratio=0.7)
        paths = h1.get_overlapping_paths(h2, config=config)
        assert "src/a.py" not in paths
        assert "src/b.py" not in paths

    def test_directories_overlap_with_custom_depth(self) -> None:
        """_directories_overlap should respect min_depth parameter."""
        assert _directories_overlap("scripts/", "scripts/", min_depth=1) is True
        assert _directories_overlap("scripts/", "scripts/", min_depth=2) is False

    def test_file_in_directory_with_custom_depth(self) -> None:
        """_file_in_directory should respect min_depth parameter."""
        assert _file_in_directory("scripts/a.py", "scripts/", min_depth=1) is True
        assert _file_in_directory("scripts/a.py", "scripts/", min_depth=2) is False
