"""
Docker Executor
Runs tests in isolated Docker containers

This is adapted from the server-side TestRunner for client-side execution.
"""
import subprocess
import tempfile
import shutil
import json
import logging
import time
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import Optional, Dict, Any, List, Tuple
from datetime import datetime

from rich.console import Console

console = Console()
logger = logging.getLogger(__name__)


@dataclass
class TestResult:
    """Result of a test execution"""
    status: str  # "passed", "failed", "error", "timeout"
    exit_code: int
    duration_seconds: float
    tests_run: int = 0
    tests_passed: int = 0
    tests_failed: int = 0
    stdout: Optional[str] = None
    stderr: Optional[str] = None
    error_message: Optional[str] = None
    test_details: List[Dict] = None
    
    def __post_init__(self):
        if self.test_details is None:
            self.test_details = []
    
    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


class DockerExecutor:
    """
    Execute tests in isolated Docker containers.
    
    Security features:
    - --network=none: No network access
    - --cpus: CPU limit
    - --memory: Memory limit
    """
    
    # Configuration (matching server-side defaults)
    DEFAULT_TIMEOUT = 120  # seconds
    DOCKER_CPU_LIMIT = 1
    DOCKER_MEMORY_MB = 1024
    PYTHON_IMAGE = "python:3.10-slim"
    JAVA_IMAGE = "maven:3.9-eclipse-temurin-17"
    
    def __init__(self):
        pass  # Don't check Docker on init, check when running
    
    def ensure_docker_available(self, max_wait_seconds: int = 60) -> Tuple[bool, str]:
        """
        Check if Docker is running, wait if starting.
        Matches server-side ensure_docker_available().
        """
        # Quick check first
        if self._is_docker_available():
            return True, "Docker is available"
        
        console.print("   ⏳ Waiting for Docker to start...", style="dim")
        
        # Wait for Docker to become available
        start_time = time.time()
        check_interval = 5
        
        while time.time() - start_time < max_wait_seconds:
            time.sleep(check_interval)
            if self._is_docker_available():
                console.print("   ✓ Docker is ready", style="green dim")
                return True, "Docker is available"
        
        return False, "Docker is not available. Please start Docker Desktop."
    
    def _is_docker_available(self) -> bool:
        """Check if Docker daemon is running"""
        try:
            result = subprocess.run(
                ["docker", "info"],
                capture_output=True,
                timeout=10
            )
            return result.returncode == 0
        except Exception:
            return False
    
    def run_test(
        self, 
        workspace: Path, 
        test_file: str, 
        language: str = "python",
        timeout: int = None
    ) -> TestResult:
        """
        Run a test file in Docker.
        
        Args:
            workspace: Path to the workspace containing test files
            test_file: Name of the test file to run
            language: Programming language ("python" or "java")
            timeout: Execution timeout in seconds
            
        Returns:
            TestResult with execution details
        """
        # Check Docker availability
        available, message = self.ensure_docker_available()
        if not available:
            return TestResult(
                status="error",
                exit_code=127,
                duration_seconds=0,
                error_message=message
            )
        
        timeout = timeout or self.DEFAULT_TIMEOUT
        
        console.print(f"   🐳 [dim]Running in Docker container[/dim]")
        
        if language == "python":
            return self._run_python_test(workspace, test_file, timeout)
        elif language == "java":
            return self._run_java_test(workspace, test_file, timeout)
        else:
            return TestResult(
                status="error",
                exit_code=-1,
                duration_seconds=0,
                error_message=f"Unsupported language: {language}"
            )
    
    def _run_python_test(self, workspace: Path, test_file: str, timeout: int) -> TestResult:
        """Run Python test with pytest - matches server-side TestRunner"""
        start_time = datetime.now()
        
        # Docker command - MATCHES server-side TestRunner._run_in_docker()
        # Note: Using 'bridge' network to allow pip install (matching server behavior)
        # For stricter security, use a pre-built image with pytest pre-installed
        cmd = [
            "docker", "run", "--rm",
            "--network", "bridge",                            # Allow network for pip install
            "--cpus", str(self.DOCKER_CPU_LIMIT),            # CPU limit
            "--memory", f"{self.DOCKER_MEMORY_MB}m",         # Memory limit
            "-v", f"{workspace}:/app",                        # Mount workspace
            "-w", "/app",
            self.PYTHON_IMAGE,
            "/bin/bash", "-c",
            f"""
            pip install pytest pytest-timeout pytest-json-report pytest-mock > /dev/null 2>&1
            export PYTHONPATH=$PYTHONPATH:.
            if [ -f requirements.txt ]; then pip install -r requirements.txt > /dev/null 2>&1; fi
            pytest {test_file} --timeout={timeout} --json-report --json-report-file=test_report.json -v
            cat test_report.json 2>/dev/null || echo '{{"error": "no report"}}'
            """
        ]
        
        # Print command for transparency
        console.print(f"   [dim]docker run --rm {self.PYTHON_IMAGE} pytest {test_file}[/dim]")
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout + 60  # Extra time for Docker startup/install
            )
            
            duration = (datetime.now() - start_time).total_seconds()
            
            # Try to read report from mounted workspace
            report_path = workspace / "test_report.json"
            
            return self._parse_pytest_result(result, duration, report_path)
            
        except subprocess.TimeoutExpired:
            return TestResult(
                status="timeout",
                exit_code=-1,
                duration_seconds=timeout,
                error_message=f"Test execution timed out after {timeout}s"
            )
        except Exception as e:
            return TestResult(
                status="error",
                exit_code=-1,
                duration_seconds=0,
                error_message=str(e)
            )
    
    def _parse_pytest_result(
        self, 
        result: subprocess.CompletedProcess, 
        duration: float,
        report_path: Optional[Path] = None
    ) -> TestResult:
        """Parse pytest JSON output - matches server-side TestRunner._parse_result()"""
        status = "passed" if result.returncode == 0 else "failed"
        if result.returncode != 0 and result.stderr and "timeout" in result.stderr.lower():
            status = "timeout"
        
        tests_run = 0
        tests_passed = 0
        tests_failed = 0
        test_details = []
        error_message = None
        
        # Try to read JSON report from file first (most reliable)
        report_data = None
        if report_path and report_path.exists():
            try:
                report_data = json.loads(report_path.read_text())
            except Exception as e:
                logger.debug(f"Failed to read report file: {e}")
        
        # Fallback: parse from stdout (cat output)
        if not report_data:
            try:
                stdout_lines = result.stdout.strip().split('\n')
                json_line = stdout_lines[-1] if stdout_lines else ""
                
                if json_line.startswith('{'):
                    report_data = json.loads(json_line)
            except Exception as e:
                logger.debug(f"Failed to parse JSON report: {e}")
        
        # Extract data from report
        if report_data and "summary" in report_data:
            summary = report_data.get("summary", {})
            tests_passed = summary.get("passed", 0)
            tests_failed = summary.get("failed", 0) + summary.get("error", 0)
            tests_run = tests_passed + tests_failed + summary.get("skipped", 0)
            
            # Check for collection errors (import failures, syntax errors)
            for collector in report_data.get("collectors", []):
                if collector.get("outcome") == "failed":
                    status = "error"
                    tests_failed += 1
                    error_info = collector.get("longrepr", "")
                    
                    if "ModuleNotFoundError" in error_info:
                        error_message = f"Import error: {error_info.split('ModuleNotFoundError:')[-1].split(chr(10))[0].strip()}"
                    elif "ImportError" in error_info:
                        error_message = f"Import error: {error_info.split('ImportError:')[-1].split(chr(10))[0].strip()}"
                    elif "SyntaxError" in error_info:
                        error_message = "Syntax error in test file"
                    else:
                        error_message = f"Collection failed: {collector.get('nodeid', 'unknown')}"
                    
                    test_details.append({
                        "name": collector.get("nodeid", "collection_error"),
                        "status": "error",
                        "duration": 0
                    })
            
            # Get test details
            for test in report_data.get("tests", []):
                test_outcome = test.get("outcome", "unknown")
                test_details.append({
                    "name": test.get("nodeid", "unknown"),
                    "status": test_outcome,
                    "duration": test.get("call", {}).get("duration", 0)
                })
                
                # Extract error message from failed tests
                if test_outcome in ["failed", "error"] and not error_message:
                    error_message = test.get("call", {}).get("crash", {}).get("message")
        
        # Fallback: check stderr for common errors
        if not error_message and result.returncode != 0:
            stderr = result.stderr or ""
            if "ModuleNotFoundError" in stderr:
                error_message = f"Import error: {stderr.split('ModuleNotFoundError:')[-1].split(chr(10))[0].strip()}"
            elif "ImportError" in stderr:
                error_message = f"Import error: {stderr.split('ImportError:')[-1].split(chr(10))[0].strip()}"
            elif stderr:
                error_message = stderr[-500:] if len(stderr) > 500 else stderr
            
            # If no tests ran but we have an error, mark as error status
            if tests_run == 0:
                status = "error"
                tests_failed = 1
        
        return TestResult(
            status=status,
            exit_code=result.returncode,
            duration_seconds=duration,
            tests_run=tests_run,
            tests_passed=tests_passed,
            tests_failed=tests_failed,
            stdout=result.stdout[-2000:] if result.stdout else None,
            stderr=result.stderr[-500:] if result.stderr else None,
            error_message=error_message,
            test_details=test_details
        )
    
    def _run_java_test(self, workspace: Path, test_file: str, timeout: int) -> TestResult:
        """Run Java test with Maven - matches server-side TestRunner"""
        start_time = datetime.now()
        
        # Find project root (where pom.xml is)
        project_root = workspace
        current = workspace
        for _ in range(4):
            if (current / "pom.xml").exists() or (current / "build.gradle").exists():
                project_root = current
                break
            parent = current.parent
            if parent == current:
                break
            current = parent
        
        # Check for pom.xml
        pom_path = project_root / "pom.xml"
        if not pom_path.exists():
            # Check for build.gradle
            if (project_root / "build.gradle").exists():
                return self._run_gradle_test(project_root, test_file, timeout)
            return TestResult(
                status="error",
                exit_code=-1,
                duration_seconds=0,
                error_message="No pom.xml or build.gradle found for Java project"
            )
        
        test_name = Path(test_file).stem
        
        cmd = [
            "docker", "run", "--rm",
            "--cpus", str(self.DOCKER_CPU_LIMIT),
            "--memory", "2048m",  # Java needs more memory
            "-v", f"{project_root}:/app",
            "-w", "/app",
            self.JAVA_IMAGE,
            "mvn", "test",
            f"-Dtest={test_name}",
            "-DfailIfNoTests=false",
            "--batch-mode", "-q"
        ]
        
        console.print(f"   [dim]docker run --rm {self.JAVA_IMAGE} mvn test -Dtest={test_name}[/dim]")
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout + 120  # Extra time for Maven dependency download
            )
            
            duration = (datetime.now() - start_time).total_seconds()
            
            status = "passed" if result.returncode == 0 else "failed"
            
            # Try to parse test counts from Maven output
            tests_run, tests_passed, tests_failed = self._parse_maven_output(result.stdout)
            
            return TestResult(
                status=status,
                exit_code=result.returncode,
                duration_seconds=duration,
                tests_run=tests_run,
                tests_passed=tests_passed,
                tests_failed=tests_failed,
                stdout=result.stdout[-2000:] if result.stdout else None,
                stderr=result.stderr[-500:] if result.stderr else None
            )
            
        except subprocess.TimeoutExpired:
            return TestResult(
                status="timeout",
                exit_code=-1,
                duration_seconds=timeout,
                error_message="Test execution timed out"
            )
        except Exception as e:
            return TestResult(
                status="error",
                exit_code=-1,
                duration_seconds=0,
                error_message=str(e)
            )
    
    def _run_gradle_test(self, workspace: Path, test_file: str, timeout: int) -> TestResult:
        """Run Java test with Gradle"""
        start_time = datetime.now()
        
        test_name = Path(test_file).stem
        
        cmd = [
            "docker", "run", "--rm",
            "--cpus", str(self.DOCKER_CPU_LIMIT),
            "--memory", "2048m",
            "-v", f"{workspace}:/app",
            "-w", "/app",
            "gradle:8-jdk17",
            "gradle", "test",
            f"--tests", test_name,
            "--no-daemon", "-q"
        ]
        
        console.print(f"   [dim]docker run --rm gradle:8-jdk17 gradle test --tests {test_name}[/dim]")
        
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout + 120
            )
            
            duration = (datetime.now() - start_time).total_seconds()
            status = "passed" if result.returncode == 0 else "failed"
            
            return TestResult(
                status=status,
                exit_code=result.returncode,
                duration_seconds=duration,
                stdout=result.stdout[-2000:] if result.stdout else None,
                stderr=result.stderr[-500:] if result.stderr else None
            )
            
        except subprocess.TimeoutExpired:
            return TestResult(
                status="timeout",
                exit_code=-1,
                duration_seconds=timeout,
                error_message="Test execution timed out"
            )
        except Exception as e:
            return TestResult(
                status="error",
                exit_code=-1,
                duration_seconds=0,
                error_message=str(e)
            )
    
    def _parse_maven_output(self, stdout: str) -> Tuple[int, int, int]:
        """Parse Maven test output to get test counts"""
        tests_run = 0
        tests_passed = 0
        tests_failed = 0
        
        if not stdout:
            return tests_run, tests_passed, tests_failed
        
        # Look for "Tests run: X, Failures: Y, Errors: Z"
        import re
        pattern = r"Tests run: (\d+), Failures: (\d+), Errors: (\d+)"
        matches = re.findall(pattern, stdout)
        
        for match in matches:
            run, failures, errors = int(match[0]), int(match[1]), int(match[2])
            tests_run += run
            tests_failed += failures + errors
            tests_passed += run - failures - errors
        
        return tests_run, tests_passed, tests_failed
