"""Tests for detecting unnecessary wrappers and aliases.

Detects:
- Thin wrapper functions that add no value
- Alias imports that obscure the original
- Proxy classes with no additional logic
- Redundant type aliases
"""

from __future__ import annotations

import ast
import re

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


def is_thin_wrapper(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
    """Check if function is a thin wrapper returning another call directly."""
    body_stmts = [
        s
        for s in node.body
        if not isinstance(s, ast.Expr) or not isinstance(s.value, ast.Constant)
    ]

    if len(body_stmts) == 1:
        stmt = body_stmts[0]
        if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.Call):
            call = stmt.value
            if isinstance(call.func, ast.Name):
                return call.func.id
            elif isinstance(call.func, ast.Attribute):
                return call.func.attr
    return None


def test_no_trivial_wrapper_functions() -> None:
    """Detect functions that simply wrap another function call.

    Note: Some thin wrappers are valid patterns:
    - Public API facades over private loaders (get_settings -> _load_settings)
    - Property accessors that compute derived values (full_transcript -> join)
    - Factory methods that use cls() pattern
    - Domain methods that provide semantic meaning (is_active -> property check)
    """
    # Valid wrapper patterns that should be allowed
    allowed_wrappers = {
        # Public API facades
        ("get_settings", "_load_settings"),
        ("get_trigger_settings", "_load_trigger_settings"),
        # Cached settings loaders (@lru_cache provides memoization)
        ("get_feature_flags", "model_validate"),
        ("get_calendar_settings", "model_validate"),
        # Factory patterns
        ("from_args", "cls"),
        # Properties that add semantic meaning
        ("segment_count", "len"),
        ("full_transcript", "join"),
        ("duration", "sub"),
        ("is_active", "property"),
        ("is_admin", "can_admin"),  # semantic alias for operation context
        # Domain method accessors (type-safe dict access)
        ("get_metadata", "get"),
        # Strategy pattern implementations (RuleType.evaluate for simple mode)
        ("evaluate", "RuleResult"),
        # Type conversions
        ("database_url_str", "str"),
        ("generate_request_id", "str"),  # UUID to string conversion
        # Context variable accessors (public API over internal contextvars)
        ("get_request_id", "get"),
        ("get_user_id", "get"),
        ("get_workspace_id", "get"),
        # Time conversion utilities (semantic naming for datetime operations)
        ("datetime_to_epoch_seconds", "timestamp"),
        ("datetime_to_iso_string", "isoformat"),
        ("epoch_seconds_to_datetime", "fromtimestamp"),
        ("proto_timestamp_to_datetime", "replace"),
    }

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

    for py_file in find_source_files():
        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)):
                if node.name.startswith("_"):
                    continue

                wrapped = is_thin_wrapper(node)
                if wrapped and node.name != wrapped:
                    # Skip known valid patterns
                    if (node.name, wrapped) in allowed_wrappers:
                        continue
                    violations.append(
                        Violation(
                            rule="thin_wrapper",
                            relative_path=rel_path,
                            identifier=node.name,
                            detail=wrapped,
                        )
                    )

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


def test_no_alias_imports() -> None:
    """Detect imports that alias to confusing names."""
    alias_pattern = re.compile(r"^import\s+(\w+)\s+as\s+(\w+)")
    from_alias_pattern = re.compile(r"from\s+\S+\s+import\s+(\w+)\s+as\s+(\w+)")

    violations: list[Violation] = []

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

        rel_path = relative_path(py_file)
        lines = content.splitlines()

        for i, line in enumerate(lines, start=1):
            for pattern in [alias_pattern, from_alias_pattern]:
                if match := pattern.search(line):
                    original, alias = match.groups()
                    if original.lower() not in alias.lower():
                        # Common well-known aliases that don't need original name
                        if alias not in {"np", "pd", "plt", "tf", "nn", "F", "sa", "sd"}:
                            violations.append(
                                Violation(
                                    rule="alias_import",
                                    relative_path=rel_path,
                                    identifier=content_hash(f"{i}:{line.strip()}"),
                                    detail=f"{original}->{alias}",
                                )
                            )

    assert_no_new_violations("alias_import", violations)


def test_no_redundant_type_aliases() -> None:
    """Detect type aliases that don't add semantic meaning."""
    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files():
        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.AnnAssign) and isinstance(node.target, ast.Name):
                if isinstance(node.annotation, ast.Name):
                    if node.annotation.id == "TypeAlias":
                        if isinstance(node.value, ast.Name):
                            base_type = node.value.id
                            if base_type in {"str", "int", "float", "bool", "bytes"}:
                                target_name = node.target.id
                                violations.append(
                                    Violation(
                                        rule="redundant_type_alias",
                                        relative_path=rel_path,
                                        identifier=target_name,
                                        detail=base_type,
                                    )
                                )

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


def test_no_passthrough_classes() -> None:
    """Detect classes that only delegate to another object."""
    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files():
        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))
                    and not n.name.startswith("_")
                ]

                if len(methods) >= 3:
                    passthrough_count = sum(
                        1 for m in methods if is_thin_wrapper(m) is not None
                    )

                    if passthrough_count == len(methods):
                        violations.append(
                            Violation(
                                rule="passthrough_class",
                                relative_path=rel_path,
                                identifier=node.name,
                                detail=f"{passthrough_count}_methods",
                            )
                        )

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