"""Tests for detecting general code smells.

Detects:
- Overly complex functions (high cyclomatic complexity)
- Long parameter lists
- God classes
- Feature envy
- Long methods
- Deep nesting
"""

from __future__ import annotations

import ast

from tests.quality._baseline import (
    Violation,
    assert_no_new_violations,
)
from tests.quality._helpers import (
    collect_parse_errors,
    find_source_files,
    parse_file_safe,
    read_file_safe,
    relative_path,
)


def count_branches(node: ast.AST) -> int:
    """Count decision points (branches) in an AST node."""
    count = 0
    for child in ast.walk(node):
        if isinstance(
            child, (ast.If, ast.While, ast.For, ast.AsyncFor, ast.And, ast.Or)
        ):
            count += 1
        elif isinstance(child, ast.comprehension):
            count += 1
            count += len(child.ifs)
        elif isinstance(child, ast.ExceptHandler):
            count += 1
    return count


def count_nesting_depth(node: ast.AST, current_depth: int = 0) -> int:
    """Calculate maximum nesting depth."""
    max_depth = current_depth

    nesting_nodes = (
        ast.If,
        ast.While,
        ast.For,
        ast.AsyncFor,
        ast.With,
        ast.AsyncWith,
        ast.Try,
        ast.FunctionDef,
        ast.AsyncFunctionDef,
    )

    for child in ast.iter_child_nodes(node):
        if isinstance(child, nesting_nodes):
            child_depth = count_nesting_depth(child, current_depth + 1)
        else:
            child_depth = count_nesting_depth(child, current_depth)
        max_depth = max(max_depth, child_depth)

    return max_depth


