"""Tests for detecting stale code and artifacts.

Detects:
- Unused imports
- Unreferenced functions/classes
- Dead code patterns (unreachable code)
- Orphaned test files
- Stale TODO/FIXME comments
"""

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 test_no_stale_todos() -> None:
    """Detect old TODO/FIXME comments that should be addressed."""
    stale_pattern = re.compile(
        r"#\s*(TODO|FIXME|HACK|XXX|DEPRECATED)[\s:]*(.{0,100})",
        re.IGNORECASE,
    )

    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):
            if match := stale_pattern.search(line):
                tag = match[1].upper()
                message = match[2].strip()[:50]
                violations.append(
                    Violation(
                        rule="stale_todo",
                        relative_path=rel_path,
                        identifier=content_hash(f"{i}:{line.strip()}"),
                        detail=f"{tag}:{message}",
                    )
                )

    assert_no_new_violations("stale_todo", violations)


def test_no_commented_out_code() -> None:
    """Detect large blocks of commented-out code."""
    code_pattern = re.compile(
        r"^#\s*(?:def |class |import |from |if |for |while |return |raise )"
    )

    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()
        consecutive_code_comments = 0
        block_start = 0

        for i, line in enumerate(lines, start=1):
            if code_pattern.match(line.strip()):
                if consecutive_code_comments == 0:
                    block_start = i
                consecutive_code_comments += 1
            else:
                if consecutive_code_comments >= 3:
                    violations.append(
                        Violation(
                            rule="commented_out_code",
                            relative_path=rel_path,
                            identifier=f"lines_{block_start}_{i - 1}",
                            detail=f"{consecutive_code_comments}_lines",
                        )
                    )
                consecutive_code_comments = 0

        # Check end of file
        if consecutive_code_comments >= 3:
            violations.append(
                Violation(
                    rule="commented_out_code",
                    relative_path=rel_path,
                    identifier=f"lines_{block_start}_{len(lines)}",
                    detail=f"{consecutive_code_comments}_lines",
                )
            )

    # Commented-out code should be removed entirely - zero tolerance
    assert not violations, (
        f"Found {len(violations)} blocks of commented-out code "
        "(remove or implement):\n"
        + "\n".join(str(v) for v in violations)
    )


def test_no_orphaned_imports() -> None:
    """Detect modules that import but never use certain names.

    Note: Skips __init__.py files since re-exports are intentional public API.
    """
    violations: list[Violation] = []
    parse_errors: list[str] = []

    for py_file in find_source_files(include_init=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)

        imported_names: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    name = alias.asname or alias.name.split(".")[0]
                    imported_names.add(name)
            elif isinstance(node, ast.ImportFrom):
                for alias in node.names:
                    if alias.name != "*":
                        name = alias.asname or alias.name
                        imported_names.add(name)

        used_names: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Name):
                used_names.add(node.id)
            elif isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name):
                used_names.add(node.value.id)

        # Check for __all__ re-exports (names listed in __all__ are considered used)
        all_exports: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == "__all__":
                        if isinstance(node.value, ast.List):
                            for elt in node.value.elts:
                                if isinstance(elt, ast.Constant) and isinstance(
                                    elt.value, str
                                ):
                                    all_exports.add(elt.value)

        # Check for TYPE_CHECKING imports
        type_checking_imports: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.If):
                if isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING":
                    for subnode in ast.walk(node):
                        if isinstance(subnode, ast.ImportFrom):
                            for alias in subnode.names:
                                name = alias.asname or alias.name
                                type_checking_imports.add(name)

        unused = imported_names - used_names - type_checking_imports - all_exports
        unused -= {"__future__", "annotations"}

        violations.extend(
            Violation(
                rule="orphaned_import",
                relative_path=rel_path,
                identifier=name,
            )
            for name in sorted(unused)
            if not name.startswith("_")
        )
    collect_parse_errors(parse_errors)
    assert_no_new_violations("orphaned_import", violations)


def test_no_unreachable_code() -> None:
    """Detect code after return/raise/break/continue statements."""
    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)):
                for i, stmt in enumerate(node.body[:-1]):
                    if isinstance(stmt, (ast.Return, ast.Raise)):
                        next_stmt = node.body[i + 1]
                        if not isinstance(next_stmt, ast.Pass):
                            violations.append(
                                Violation(
                                    rule="unreachable_code",
                                    relative_path=rel_path,
                                    identifier=f"{node.name}:{next_stmt.lineno}",
                                    detail=type(stmt).__name__.lower(),
                                )
                            )

    collect_parse_errors(parse_errors)

    # Unreachable code should be removed entirely - zero tolerance
    assert not violations, (
        f"Found {len(violations)} instances of unreachable code:\n"
        + "\n".join(str(v) for v in violations)
    )


def test_no_deprecated_patterns() -> None:
    """Detect usage of deprecated patterns and APIs."""
    deprecated_patterns = [
        (r"\.format\s*\(", "str.format()"),
        (r"% \(", "%-formatting"),
        (r"from typing import Optional", "Optional[X]"),
        (r"from typing import Union", "Union[X, Y]"),
        (r"from typing import List\b", "List[X]"),
        (r"from typing import Dict\b", "Dict[K, V]"),
        (r"from typing import Tuple\b", "Tuple[X, ...]"),
        (r"from typing import Set\b", "Set[X]"),
    ]

    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):
            violations.extend(
                Violation(
                    rule="deprecated_pattern",
                    relative_path=rel_path,
                    identifier=content_hash(f"{i}:{line.strip()}"),
                    detail=old_style,
                )
                for pattern, old_style in deprecated_patterns
                if re.search(pattern, line)
            )
    assert_no_new_violations("deprecated_pattern", violations)
