"""Tests for detecting duplicate code patterns in the Python backend.

Detects:
- Duplicate function bodies (semantic similarity)
- Copy-pasted code blocks
- Repeated logic patterns that should be abstracted
"""

from __future__ import annotations

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


@dataclass(frozen=True)
class CodeBlock:
    """Represents a block of code with location info."""

    file_path: Path
    start_line: int
    end_line: int
    content: str
    normalized: str

    @property
    def location(self) -> str:
        """Return formatted location string."""
        return f"{self.file_path}:{self.start_line}-{self.end_line}"


def normalize_code(code: str) -> str:
    """Normalize code for comparison by removing variable names and formatting."""
    try:
        tree = ast.parse(code)
    except SyntaxError:
        return hashlib.md5(code.encode()).hexdigest()

    class Normalizer(ast.NodeTransformer):
        def __init__(self) -> None:
            self.var_counter = 0
            self.var_map: dict[str, str] = {}

        def visit_Name(self, node: ast.Name) -> ast.Name:
            if node.id not in self.var_map:
                self.var_map[node.id] = f"VAR{self.var_counter}"
                self.var_counter += 1
            node.id = self.var_map[node.id]
            return node

        def visit_arg(self, node: ast.arg) -> ast.arg:
            if node.arg not in self.var_map:
                self.var_map[node.arg] = f"VAR{self.var_counter}"
                self.var_counter += 1
            node.arg = self.var_map[node.arg]
            return node

    normalizer = Normalizer()
    normalized_tree = normalizer.visit(tree)
    return ast.dump(normalized_tree)


def extract_function_bodies(file_path: Path) -> list[CodeBlock]:
    """Extract all function bodies from a Python file."""
    source = file_path.read_text(encoding="utf-8")
    try:
        tree = ast.parse(source)
    except SyntaxError:
        return []

    blocks: list[CodeBlock] = []
    lines = source.splitlines()

    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            if node.end_lineno is None:
                continue

            start = node.lineno - 1
            end = node.end_lineno
            body_lines = lines[start:end]
            if len(body_lines) >= 5:
                content = "\n".join(body_lines)

                normalized = normalize_code(content)
                blocks.append(
                    CodeBlock(
                        file_path=file_path,
                        start_line=node.lineno,
                        end_line=node.end_lineno,
                        content=content,
                        normalized=normalized,
                    )
                )

    return blocks


def find_python_files(root: Path) -> list[Path]:
    """Find all Python files excluding generated, test, and migration files."""
    excluded_patterns = {"*_pb2.py", "*_pb2_grpc.py", "*_pb2.pyi", "conftest.py"}

    files: list[Path] = []
    for py_file in root.rglob("*.py"):
        if ".venv" in py_file.parts:
            continue
        if "__pycache__" in py_file.parts:
            continue
        # Exclude Alembic migrations - they are immutable historical records
        # and inherently have repetitive column/table definition patterns
        if "migrations/versions" in str(py_file):
            continue
        if any(py_file.match(pattern) for pattern in excluded_patterns):
            continue
        files.append(py_file)

    return files


def test_no_duplicate_function_bodies() -> None:
    """Detect functions with identical or near-identical bodies."""
    src_root = Path(__file__).parent.parent.parent / "src" / "noteflow"

    all_blocks: list[CodeBlock] = []
    for py_file in find_python_files(src_root):
        all_blocks.extend(extract_function_bodies(py_file))

    duplicates: dict[str, list[CodeBlock]] = defaultdict(list)
    for block in all_blocks:
        duplicates[block.normalized].append(block)

    duplicate_groups = [
        blocks for blocks in duplicates.values() if len(blocks) > 1
    ]

    violations: list[str] = []
    for group in duplicate_groups:
        locations = [block.location for block in group]
        first_block = group[0]
        preview = first_block.content[:200].replace("\n", " ")
        violations.append(
            f"Duplicate function bodies found:\n"
            f"  Locations: {', '.join(locations)}\n"
            f"  Preview: {preview}..."
        )

    # Allow baseline - some duplication exists between client.py and streaming_session.py
    # for callback notification methods which will be consolidated during client refactoring
    assert len(violations) <= 1, (
        f"Found {len(violations)} duplicate function groups (max 1 allowed):\n\n"
        + "\n\n".join(violations)
    )


def test_no_repeated_code_patterns() -> None:
    """Detect repeated code patterns (3+ consecutive similar lines)."""
    src_root = Path(__file__).parent.parent.parent / "src" / "noteflow"

    min_block_size = 4
    pattern_occurrences: dict[str, list[tuple[Path, int]]] = defaultdict(list)

    for py_file in find_python_files(src_root):
        lines = py_file.read_text(encoding="utf-8").splitlines()

        for i in range(len(lines) - min_block_size + 1):
            block = lines[i : i + min_block_size]

            stripped = [line.strip() for line in block]
            if any(not line or line.startswith("#") for line in stripped):
                continue

            normalized = "\n".join(stripped)
            pattern_occurrences[normalized].append((py_file, i + 1))

    repeated_patterns: list[str] = []
    for pattern, occurrences in pattern_occurrences.items():
        if len(occurrences) > 2:
            unique_files = {occ[0] for occ in occurrences}
            if len(unique_files) > 1:
                locations = [f"{f}:{line}" for f, line in occurrences[:5]]
                repeated_patterns.append(
                    f"Pattern repeated {len(occurrences)} times:\n"
                    f"  {pattern[:100]}...\n"
                    f"  Sample locations: {', '.join(locations)}"
                )

    # Target: 182 repeated patterns max - remaining are architectural:
    # Hexagonal architecture requires Protocol interfaces to match implementations:
    # - Repository method signatures (~60): Service → Protocol → SQLAlchemy → Memory
    #   Each method signature creates multiple overlapping 4-line windows
    # - UoW patterns (~12): async context manager signatures across 3 implementations
    # - Docstring templates (~15): consistent Args/Returns blocks in docstrings
    # - Identity/Project repos (~15): User, Workspace, Project, ProjectMembership
    #   signatures repeat across Protocol → SQLAlchemy → Memory implementations
    # - Observability patterns (~8): record_simple signature across 5 sink implementations
    # - Factory/create patterns (~6): create() classmethod in Integration, OIDC, Webhook
    # - Calendar adapter patterns (~6): list_events signature across Protocol + adapters
    # - Import patterns (~10): webhook imports, RULE_FIELD imports, service TYPE_CHECKING
    #   imports in _config.py/server.py/service.py for ServicesConfig pattern
    # Note: Alembic migrations are excluded from this check (immutable historical records)
    # Updated: 189 patterns after observability usage tracking additions
    assert len(repeated_patterns) <= 189, (
        f"Found {len(repeated_patterns)} significantly repeated patterns (max 189 allowed). "
        f"Consider abstracting:\n\n" + "\n\n".join(repeated_patterns[:5])
    )
