"""Mutation testing integration.

Measures test quality by injecting faults (mutants) and checking
if tests catch them. High mutation score = high quality tests.

Supports:
- mutmut (Python)
- Stryker (JS/TS)
- Cosmic Ray (Python, alternative)

GitHub:
- https://github.com/boxed/mutmut
- https://github.com/stryker-mutator/stryker-js
"""
from __future__ import annotations

import asyncio
import json
import logging
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


@dataclass
class MutantDetail:
    """Details of a single mutant."""

    id: str
    status: str  # killed, survived, timeout, error
    file_path: str
    line_number: int
    mutation_type: str
    diff: str

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "id": self.id,
            "status": self.status,
            "file_path": self.file_path,
            "line_number": self.line_number,
            "mutation_type": self.mutation_type,
            "diff": self.diff,
        }


@dataclass
class MutationResult:
    """Results from mutation testing."""

    total_mutants: int
    killed: int  # Caught by tests
    survived: int  # Not caught (test quality issue)
    timeout: int
    error: int
    mutation_score: float  # killed / total (higher = better tests)
    survived_mutants: List[MutantDetail] = field(default_factory=list)
    execution_time_seconds: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "total_mutants": self.total_mutants,
            "killed": self.killed,
            "survived": self.survived,
            "timeout": self.timeout,
            "error": self.error,
            "mutation_score": self.mutation_score,
            "survived_mutants": [m.to_dict() for m in self.survived_mutants],
            "execution_time_seconds": self.execution_time_seconds,
        }

    @property
    def quality_rating(self) -> str:
        """Get human-readable quality rating."""
        if self.mutation_score >= 0.9:
            return "Excellent"
        elif self.mutation_score >= 0.8:
            return "Good"
        elif self.mutation_score >= 0.6:
            return "Fair"
        elif self.mutation_score >= 0.4:
            return "Poor"
        else:
            return "Critical"


