"""
Tests Router - Manages generated tests for the Arcturus IDE.
Handles test manifest, test generation triggers, and test execution.
"""
import os
import json
import sys
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing import List, Optional, Dict, Any

# Add project root to path to allow importing tools and agents
sys.path.append(str(Path(__file__).parent.parent))
from tools.ast_differ import analyze_file, FileAnalysis
from agents.base_agent import AgentRunner

router = APIRouter(prefix="/tests", tags=["tests"])

# ... (Models and Helpers remain same)

# ... (Endpoints)

# =============================================================================
# Models
# =============================================================================

class TestItem(BaseModel):
    id: str
    name: str
    status: str  # 'passing' | 'failing' | 'stale' | 'orphaned' | 'pending'
    type: str    # 'behavior' | 'spec'
    lastRun: Optional[str] = None
    code: Optional[str] = None
    target_line: Optional[int] = None


class FileTests(BaseModel):
    file: str
    tests: List[TestItem]


class GenerateTestsRequest(BaseModel):
    path: str
    file_path: str
    function_names: Optional[List[str]] = None
    test_type: str = "behavior"  # 'behavior' or 'spec'
    force: bool = False # If true, ignore semantic hashing check
    deleted_functions: Optional[List[str]] = None



class RunTestsRequest(BaseModel):
    path: str
    test_ids: List[str]


class FixTestsRequest(BaseModel):
    path: str
    failures: List[Dict[str, Any]]



class SyncTestsRequest(BaseModel):
    path: str


# =============================================================================
# Helpers
# =============================================================================

def load_settings(path: str) -> dict:
    """Load settings.json to get feedback_mode"""
    settings_path = os.path.join(path, "config", "settings.json")
    if not os.path.exists(settings_path):
        return {"testing": {"feedback_mode": "with_permission"}}
    try:
        with open(settings_path, "r") as f:
            return json.load(f)
    except:
        return {"testing": {"feedback_mode": "with_permission"}}

def get_source_file_from_test(test_file: str, project_path: str) -> Optional[str]:
    """Find source file for a test file."""
    name = Path(test_file).name
    if name.startswith("test_"):
        source_name = name[5:] 
    elif name.endswith("_test.py"):
        source_name = name[:-8] + ".py"
    elif name.endswith("_spec.py"):
        source_name = name[:-8] + ".py"
    else:
        return None
        
    for root, dirs, files in os.walk(project_path):
        if source_name in files:
            if ".arcturus/tests" in root:
                continue
            return os.path.join(root, source_name)
    return None

def get_arcturus_tests_dir(project_root: str) -> Path:
    """Get the .arcturus/tests directory for a project."""
    tests_dir = Path(project_root) / ".arcturus" / "tests"
    tests_dir.mkdir(parents=True, exist_ok=True)
    return tests_dir


def get_manifest_path(project_root: str) -> Path:
    """Get the path to the test manifest file."""
    return get_arcturus_tests_dir(project_root) / "manifest.json"


def load_manifest(project_root: str) -> Dict:
    """Load the test manifest, creating empty one if it doesn't exist."""
    manifest_path = get_manifest_path(project_root)
    if manifest_path.exists():
        try:
            return json.loads(manifest_path.read_text())
        except json.JSONDecodeError:
            return {}
    return {}


def save_manifest(project_root: str, manifest: Dict):
    """Save the test manifest."""
    manifest_path = get_manifest_path(project_root)
    manifest_path.write_text(json.dumps(manifest, indent=2))


# =============================================================================
# Endpoints
# =============================================================================