def count_function_lines(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
    """Count lines in a function body."""
    return 0 if node.end_lineno is None else node.end_lineno - node.lineno + 1


def test_no_high_complexity_functions() -> None:
    """Detect functions with high cyclomatic complexity."""
    max_complexity = 15

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                complexity = count_branches(node) + 1
                if complexity > max_complexity:
                    violations.append(
                        Violation(
                            rule="high_complexity",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"complexity={complexity}",
                        )
                    )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("high_complexity", violations)


def test_no_long_parameter_lists() -> None:
    """Detect functions with too many parameters."""
    max_params = 7

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                args = node.args
                total_params = (
                    len(args.args) + len(args.posonlyargs) + len(args.kwonlyargs)
                )
                if "self" in [a.arg for a in args.args]:
                    total_params -= 1
                if "cls" in [a.arg for a in args.args]:
                    total_params -= 1

                if total_params > max_params:
                    violations.append(
                        Violation(
                            rule="long_parameter_list",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"params={total_params}",
                        )
                    )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("long_parameter_list", violations)


def test_no_god_classes() -> None:
    """Detect classes with too many methods or too much responsibility."""
    max_methods = 20
    max_lines = 500

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                methods = [
                    n
                    for n in node.body
                    if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
                ]

                if len(methods) > max_methods:
                    violations.append(
                        Violation(
                            rule="god_class",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"methods={len(methods)}",
                        )
                    )

                if node.end_lineno:
                    class_lines = node.end_lineno - node.lineno + 1
                    if class_lines > max_lines:
                        violations.append(
                            Violation(
                                rule="god_class",
                                relative_path=rel_path,
                                identifier=node.name,
                                detail=f"lines={class_lines}",
                            )
                        )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("god_class", violations)


def test_no_deep_nesting() -> None:
    """Detect functions with excessive nesting depth."""
    max_nesting = 5

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                depth = count_nesting_depth(node)
                if depth > max_nesting:
                    violations.append(
                        Violation(
                            rule="deep_nesting",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"depth={depth}",
                        )
                    )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("deep_nesting", violations)


def test_no_long_methods() -> None:
    """Detect methods that are too long."""
    max_lines = 75

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                lines = count_function_lines(node)
                if lines > max_lines:
                    violations.append(
                        Violation(
                            rule="long_method",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=f"lines={lines}",
                        )
                    )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("long_method", violations)


def test_no_feature_envy() -> None:
    """Detect methods that use other objects more than self.

    Note: Many apparent "feature envy" cases are FALSE POSITIVES:
    - Converter classes: Naturally transform external objects to domain entities
    - Repository methods: Query→fetch→convert is the standard pattern
    - Exporter classes: Transform domain entities to output format
    - Proto converters: Adapt between protobuf and domain types

    These patterns are excluded from detection.
    """
    # Patterns that are NOT feature envy (they legitimately work with external objects)
    excluded_class_patterns = {
        "converter",
        "exporter",
        "repository",
        "repo",
    }
    excluded_method_patterns = {
        "_to_domain",
        "_to_proto",
        "_proto_to_",
        "_to_orm",
        "_from_orm",
        "export",
        "_parse",  # Parsing external data (API responses)
    }
    # Objects that are commonly used more than self but aren't feature envy
    excluded_object_names = {
        "model",  # ORM model in repo methods
        "meeting",  # Domain entity in exporters
        "segment",  # Domain entity in converters
        "request",  # gRPC request in handlers
        "response",  # gRPC response in handlers
        "np",  # numpy operations
        "noteflow_pb2",  # protobuf module
        "seg",  # Loop iteration over segments
        "job",  # Job processing in background tasks
        "repo",  # Repository access in service methods
        "ai",  # AI response processing
        "summary",  # Summary processing in verification
        "MeetingState",  # Enum class methods
        "event",  # Usage event, calendar event processing
        "item",  # API response item parsing
        "entity",  # Named entity processing
        "uow",  # Unit of work in service methods
        "span",  # OpenTelemetry span in observability
        "host",  # Servicer host in mixin methods
        "logger",  # Logging is cross-cutting, not feature envy
        "data",  # Dict parsing in from_dict factory methods
        "config",  # Configuration object access
        "p",  # Short alias for params in factory methods
        "params",  # Parameters object in factory methods
    }

    def _is_excluded_class(class_name: str) -> bool:
        """Check if class should be excluded from feature envy detection."""
        class_name_lower = class_name.lower()
        return any(p in class_name_lower for p in excluded_class_patterns)

    def _is_excluded_method(method_name: str) -> bool:
        """Check if method should be excluded from feature envy detection."""
        method_name_lower = method_name.lower()
        return any(p in method_name_lower for p in excluded_method_patterns)

    def _count_accesses(
        method: ast.FunctionDef | ast.AsyncFunctionDef,
    ) -> tuple[int, dict[str, int]]:
        """Count self accesses and other object accesses in a method."""
        self_accesses = 0
        other_accesses: dict[str, int] = {}

        for node in ast.walk(method):
            if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
                if node.value.id == "self":
                    self_accesses += 1
                else:
                    other_accesses[node.value.id] = (
                        other_accesses.get(node.value.id, 0) + 1
                    )

        return self_accesses, other_accesses

    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_migrations=False):
        tree, error = parse_file_safe(py_file)
        if error:
            parse_errors.append(error)
            continue
        if tree is None:
            continue

        rel_path = relative_path(py_file)

        for class_node in ast.walk(tree):
            if not isinstance(class_node, ast.ClassDef):
                continue

            if _is_excluded_class(class_node.name):
                continue

            for method in class_node.body:
                if not isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    continue

                if _is_excluded_method(method.name):
                    continue

                self_accesses, other_accesses = _count_accesses(method)

                for other_obj, count in other_accesses.items():
                    if other_obj in excluded_object_names:
                        continue
                    if count > self_accesses + 3 and count > 5:
                        violations.append(
                            Violation(
                                rule="feature_envy",
                                relative_path=rel_path,
                                identifier=f"{class_node.name}.{method.name}",
                                detail=f"{other_obj}={count}_vs_self={self_accesses}",
                            )
                        )

    collect_parse_errors(parse_errors)
    assert_no_new_violations("feature_envy", violations)


def test_module_size_limits() -> None:
    """Check that modules don't exceed size limits."""
    soft_limit = 500
    hard_limit = 750

    soft_violations: list[Violation] = []
    hard_violations: list[Violation] = []

    for py_file in find_source_files(include_migrations=False):
        content, error = read_file_safe(py_file)
        if error or content is None:
            continue

        rel_path = relative_path(py_file)
        line_count = len(content.splitlines())

        if line_count > hard_limit:
            hard_violations.append(
                Violation(
                    rule="module_size_hard",
                    relative_path=rel_path,
                    identifier="module",
                    detail=f"lines={line_count}",
                )
            )
        elif line_count > soft_limit:
            soft_violations.append(
                Violation(
                    rule="module_size_soft",
                    relative_path=rel_path,
                    identifier="module",
                    detail=f"lines={line_count}",
                )
            )

    # Hard limit: zero tolerance
    assert not hard_violations, (
        f"Found {len(hard_violations)} modules exceeding hard limit:\n"
        + "\n".join(str(v) for v in hard_violations)
    )

    # Soft limit: baseline enforcement
    assert_no_new_violations("module_size_soft", soft_violations)