class MutmutRunner:
    """Run mutation testing with mutmut.

    mutmut is the recommended Python mutation testing tool (2025+).
    Faster than Cosmic Ray, better defaults.

    Usage:
        runner = MutmutRunner()
        result = await runner.run(
            paths_to_mutate=["src/"],
            test_command="pytest tests/",
        )
        print(f"Mutation score: {result.mutation_score:.0%}")
    """

    def __init__(self, project_root: Optional[Path] = None):
        """Initialize mutmut runner.

        Args:
            project_root: Root of the project
        """
        self.project_root = project_root or Path.cwd()

    async def run(
        self,
        paths_to_mutate: List[str],
        test_command: str = "pytest",
        max_mutants: Optional[int] = None,
        timeout_seconds: int = 3600,
    ) -> MutationResult:
        """Run mutation testing.

        Args:
            paths_to_mutate: Source files/directories to mutate
            test_command: Command to run tests
            max_mutants: Maximum mutants to generate (None = all)
            timeout_seconds: Maximum time to run

        Returns:
            MutationResult with scores
        """
        import time

        start_time = time.time()

        # Build mutmut command
        cmd = [
            "mutmut",
            "run",
            "--paths-to-mutate",
            ",".join(paths_to_mutate),
            "--runner",
            test_command,
            "--no-progress",
        ]

        if max_mutants:
            cmd.extend(["--max-number-of-mutants-to-try", str(max_mutants)])

        try:
            # Run mutmut — offload to thread to avoid blocking the event loop
            result = await asyncio.to_thread(
                subprocess.run,
                cmd,
                cwd=self.project_root,
                capture_output=True,
                text=True,
                timeout=timeout_seconds,
            )

            logger.info(f"mutmut completed with exit code {result.returncode}")

        except subprocess.TimeoutExpired:
            logger.error("Mutation testing timed out")
            return MutationResult(
                total_mutants=0,
                killed=0,
                survived=0,
                timeout=1,
                error=0,
                mutation_score=0.0,
                execution_time_seconds=timeout_seconds,
            )
        except FileNotFoundError:
            logger.error("mutmut not found. Run: pip install mutmut")
            return MutationResult(
                total_mutants=0,
                killed=0,
                survived=0,
                timeout=0,
                error=1,
                mutation_score=0.0,
            )

        # Parse results
        mutation_result = await self._parse_results()
        mutation_result.execution_time_seconds = time.time() - start_time

        return mutation_result

    async def _parse_results(self) -> MutationResult:
        """Parse mutmut results."""
        # Get results as JSON
        cmd = ["mutmut", "results", "--json"]

        try:
            result = await asyncio.to_thread(
                subprocess.run,
                cmd,
                cwd=self.project_root,
                capture_output=True,
                text=True,
            )

            if result.returncode != 0:
                # Try parsing text output instead
                return self._parse_text_results()

            data = json.loads(result.stdout)

        except json.JSONDecodeError:
            return self._parse_text_results()
        except Exception as e:
            logger.error(f"Failed to parse mutmut results: {e}")
            return MutationResult(
                total_mutants=0,
                killed=0,
                survived=0,
                timeout=0,
                error=1,
                mutation_score=0.0,
            )

        # Parse counts
        killed_ids = data.get("killed", [])
        survived_ids = data.get("survived", [])
        timeout_ids = data.get("timeout", [])

        killed = len(killed_ids)
        survived = len(survived_ids)
        timeout = len(timeout_ids)

        total = killed + survived + timeout
        mutation_score = killed / total if total > 0 else 0.0

        # Get details of survived mutants
        survived_details = await self._get_mutant_details(survived_ids[:20])

        return MutationResult(
            total_mutants=total,
            killed=killed,
            survived=survived,
            timeout=timeout,
            error=0,
            mutation_score=mutation_score,
            survived_mutants=survived_details,
        )

    def _parse_text_results(self) -> MutationResult:
        """Parse text output when JSON fails."""
        # Try junitxml output
        junit_path = self.project_root / ".mutmut-cache" / "junit.xml"

        if junit_path.exists():
            # Parse junit XML
            try:
                try:
                    import defusedxml.ElementTree as ET
                except ImportError:
                    import xml.etree.ElementTree as ET

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

                testsuite = root.find("testsuite")
                if testsuite is not None:
                    total = int(testsuite.get("tests", 0))
                    failures = int(testsuite.get("failures", 0))
                    errors = int(testsuite.get("errors", 0))
                    killed = total - failures - errors

                    return MutationResult(
                        total_mutants=total,
                        killed=killed,
                        survived=failures,
                        timeout=0,
                        error=errors,
                        mutation_score=killed / total if total > 0 else 0.0,
                    )
            except Exception as e:
                logger.warning(f"Failed to parse junit XML: {e}")

        # Return empty result
        return MutationResult(
            total_mutants=0,
            killed=0,
            survived=0,
            timeout=0,
            error=1,
            mutation_score=0.0,
        )

    async def _get_mutant_details(
        self,
        mutant_ids: List[str],
    ) -> List[MutantDetail]:
        """Get details of specific mutants."""
        details: List[MutantDetail] = []

        for mutant_id in mutant_ids:
            try:
                cmd = ["mutmut", "show", str(mutant_id)]
                result = await asyncio.to_thread(
                    subprocess.run,
                    cmd,
                    cwd=self.project_root,
                    capture_output=True,
                    text=True,
                )

                if result.returncode == 0:
                    # Parse the diff output
                    diff = result.stdout

                    # Extract file path and line from diff
                    file_path = ""
                    line_number = 0

                    for line in diff.split("\n"):
                        if line.startswith("--- "):
                            file_path = line[4:].split("\t")[0]
                        elif line.startswith("@@"):
                            # Parse @@ -1,2 +1,2 @@ format
                            import re

                            match = re.search(r"\+(\d+)", line)
                            if match:
                                line_number = int(match.group(1))

                    details.append(
                        MutantDetail(
                            id=str(mutant_id),
                            status="survived",
                            file_path=file_path,
                            line_number=line_number,
                            mutation_type="unknown",
                            diff=diff[:500],  # Limit diff size
                        )
                    )
            except Exception as e:
                logger.warning(f"Failed to get details for mutant {mutant_id}: {e}")

        return details

    def generate_report(self, result: MutationResult) -> str:
        """Generate human-readable mutation testing report.

        Args:
            result: Mutation testing result

        Returns:
            Formatted report string
        """
        lines = [
            "Mutation Testing Report",
            "=" * 50,
            "",
            f"Total Mutants:    {result.total_mutants}",
            f"Killed (caught):  {result.killed}",
            f"Survived (missed): {result.survived}",
            f"Timeouts:         {result.timeout}",
            f"Errors:           {result.error}",
            "",
            f"Mutation Score:   {result.mutation_score:.1%}",
            f"Test Quality:     {result.quality_rating}",
            "",
        ]

        if result.execution_time_seconds:
            lines.append(f"Execution Time:   {result.execution_time_seconds:.1f}s")
            lines.append("")

        if result.survived_mutants:
            lines.append("Surviving Mutants (tests need improvement):")
            lines.append("-" * 50)

            for mutant in result.survived_mutants[:10]:
                lines.append(
                    f"\n[{mutant.id}] {mutant.file_path}:{mutant.line_number}"
                )
                if mutant.diff:
                    # Show abbreviated diff
                    diff_lines = mutant.diff.split("\n")[:8]
                    for diff_line in diff_lines:
                        lines.append(f"  {diff_line}")
                    if len(mutant.diff.split("\n")) > 8:
                        lines.append("  ...")

        else:
            lines.append("All mutants were killed - excellent test coverage!")

        lines.extend([
            "",
            "Recommendations:",
            "-" * 50,
        ])

        if result.mutation_score < 0.6:
            lines.append("- CRITICAL: Add more assertions to your tests")
            lines.append("- Focus on edge cases and boundary conditions")
        elif result.mutation_score < 0.8:
            lines.append("- Add tests for surviving mutants listed above")
            lines.append("- Consider property-based testing for better coverage")
        else:
            lines.append("- Good test quality! Consider maintaining this level")

        return "\n".join(lines)