@router.get("/manifest")
async def get_manifest(path: str):
    """Get the full test manifest for a project."""
    if not os.path.exists(path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    manifest = load_manifest(path)
    return {"manifest": manifest}


@router.get("/for-file")
async def get_tests_for_file(path: str, file_path: str):
    """Get all tests associated with a specific source file."""
    if not os.path.exists(path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    manifest = load_manifest(path)
    file_data = manifest.get(file_path, {})
    
    tests = []
    
    # Check if file has changed semantically since last generation
    has_changes = False
    analysis = None
    try:
        abs_file_path = os.path.join(path, file_path) if not file_path.startswith('/') else file_path
        if os.path.exists(abs_file_path):
            analysis = analyze_file(abs_file_path)
            if analysis:
                current_hash = analysis.file_hash
                stored_hash = file_data.get("file_hash")
                if current_hash != stored_hash:
                    has_changes = True
    except Exception as e:
        print(f"Error checking file changes: {e}")

    # Build function line map from analysis
    function_lines = {}
    if analysis:
         for name, info in analysis.functions.items():
            function_lines[name] = info.start_line
         for class_name, methods in analysis.classes.items():
            for name, info in methods.items():
                function_lines[f"{class_name}.{name}"] = info.start_line
                function_lines[name] = info.start_line # Allow short match

    import ast
    
    def resolve_target_line(test_name: str) -> Optional[int]:
         # 1. Exact match (test_foo -> foo)
         target = test_name.replace("test_", "")
         if target in function_lines:
             return function_lines[target]
             
         # 2. Contains match (test_vector_multiply_empty -> vector_multiply)
         # Find longest key in function_lines that is a substring of target
         matches = [k for k in function_lines.keys() if k in target]
         if matches:
             best_match = max(matches, key=len)
             return function_lines[best_match]
             
         return None

    def get_test_source(func_name, content):
        if not content: return None
        try:
            # Simple AST extraction
            tree = ast.parse(content)
            for node in ast.walk(tree):
                if isinstance(node, ast.FunctionDef) and node.name == func_name:
                    return ast.get_source_segment(content, node)
        except:
            pass
        return None

    # Load test file content for code extraction
    test_filename = f"test_{Path(file_path).name}"
    test_path = get_arcturus_tests_dir(path) / test_filename
    test_file_content = ""
    if test_path.exists():
        try:
            test_file_content = test_path.read_text()
        except:
            pass

    # 1. Add top-level tests (from file-level generation)
    seen_tests = set()
    for test_id in file_data.get("tests", []):
        if test_id in seen_tests: continue
        seen_tests.add(test_id)
        
        # Read persisted status/timestamp
        stored_status = file_data.get("test_statuses", {}).get(test_id, {})
        status = stored_status.get("status", "pending")
        last_run = stored_status.get("last_run", None)
        message = stored_status.get("message", None)
        
        if has_changes: status = "stale"
            
        tests.append({
            "id": test_id,
            "name": test_id.replace("test_", "").replace("_", " ").title(),
            "status": status,
            "type": "behavior", 
            "lastRun": last_run,
            "message": message,
            "code": get_test_source(test_id, test_file_content),
            "target_line": resolve_target_line(test_id)
        })

    # 2. Add function-level tests
    for func_name, func_data in file_data.get("functions", {}).items():
        for test_id in func_data.get("tests", []):
            if test_id in seen_tests: continue
            seen_tests.add(test_id)
            
            status = func_data.get("status", "pending")
            # Mark as stale if file changed
            if has_changes:
                status = "stale"
                
            tests.append({
                "id": test_id,
                "name": test_id.replace("test_", "").replace("_", " ").title(),
                "status": status,
                "type": func_data.get("type", "behavior"),
                "lastRun": func_data.get("last_run"),
                "message": func_data.get("message"),
                "code": get_test_source(test_id, test_file_content),
                "target_line": resolve_target_line(test_id)
            })
    
    return {"tests": tests, "file": file_path, "has_changes": has_changes}


@router.post("/generate")
async def generate_tests(request: GenerateTestsRequest):
    """
    Trigger test generation for functions in a file using AST analysis.
    Only generates tests for modified functions unless forced.
    """
    if not os.path.exists(request.path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    manifest = load_manifest(request.path)
    
    abs_file_path = os.path.join(request.path, request.file_path) if not request.file_path.startswith('/') else request.file_path
    
    if not os.path.exists(abs_file_path):
        raise HTTPException(status_code=404, detail=f"Source file not found: {request.file_path}")

    # Analyze file
    analysis = analyze_file(abs_file_path)
    if not analysis:
        raise HTTPException(status_code=500, detail="Failed to analyze source file")
        
    # Initialize file entry if needed
    if request.file_path not in manifest:
        manifest[request.file_path] = {
            "tests": [],
            "functions": {},
            "last_generated": None,
            "file_hash": None
        }
        
    file_entry = manifest[request.file_path]
    
    # Identify changed functions
    functions_to_process = []
    
    if request.force:
        functions_to_process = list(analysis.functions.keys())
        for class_name, methods in analysis.classes.items():
            for method_name in methods:
                 functions_to_process.append(f"{class_name}.{method_name}")
    else:
        # Compare hashes
        for func_name, info in analysis.functions.items():
            stored_info = file_entry["functions"].get(func_name, {})
            if stored_info.get("body_hash") != info.body_hash:
                functions_to_process.append(func_name)
                # Update stored info
                if func_name not in file_entry["functions"]:
                    file_entry["functions"][func_name] = {}
                file_entry["functions"][func_name]["body_hash"] = info.body_hash

        for class_name, methods in analysis.classes.items():
            for method_name, info in methods.items():
                full_name = f"{class_name}.{method_name}"
                stored_info = file_entry["functions"].get(full_name, {})
                if stored_info.get("body_hash") != info.body_hash:
                    functions_to_process.append(full_name)
                    if full_name not in file_entry["functions"]:
                        file_entry["functions"][full_name] = {}
                    file_entry["functions"][full_name]["body_hash"] = info.body_hash

    # Save manifest updates
    file_entry["file_hash"] = analysis.file_hash
    file_entry["last_generated"] = datetime.now().isoformat()
    save_manifest(request.path, manifest)
    
    if not functions_to_process:
        return {
            "success": True,
            "message": "No semantic changes detected. No tests generated.",
            "functions": []
        }

    # Invoke Test Agent
    try:
        # read source code
        with open(abs_file_path, 'r') as f:
            source_code = f.read()
            
        # Load existing tests if available
        test_filename = f"test_{Path(request.file_path).name}"
        test_path = get_arcturus_tests_dir(request.path) / test_filename
        existing_tests_code = ""
        if test_path.exists():
            with open(test_path, 'r') as f:
                existing_tests_code = f.read()

        runner = AgentRunner(multi_mcp=None) # No MCP needed for TestAgent
        
        # Prepare context
        deleted_msg = f"Deleted Functions: {', '.join(request.deleted_functions)}\n" if request.deleted_functions else ""
        context = (f"Project: {os.path.basename(request.path)}. Analyzing file: {request.file_path}\n"
                   f"{deleted_msg}"
                   f"TASK: Generate/Update tests. Source code has changed. "
                   f"Update existing tests to match new logic. Remove tests for deleted functions.")

        input_data = {
            "source_code": source_code,
            "existing_tests": existing_tests_code, 
            "context": context,
            "deleted_functions": request.deleted_functions or []
        }
        
        # Run agent
        result = await runner.run_agent("TestAgent", input_data)
        
        if not result.get("success"):
            raise Exception(result.get("error", "TestAgent failed"))
        
        # Extract test code from agent output
        agent_output = result.get("output", {})
        
        # Agent returns structured JSON with 'test_code' key
        if isinstance(agent_output, dict):
            # Get test_code from the structured response (preferred)
            code = agent_output.get("test_code", "")
            tests_generated = agent_output.get("tests_generated", [])
            
            # Fallback to other possible keys
            if not code:
                code = agent_output.get("code") or ""
            
            # If still no code, stringify the output
            if not code:
                code = str(agent_output)
        else:
            code = str(agent_output)
            tests_generated = []

        # Handle DELETED signal
        if code.strip() == "DELETED":
            if test_path.exists():
                os.remove(test_path)
            # Remove from manifest
            if request.file_path in manifest:
                manifest[request.file_path]["tests"] = [t for t in manifest[request.file_path].get("tests", []) if t != str(test_filename)]
                save_manifest(request.path, manifest)
            return {
                "success": True,
                "message": f"Test file {test_filename} deleted (source cleanup)",
                "test_file": None,
                "test_type": request.test_type
            }
        
        # Handle escaped newlines (from JSON string)
        if "\\n" in code:
            code = code.replace("\\n", "\n")
        
        # Fallback: extract code block from markdown-formatted response
        if "```python" in code:
            code = code.split("```python")[1].split("```")[0].strip()
        elif "```" in code:
            code = code.split("```")[1].split("```")[0].strip()
        
        # Save generated tests
        test_filename = f"test_{Path(request.file_path).name}"
        test_path = get_arcturus_tests_dir(request.path) / test_filename
        test_path.write_text(code)
        
        # ---------------------------------------------------------
        # UPDATE MANIFEST WITH PARSED TESTS
        # ---------------------------------------------------------
        try:
            import ast
            tree = ast.parse(code)
            found_tests = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name.startswith("test_")]
            
            if found_tests:
                # Reload manifest to get latest state
                manifest = load_manifest(request.path)
                if request.file_path not in manifest:
                    manifest[request.file_path] = {}
                
                # Update top-level tests list
                manifest[request.file_path]["tests"] = list(set(manifest[request.file_path].get("tests", []) + found_tests))
                
                # Optional: Try to map to functions if possible (skipped for now to be safe)
                # Just ensuring they exist in the file entry is enough for get_tests_for_file now.
                
                save_manifest(request.path, manifest)
                print(f"✅ Updated manifest with {len(found_tests)} tests for {request.file_path}")
        except Exception as e:
             print(f"⚠️ Failed to parse generated tests for manifest update: {e}")
        # ---------------------------------------------------------
        
        return {
            "success": True,
            "message": f"Generated tests for {len(functions_to_process)} functions",
            "changed_functions": functions_to_process,
            "test_file": str(test_path)
        }
        
    except Exception as e:
        print(f"Test Agent failed: {e}")
        raise HTTPException(status_code=500, detail=f"Test generation failed: {str(e)}")


@router.post("/generate-spec")
async def generate_spec_tests(request: GenerateTestsRequest):
    """
    Generate spec-based tests from docstrings and comments.
    This is triggered manually by the user.
    """
    if not os.path.exists(request.path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    abs_file_path = os.path.join(request.path, request.file_path) if not request.file_path.startswith('/') else request.file_path
    
    if not os.path.exists(abs_file_path):
        raise HTTPException(status_code=404, detail=f"Source file not found: {request.file_path}")

    # Invoke Test Agent for Spec Generation
    try:
        # read source code
        with open(abs_file_path, 'r') as f:
            source_code = f.read()

        runner = AgentRunner(multi_mcp=None)
        input_data = {
            "source_code": source_code,
            "existing_tests": "", 
            "context": f"Project: {os.path.basename(request.path)}. Analyzing file: {request.file_path}\n"
                       f"TASK: Generate SPEC TESTS. Strictly test against the contracts defined in docstrings, type hints, and comments. "
                       f"Do not test implementation details not promised in the spec."
        }
        
        # Run agent
        result = await runner.run_agent("TestAgent", input_data)
        
        if not result.get("success"):
            raise Exception(result.get("error", "TestAgent failed"))
        
        # Extract test code from agent output
        agent_output = result.get("output", {})
        
        if isinstance(agent_output, dict):
            code = agent_output.get("test_code") or agent_output.get("code") or str(agent_output)
        else:
            code = str(agent_output)
        
        # Handle DELETED signal
        if code.strip() == "DELETED":
            if test_path.exists():
                os.remove(test_path)
            # Remove from manifest
            if request.file_path in manifest:
                manifest[request.file_path]["tests"] = [t for t in manifest[request.file_path].get("tests", []) if t != str(test_filename)]
                save_manifest(request.path, manifest)
            return {
                "success": True,
                "message": f"Test file {test_filename} deleted (source cleanup)",
                "test_file": None,
                "test_type": request.test_type
            }

        # Extract code block if markdown formatted
        if "```python" in code:
            code = code.split("```python")[1].split("```")[0].strip()
        elif "```" in code:
            code = code.split("```")[1].split("```")[0].strip()
        
        # Save generated tests
        test_filename = f"test_{Path(request.file_path).name.replace('.py', '_spec.py')}"
        test_path = get_arcturus_tests_dir(request.path) / test_filename
        test_path.write_text(code)
        
        return {
            "success": True,
            "message": f"Spec tests generated at {test_filename}",
            "test_file": str(test_path),
            "test_type": "spec"
        }
        
    except Exception as e:
        print(f"Test Agent failed: {e}")
        raise HTTPException(status_code=500, detail=f"Spec test generation failed: {str(e)}")


@router.put("/activate")
async def toggle_test_activation(path: str, file_path: str, test_id: str, active: bool):
    """Toggle whether a test is active (will be run)."""
    if not os.path.exists(path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    manifest = load_manifest(path)
    
    if file_path not in manifest:
        manifest[file_path] = {"tests": [], "functions": {}}
    
    # Find and update the test
    for func_name, func_data in manifest[file_path].get("functions", {}).items():
        if test_id in func_data.get("tests", []):
            if "active" not in func_data:
                func_data["active"] = []
            
            if active and test_id not in func_data["active"]:
                func_data["active"].append(test_id)
            elif not active and test_id in func_data["active"]:
                func_data["active"].remove(test_id)
    
    save_manifest(path, manifest)
    
    return {"success": True, "test_id": test_id, "active": active}


@router.post("/fix")
async def fix_test_failures(request: FixTestsRequest):
    """
    Analyze test failures and fix the corresponding source code using DebuggerAgent.
    """
    if not os.path.exists(request.path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    runner = AgentRunner(multi_mcp=None)
    fixed_files = []
    
    # Group failures by test file to handle them in batches per source file
    failures_by_file = {}
    for failure in request.failures:
        # failure structure from pytest json: {'test_id': 'tests/unit/test_foo.py::test_bar', 'message': '...'}
        test_id = failure.get("test_id", "")
        if "::" in test_id:
            test_file = test_id.split("::")[0]
            # pytest returned path relative to cwd (project root)
            # but our tests are in .arcturus/tests
            
            # Resolve source file
            source_file = get_source_file_from_test(os.path.basename(test_file), request.path)
            if source_file:
                if source_file not in failures_by_file:
                    failures_by_file[source_file] = []
                failures_by_file[source_file].append(failure)
    
    # Process each source file
    for source_file, failures in failures_by_file.items():
        try:
            # Read source code
            with open(source_file, 'r') as f:
                source_code = f.read()
            
            # Prepare failure report
            failure_report = "\n".join([
                f"Test: {f.get('test_id')}\nError: {f.get('message')}\n---" 
                for f in failures
            ])
            
            input_data = {
                "source_code": source_code,
                "test_context": "Test execution failed.", # Could read test file content if needed
                "failure_report": failure_report
            }
            
            # Run DebuggerAgent
            print(f"🤖 DebuggerAgent fixing {os.path.basename(source_file)}...")
            result = await runner.run_agent("DebuggerAgent", input_data)
            
            if result.get("success"):
                output = result.get("output", {})
                if isinstance(output, dict) and "fixed_code" in output:
                    new_code = output["fixed_code"]
                    explanation = output.get("explanation", "Fixed based on test failures")
                    
                    # Verify it's not empty
                    if new_code and len(new_code) > 10:
                        # Write back to file
                        with open(source_file, 'w') as f:
                            f.write(new_code)
                        fixed_files.append({
                            "file": os.path.basename(source_file),
                            "explanation": explanation
                        })
        except Exception as e:
            print(f"Failed to fix {source_file}: {e}")
            
    return {
        "success": True,
        "fixed_files": fixed_files,
        "message": f"Fixed {len(fixed_files)} files"
    }


@router.post("/sync")
async def sync_missing_tests(request: SyncTestsRequest):
    """
    Check for Python files modified in recent commits that don't have tests,
    and trigger generation for them.
    """
    if not os.path.exists(request.path):
        raise HTTPException(status_code=404, detail="Path not found")
        
    import subprocess
    
    # 1. Get modified files in last 10 commits on arcturus branch
    try:
        # Check if arcturus branch exists
        subprocess.run(["git", "rev-parse", "--verify", "arcturus"], cwd=request.path, check=True, capture_output=True)
        
        # Get files
        cmd = ["git", "log", "arcturus", "-n", "10", "--name-only", "--pretty=format:"]
        result = subprocess.run(cmd, cwd=request.path, capture_output=True, text=True)
        
        if result.returncode != 0:
            return {"success": False, "message": "Failed to get git log"}
            
        files = set()
        for line in result.stdout.split("\n"):
            line = line.strip()
            if line and line.endswith(".py") and os.path.exists(os.path.join(request.path, line)):
                files.add(line)
        
        # 2. Check for missing tests (and repair manifest if needed)
        triggered = []
        repaired = []
        tests_dir = get_arcturus_tests_dir(request.path)
        manifest = load_manifest(request.path)
        
        for py_file in files:
            # Check manifest first
            in_manifest = False
            if py_file in manifest:
                entry = manifest[py_file]
                if entry.get("tests") or any(f.get("tests") for f in entry.get("functions", {}).values()):
                    in_manifest = True
            
            # Check disk
            # Matches generate_tests naming: test_{filename}
            test_filename = f"test_{Path(py_file).name}"
            test_path = tests_dir / test_filename
            
            if in_manifest:
                continue

            if test_path.exists():
                # Exists on disk but not in manifest -> REPAIR
                print(f"🔧 Sync: Repairing manifest for {py_file} (found {test_filename})")
                try:
                    import ast
                    code = test_path.read_text()
                    tree = ast.parse(code)
                    found_tests = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name.startswith("test_")]
                    
                    if found_tests:
                        if py_file not in manifest: manifest[py_file] = {}
                        # Merge with existing just in case
                        existing = set(manifest[py_file].get("tests", []))
                        existing.update(found_tests)
                        manifest[py_file]["tests"] = list(existing)
                        save_manifest(request.path, manifest)
                        repaired.append(py_file)
                except Exception as e:
                    print(f"⚠️ Repair failed for {py_file}: {e}")
            else:
                # Missing completely -> GENERATE
                from routers.tests import generate_tests # Import here
                
                print(f"🔄 Sync: Generating missing tests for {py_file}")
                gen_req = GenerateTestsRequest(
                    path=request.path,
                    file_path=py_file,
                    force=False
                )
                await generate_tests(gen_req)
                triggered.append(py_file)
                
        return {
            "success": True, 
            "triggered": triggered, 
            "repaired": repaired,
            "scanned": list(files)
        }
        
    except Exception as e:
        print(f"Sync failed: {e}")
        return {"success": False, "error": str(e)}


@router.post("/run")
async def run_tests(request: RunTestsRequest, background_tasks: BackgroundTasks):
    """
    Run the specified tests using pytest.
    Returns results after execution.
    """
    import subprocess
    import tempfile
    import sys
    import re
    
    if not os.path.exists(request.path):
        raise HTTPException(status_code=404, detail="Path not found")
        
    # Auto-install dependencies if missing
    try:
        import pytest
        import pytest_json_report
    except ImportError:
        print("📦 Installing pytest dependencies (pytest, pytest-json-report)...")
        subprocess.run(
            [sys.executable, "-m", "pip", "install", "pytest", "pytest-json-report"],
            cwd=request.path, check=False, capture_output=True
        )
    
    tests_dir = get_arcturus_tests_dir(request.path)
    
    if not tests_dir.exists():
        return {
            "success": False,
            "error": "No tests directory found",
            "results": [],
            "total": 0,
            "passed": 0,
            "failed": 0
        }
    
    # Build pytest command
    # If specific test_ids provided, filter; otherwise run all
    test_files = list(tests_dir.glob("test_*.py"))
    
    if not test_files:
        return {
            "success": True,
            "results": [],
            "total": 0,
            "passed": 0,
            "failed": 0,
            "message": "No test files found"
        }
    
    # Create temp file for JSON output
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        json_output_path = f.name
    
    try:
        # Run pytest with JSON report
        print(f"🐍 Executing tests using Python: {sys.executable}")
        cmd = [
            sys.executable, "-m", "pytest",
            str(tests_dir),
            "-v",
            "--tb=short",
            f"--json-report",
            f"--json-report-file={json_output_path}",
            "--json-report-indent=2"
        ]
        
        print(f"[DEBUG] Running pytest command: {' '.join(cmd)}")
        
        result = subprocess.run(
            cmd,
            cwd=request.path,
            capture_output=True,
            text=True,
            timeout=120  # 2 min timeout
        )
        
        print(f"[DEBUG] Pytest stdout:\n{result.stdout}")
        print(f"[DEBUG] Pytest stderr:\n{result.stderr}")
        
        # Parse JSON output
        results = []
        passed = 0
        failed = 0
        
        if os.path.exists(json_output_path):
            try:
                with open(json_output_path, 'r') as f:
                    report = json.load(f)
                
                for test in report.get("tests", []):
                    test_name = test.get("nodeid", "")
                    outcome = test.get("outcome", "unknown")
                    
                    # Map 'error' outcome to 'failed' for consistent handling
                    if outcome == "error":
                        outcome = "failed"
                    
                    status = "passing" if outcome == "passed" else "failing"
                    
                    if status == "passing":
                        passed += 1
                    else:
                        failed += 1
                    
                    results.append({
                        "test_id": test_name,
                        "name": test_name.split("::")[-1] if "::" in test_name else test_name,
                        "status": status,
                        "duration": test.get("call", {}).get("duration", 0),
                        "message": test.get("call", {}).get("longrepr", "") if outcome == "failed" else ""
                    })
                    
            except json.JSONDecodeError:
                pass
        
        # Fallback to stdout parsing if JSON failed
        if not results:
            stdout = result.stdout
            # Simple parsing of pytest output
            for line in stdout.split("\n"):
                if "PASSED" in line:
                    passed += 1
                    test_name = line.split(" ")[0] if " " in line else line
                    results.append({"test_id": test_name, "status": "passing"})
                elif "FAILED" in line:
                    failed += 1
                    test_name = line.split(" ")[0] if " " in line else line
                    results.append({"test_id": test_name, "status": "failing"})
        
        # If still no results and execution failed, return error
        if not results and (result.returncode != 0 or passed == 0 and failed == 0):
            error_msg = result.stderr or result.stdout
            
            # Strip ANSI codes
            ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
            error_msg = ansi_escape.sub('', error_msg)
            
            # Truncate if too long (keep enough to see error)
            if len(error_msg) > 4000: error_msg = error_msg[:4000] + "..."
            
            return {
                "success": False,
                "error": f"Test Execution Failed:\n{error_msg}",
                "results": [],
                "total": 0,
                "passed": 0,
                "failed": 0
            }
        
        # Update manifest with results
        manifest = load_manifest(request.path)
        for res in results:
            test_id = res["test_id"]
            timestamp = datetime.now().isoformat()
            
            # Helper to match loose test IDs
            def match_id(stored_id, incoming_id):
                return stored_id == incoming_id or incoming_id.endswith("::" + stored_id) or stored_id in incoming_id

            for file_path, file_data in manifest.items():
                match_found = False
                
                # Check top-level tests
                for t in file_data.get("tests", []):
                    if match_id(t, test_id):
                        if "test_statuses" not in file_data:
                            file_data["test_statuses"] = {}
                        file_data["test_statuses"][t] = {
                            "status": res["status"],
                            "last_run": timestamp,
                            "message": res.get("message")
                        }
                        match_found = True
                
                # Check function-level tests
                for func_name, func_info in file_data.get("functions", {}).items():
                    if any(match_id(t, test_id) for t in func_info.get("tests", [])):
                        func_info["status"] = res["status"]
                        func_info["last_run"] = timestamp
                        func_info["message"] = res.get("message")
                        match_found = True
                        
        save_manifest(request.path, manifest)
                        
        save_manifest(request.path, manifest)
        
        # Check feedback mode and trigger auto-fix if needed
        settings = load_settings(request.path)
        feedback_mode = settings.get("testing", {}).get("feedback_mode", "with_permission")
        
        failed_tests = [r for r in results if r["status"] == "failing"]
        
        if failed > 0 and feedback_mode == "always":
            print(f"🔄 Feedback mode 'always': Triggering auto-fix for {len(failed_tests)} failures")
            # Create fix request
            fix_req = FixTestsRequest(path=request.path, failures=failed_tests)
            # Run in background to return results to UI quickly
            background_tasks.add_task(fix_test_failures, fix_req)
        
        return {
            "success": True,
            "results": results,
            "total": len(results),
            "passed": passed,
            "failed": failed,
            "feedback_mode": feedback_mode,
            "stderr": result.stderr if result.returncode != 0 else None
        }
        
    except subprocess.TimeoutExpired:
        return {
            "success": False,
            "error": "Test execution timed out (120s limit)",
            "results": [],
            "total": 0,
            "passed": 0,
            "failed": 0
        }
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "results": [],
            "total": 0,
            "passed": 0,
            "failed": 0
        }
    finally:
        # Cleanup temp file
        if os.path.exists(json_output_path):
            os.unlink(json_output_path)


@router.get("/results")
async def get_test_results(path: str, file_path: Optional[str] = None):
    """Get the latest test results for a project or specific file."""
    if not os.path.exists(path):
        raise HTTPException(status_code=404, detail="Path not found")
    
    # TODO: Read from pytest output/cache
    
    return {
        "results": [],
        "last_run": None,
        "summary": {
            "total": 0,
            "passed": 0,
            "failed": 0,
            "skipped": 0
        }
    }
