"""Tests for detecting decentralized helpers and utilities.

Detects:
- Helper functions scattered across modules instead of centralized
- Utility modules that should be consolidated
- Repeated utility patterns in different locations
"""

from __future__ import annotations

import ast
import re
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path


@dataclass
class HelperFunction:
    """Represents a helper/utility function."""

    name: str
    file_path: Path
    line_number: int
    docstring: str | None
    is_private: bool


HELPER_PATTERNS = [
    r"^_?(?:format|parse|convert|transform|normalize|validate|sanitize|clean)",
    r"^_?(?:get|create|make|build)_[\w]+(?:_from|_for|_with)?",
    r"^_?(?:is|has|can|should)_\w+$",
    r"^_?(?:to|from)_[\w]+$",
    r"^_?(?:ensure|check|verify)_\w+$",
]


def is_helper_function(name: str) -> bool:
    """Check if function name matches common helper patterns."""
    return any(re.match(pattern, name) for pattern in HELPER_PATTERNS)


def extract_helper_functions(file_path: Path) -> list[HelperFunction]:
    """Extract helper/utility functions from a Python file."""
    source = file_path.read_text(encoding="utf-8")
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return []

    helpers: list[HelperFunction] = []

    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if is_helper_function(node.name):
                docstring = ast.get_docstring(node)
                helpers.append(
                    HelperFunction(
                        name=node.name,
                        file_path=file_path,
                        line_number=node.lineno,
                        docstring=docstring,
                        is_private=node.name.startswith("_"),
                    )
                )

    return helpers


def find_python_files(root: Path, exclude_protocols: bool = False) -> list[Path]:
    """Find all Python source files excluding generated ones.

    Args:
        root: Root directory to search.
        exclude_protocols: If True, exclude protocol/port files and repository
            implementations (interfaces + their implementations are expected to match).
    """
    excluded = {"*_pb2.py", "*_pb2_grpc.py", "*_pb2.pyi"}
    # Protocol/port files define interfaces - implementations are expected to match
    protocol_patterns: set[str] = {"protocols.py", "ports.py"} if exclude_protocols else set()
    # Repository implementations implement Protocol interfaces - matching signatures expected
    repo_dir_patterns: set[str] = {"repositories", "memory"} if exclude_protocols else set()

    files: list[Path] = []
    for py_file in root.rglob("*.py"):
        if ".venv" in py_file.parts or "__pycache__" in py_file.parts:
            continue
        if any(py_file.match(p) for p in excluded):
            continue
        if exclude_protocols:
            if py_file.name in protocol_patterns:
                continue
            if "ports" in py_file.parts:
                continue
            if any(d in py_file.parts for d in repo_dir_patterns):
                continue
        files.append(py_file)

    return files


def test_helpers_not_scattered() -> None:
    """Detect helper functions scattered across unrelated modules.

    Note: Excludes protocol/port files since interface methods are expected
    to be implemented in multiple locations (hexagonal architecture pattern).
    """
    src_root = Path(__file__).parent.parent.parent / "src" / "noteflow"

    helper_locations: dict[str, list[HelperFunction]] = defaultdict(list)
    for py_file in find_python_files(src_root, exclude_protocols=True):
        for helper in extract_helper_functions(py_file):
            base_name = re.sub(r"^_+", "", helper.name)
            helper_locations[base_name].append(helper)

    scattered: list[str] = []
    for name, helpers in helper_locations.items():
        if len(helpers) > 1:
            modules = {h.file_path.parent.name for h in helpers}
            if len(modules) > 1:
                locations = [f"{h.file_path}:{h.line_number}" for h in helpers]
                scattered.append(
                    f"Helper '{name}' appears in multiple modules:\n"
                    f"  {', '.join(locations)}"
                )

    # Target: 15 scattered helpers max - some duplication is expected for:
    # - Client/server pairs with same method names
    # - Mixin protocols + implementations
    assert len(scattered) <= 15, (
        f"Found {len(scattered)} scattered helper functions (max 15 allowed). "
        "Consider consolidating:\n\n" + "\n\n".join(scattered[:5])
    )


def test_utility_modules_centralized() -> None:
    """Check that utility modules follow expected patterns."""
    src_root = Path(__file__).parent.parent.parent / "src" / "noteflow"

    utility_patterns = ["utils", "helpers", "common", "shared", "_utils", "_helpers"]
    utility_modules: list[Path] = [
        py_file
        for py_file in find_python_files(src_root)
        if any(pattern in py_file.stem for pattern in utility_patterns)
    ]
    utility_by_domain: dict[str, list[Path]] = defaultdict(list)
    for module in utility_modules:
        parts = module.relative_to(src_root).parts
        domain = parts[0] if parts else "root"
        utility_by_domain[domain].append(module)

    violations: list[str] = [
        f"Domain '{domain}' has {len(modules)} utility modules "
        f"(consider consolidating):\n  " + "\n  ".join(str(m) for m in modules)
        for domain, modules in utility_by_domain.items()
        if len(modules) > 2
    ]
    assert not violations, (
        "Found fragmented utility modules:\n\n" + "\n\n".join(violations)
    )


def test_no_duplicate_helper_implementations() -> None:
    """Detect helpers with same name and similar signatures across files.

    Note: Excludes protocol/port files since these define interfaces that
    are intentionally implemented in multiple locations.
    """
    src_root = Path(__file__).parent.parent.parent / "src" / "noteflow"

    helper_signatures: dict[str, list[tuple[Path, int, str]]] = defaultdict(list)

    for py_file in find_python_files(src_root, exclude_protocols=True):
        source = py_file.read_text(encoding="utf-8")
        try:
            tree = ast.parse(source)
        except SyntaxError:
            continue

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if is_helper_function(node.name):
                    args = [arg.arg for arg in node.args.args]
                    signature = f"{node.name}({', '.join(args)})"
                    helper_signatures[signature].append(
                        (py_file, node.lineno, node.name)
                    )

    duplicates: list[str] = []
    for signature, locations in helper_signatures.items():
        if len(locations) > 1:
            loc_strs = [f"{f}:{line}" for f, line, _ in locations]
            duplicates.append(f"'{signature}' defined at: {', '.join(loc_strs)}")

    # Target: 25 duplicate helper signatures - some duplication expected for:
    # - Mixin composition (protocol + implementation)
    # - Client/server pairs
    assert len(duplicates) <= 25, (
        f"Found {len(duplicates)} duplicate helper signatures (max 25 allowed):\n"
        + "\n".join(duplicates[:5])
    )