async def run_mutation_testing(
    paths: List[str],
    test_command: str = "pytest",
    project_root: Optional[str] = None,
    max_mutants: Optional[int] = 100,
) -> MutationResult:
    """Convenience function to run mutation testing.

    Args:
        paths: Source paths to mutate
        test_command: Test command
        project_root: Project root path
        max_mutants: Maximum mutants to generate

    Returns:
        MutationResult
    """
    root = Path(project_root) if project_root else None
    runner = MutmutRunner(project_root=root)
    return await runner.run(
        paths_to_mutate=paths,
        test_command=test_command,
        max_mutants=max_mutants,
    )


@dataclass
class FixQualityScore:
    """Quality score from mutation-based fix validation."""

    mutation_score: float  # 0-1, what fraction of mutations were caught
    total_mutants: int
    killed: int
    survived: int
    quality_rating: str  # "high", "medium", "low"
    confidence_boost: float  # Suggested boost for learning weight

    def to_dict(self) -> Dict[str, Any]:
        return {
            "mutation_score": self.mutation_score,
            "total_mutants": self.total_mutants,
            "killed": self.killed,
            "survived": self.survived,
            "quality_rating": self.quality_rating,
            "confidence_boost": self.confidence_boost,
        }


async def validate_fix_with_mutations(
    fix_file: str,
    test_file: str,
    test_command: str = "pytest",
    project_root: Optional[str] = None,
    max_mutants: int = 20,
) -> FixQualityScore:
    """Validate a fix by running targeted mutations around it.

    Generates mutations in the fixed code and runs the specific test
    to check if the test catches the mutations. Higher kill rate
    means the fix and test are well-matched.

    Args:
        fix_file: Path to the fixed source file
        test_file: Path to the test file
        test_command: Test command (will target specific test file)
        project_root: Project root path
        max_mutants: Maximum mutants to generate (kept small for speed)

    Returns:
        FixQualityScore with mutation-based quality assessment
    """
    root = Path(project_root) if project_root else None
    runner = MutmutRunner(project_root=root)

    # Run mutations targeted at the fix file, tested by the specific test
    targeted_test_cmd = f"{test_command} {test_file}"

    try:
        result = await runner.run(
            paths_to_mutate=[fix_file],
            test_command=targeted_test_cmd,
            max_mutants=max_mutants,
        )

        # Determine quality rating and confidence boost
        if result.mutation_score >= 0.8:
            quality_rating = "high"
            confidence_boost = 0.2  # Boost learning weight
        elif result.mutation_score >= 0.5:
            quality_rating = "medium"
            confidence_boost = 0.0  # Neutral
        else:
            quality_rating = "low"
            confidence_boost = -0.2  # Reduce confidence

        return FixQualityScore(
            mutation_score=result.mutation_score,
            total_mutants=result.total_mutants,
            killed=result.killed,
            survived=result.survived,
            quality_rating=quality_rating,
            confidence_boost=confidence_boost,
        )

    except Exception as e:
        logger.warning(f"Mutation validation failed: {e}")
        return FixQualityScore(
            mutation_score=0.0,
            total_mutants=0,
            killed=0,
            survived=0,
            quality_rating="unknown",
            confidence_boost=0.0,
        )
