"""Test file detection and coverage analysis.

Detects test files by naming convention and optionally parses
coverage reports for per-file line coverage data.
"""

from __future__ import annotations

import logging
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import TYPE_CHECKING, Literal

from unfamiliar.analysis.models import FileTestInfo

if TYPE_CHECKING:
    from unfamiliar.analysis.models import FileInfo, ParsedFile

logger = logging.getLogger(__name__)

# Test file naming patterns
_TEST_FILE_PATTERNS = [
    "test_",
    "_test.py",
    "_spec.py",
    ".spec.ts",
    ".test.ts",
    ".spec.js",
    ".test.js",
]

# Test directory patterns
_TEST_DIR_PATTERNS = [
    "tests/",
    "test/",
    "__tests__/",
    "spec/",
    "__spec__/",
]


def detect_tests(
    files: list[FileInfo],
    parsed_files: dict[str, ParsedFile],
    repo_path: Path,
) -> FileTestInfo:
    """Detect test files and optionally parse coverage reports.

    Tier 1 uses naming conventions. Tier 2 adds coverage report parsing
    if coverage.xml or .coverage files are found.

    Args:
        files: All discovered files with metadata.
        parsed_files: Parse results keyed by file path.
        repo_path: Path to the repository root.

    Returns:
        FileTestInfo with test files, source-to-test mapping, and coverage data.
    """
    test_files: set[str] = set()
    source_to_tests: dict[str, list[str]] = {}
    coverage_summary: dict[str, float] = {}

    # Tier 1: Identify test files by naming convention
    for file_info in files:
        if _is_test_file(file_info.path):
            test_files.add(file_info.path)

    # Build source-to-test mapping
    # For each test file, check its imports to find source files it tests
    for test_path in test_files:
        parsed = parsed_files.get(test_path)
        if parsed is None:
            continue

        for imp in parsed.imports:
            # Resolve the import target to a source file
            target = imp.target
            if target.startswith("."):
                # Relative import - convert to potential source path
                source = _resolve_test_import(test_path, target, files)
            else:
                # Absolute import
                source = _find_source_for_import(target, files)

            if source and source not in test_files:
                if source not in source_to_tests:
                    source_to_tests[source] = []
                if test_path not in source_to_tests[source]:
                    source_to_tests[source].append(test_path)

    # Find untested files (non-test, non-config files not covered by any test)
    all_source_files = {
        f.path
        for f in files
        if f.path not in test_files and not _is_config_file(f.path)
    }
    untested_files = all_source_files - set(source_to_tests.keys())

    # Tier 2: Check for coverage reports
    tier: Literal["basic", "enhanced"] = "basic"
    coverage_xml = repo_path / "coverage.xml"
    coverage_db = repo_path / ".coverage"

    if coverage_xml.exists():
        try:
            coverage_summary = _parse_coverage_xml(coverage_xml)
            tier = "enhanced"
            logger.debug("Parsed coverage.xml with %d files", len(coverage_summary))
        except ET.ParseError as e:
            logger.warning("Failed to parse coverage.xml: %s", e)
    elif coverage_db.exists():
        # .coverage is a SQLite database - for MVP, just note its presence
        # Full parsing would require coverage library
        logger.debug("Found .coverage file but parsing not implemented in MVP")
        tier = "basic"

    return FileTestInfo(
        test_files=test_files,
        source_to_tests=source_to_tests,
        untested_files=untested_files,
        tier=tier,
        coverage_summary=coverage_summary,
    )


def _is_test_file(path: str) -> bool:
    """Check if a file path matches test file naming patterns."""
    path_lower = path.lower()

    # Check file name patterns
    if any(pattern in path_lower for pattern in _TEST_FILE_PATTERNS):
        return True

    # Check directory patterns
    return any(path_lower.startswith(pattern) for pattern in _TEST_DIR_PATTERNS)


def _is_config_file(path: str) -> bool:
    """Check if a file is a config/setup file (not a source file to test)."""
    config_patterns = [
        "setup.py",
        "setup.cfg",
        "pyproject.toml",
        "conftest.py",
        "__init__.py",
        ".env",
        "config.",
    ]
    path_lower = path.lower()
    return any(pattern in path_lower for pattern in config_patterns)


def _resolve_test_import(
    test_path: str,
    import_target: str,
    files: list[FileInfo],
) -> str | None:
    """Resolve a relative import from a test file to a source file."""
    # Count leading dots
    level = 0
    for char in import_target:
        if char == ".":
            level += 1
        else:
            break

    module_part = import_target[level:]

    # Get the directory of the test file
    test_dir = str(Path(test_path).parent)
    parts = test_dir.split("/")

    # Navigate up 'level' directories
    if level > len(parts):
        return None

    base_parts = parts[:-level] if level <= len(parts) else []
    base_path = "/".join(base_parts) if base_parts else ""

    # Build candidate source path
    if module_part:
        candidate = f"{base_path}/{module_part.replace('.', '/')}.py"
        candidate = candidate.lstrip("/")
    else:
        return None

    # Check if candidate exists
    file_paths = {f.path for f in files}
    if candidate in file_paths:
        return candidate

    return None


def _find_source_for_import(import_target: str, files: list[FileInfo]) -> str | None:
    """Find a source file matching an absolute import target."""
    module_path = import_target.replace(".", "/")
    file_paths = {f.path for f in files}

    candidates = [
        f"{module_path}.py",
        f"{module_path}/__init__.py",
    ]

    for candidate in candidates:
        if candidate in file_paths:
            return candidate

    return None


def _parse_coverage_xml(coverage_path: Path) -> dict[str, float]:
    """Parse Cobertura XML coverage report.

    Args:
        coverage_path: Path to coverage.xml file.

    Returns:
        Dict mapping relative file path to line coverage (0.0 to 1.0).
    """
    result: dict[str, float] = {}

    tree = ET.parse(coverage_path)
    root = tree.getroot()

    # Cobertura format: <coverage><packages><package><classes><class>
    # Each class has filename, line-rate attributes
    for class_elem in root.iter("class"):
        filename = class_elem.get("filename")
        line_rate = class_elem.get("line-rate")

        if filename and line_rate:
            try:
                coverage = float(line_rate)
                # Normalize filename (remove leading ./ if present)
                if filename.startswith("./"):
                    filename = filename[2:]
                result[filename] = coverage
            except ValueError:
                continue

    return result
