"""
Test Execution API for Archon

Provides FastAPI endpoints for executing tests (pytest, vitest) with real-time streaming output.
Includes WebSocket streaming, background task management, and test result tracking.
"""

import asyncio
import os
import shutil
import uuid
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any

from fastapi import APIRouter, BackgroundTasks, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

# Removed direct logging import - using unified config
# Import logfire for comprehensive API logging
from ..config.logfire_config import get_logger, logfire

logger = get_logger(__name__)

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


# Test execution status enum
class TestStatus(str, Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"


# Test type enum
class TestType(str, Enum):
    MCP = "mcp"
    UI = "ui"


# Pydantic models for API requests/responses
class TestExecutionRequest(BaseModel):
    test_type: TestType
    options: dict[str, Any] | None = {}


class TestExecutionResponse(BaseModel):
    execution_id: str
    test_type: TestType
    status: TestStatus
    started_at: datetime
    message: str


class TestStatusResponse(BaseModel):
    execution_id: str
    test_type: TestType
    status: TestStatus
    started_at: datetime
    completed_at: datetime | None = None
    duration_seconds: float | None = None
    exit_code: int | None = None
    summary: dict[str, Any] | None = None


class TestHistoryResponse(BaseModel):
    executions: list[TestStatusResponse]
    total_count: int


# Data classes for test execution tracking
@dataclass
class TestExecution:
    execution_id: str
    test_type: TestType
    status: TestStatus
    started_at: datetime
    completed_at: datetime | None = None
    exit_code: int | None = None
    output_lines: list[str] = None
    summary: dict[str, Any] | None = None
    process: asyncio.subprocess.Process | None = None

    def __post_init__(self):
        if self.output_lines is None:
            self.output_lines = []

    @property
    def duration_seconds(self) -> float | None:
        if self.completed_at and self.started_at:
            return (self.completed_at - self.started_at).total_seconds()
        return None


# Global state for test executions
test_executions: dict[str, TestExecution] = {}
active_websockets: dict[str, list[WebSocket]] = {}


# WebSocket connection manager
class TestWebSocketManager:
    def __init__(self):
        self.connections: dict[str, list[WebSocket]] = {}

    async def connect(self, websocket: WebSocket, execution_id: str):
        await websocket.accept()
        if execution_id not in self.connections:
            self.connections[execution_id] = []
        self.connections[execution_id].append(websocket)
        logger.info(f"WebSocket connected for execution {execution_id}")

    def disconnect(self, websocket: WebSocket, execution_id: str):
        if execution_id in self.connections:
            self.connections[execution_id].remove(websocket)
            if not self.connections[execution_id]:
                del self.connections[execution_id]
        logger.info(f"WebSocket disconnected for execution {execution_id}")

    async def broadcast_to_execution(self, execution_id: str, message: dict):
        if execution_id in self.connections:
            disconnected = []
            for websocket in self.connections[execution_id]:
                try:
                    await websocket.send_json(message)
                except:
                    disconnected.append(websocket)

            # Remove disconnected websockets
            for ws in disconnected:
                self.disconnect(ws, execution_id)


websocket_manager = TestWebSocketManager()


# Test execution functions
async def execute_mcp_tests(execution_id: str) -> TestExecution:
    """Execute Python tests using pytest with real-time streaming and coverage reporting."""
    execution = test_executions[execution_id]
    logger.info(f"[DEBUG] Starting execute_mcp_tests for execution_id: {execution_id}")

    try:
        # Create coverage reports directory if it doesn't exist
        os.makedirs("/app/coverage_reports/pytest", exist_ok=True)
        logger.info("[DEBUG] Created coverage reports directory")

        # Use pytest - run only the new simplified tests (coverage disabled for now)
        cmd = [
            "pytest",
            "-v",  # verbose output
            "-s",  # don't capture stdout, allows real-time output
            "--tb=short",  # shorter traceback format
            "--no-header",  # cleaner output
            "--disable-warnings",  # cleaner output
            "tests/test_api_essentials.py",  # run specific test files
            "tests/test_service_integration.py",
            "tests/test_business_logic.py",
        ]

        logger.info(f"Starting Python test execution: {' '.join(cmd)}")
        logger.info(f"[DEBUG] Current working directory: {os.getcwd()}")
        logger.info(f"[DEBUG] /app/tests directory exists: {os.path.exists('/app/tests')}")
        logger.info(
            f"[DEBUG] Test files exist: {[os.path.exists(f'/app/{f}') for f in ['tests/test_api_essentials.py', 'tests/test_service_integration.py', 'tests/test_business_logic.py']]}"
        )

        # Check if pytest is available
        pytest_path = shutil.which("pytest")
        logger.info(f"[DEBUG] pytest executable path: {pytest_path}")
        logger.info(f"[DEBUG] PATH environment: {os.environ.get('PATH', 'NOT SET')}")

        # Start process with line buffering for real-time output
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
            cwd="/app",  # Use the app directory inside the container
            env={**os.environ, "PYTHONUNBUFFERED": "1"},  # Ensure unbuffered output
        )

        logger.info(f"[DEBUG] Process created with PID: {process.pid if process else 'None'}")

        execution.process = process
        execution.status = TestStatus.RUNNING

        # Stream output in real-time
        await stream_process_output(execution_id, process)

        # Wait for completion
        exit_code = await process.wait()
        execution.exit_code = exit_code
        execution.completed_at = datetime.now()

        if exit_code == 0:
            execution.status = TestStatus.COMPLETED
            execution.summary = {"result": "All Python tests passed", "exit_code": exit_code}
        else:
            execution.status = TestStatus.FAILED
            execution.summary = {"result": "Some Python tests failed", "exit_code": exit_code}

        logger.info(f"Python tests completed with exit code: {exit_code}")

    except Exception as e:
        logger.error(f"Error executing Python tests: {e}")
        execution.status = TestStatus.FAILED
        execution.completed_at = datetime.now()
        execution.summary = {"error": str(e)}

        # Broadcast error
        await websocket_manager.broadcast_to_execution(
            execution_id,
            {
                "type": "error",
                "message": f"Test execution failed: {str(e)}",
                "timestamp": datetime.now().isoformat(),
            },
        )

    # Broadcast completion
    await websocket_manager.broadcast_to_execution(
        execution_id,
        {
            "type": "completed",
            "status": execution.status.value,
            "exit_code": execution.exit_code,
            "summary": execution.summary,
            "timestamp": datetime.now().isoformat(),
        },
    )

    return execution


async def execute_ui_tests(execution_id: str) -> TestExecution:
    """Execute React UI tests - for now, return mock results since Docker-in-Docker is not available."""
    execution = test_executions[execution_id]
    logger.info(f"[DEBUG] Starting execute_ui_tests for execution_id: {execution_id}")

    try:
        # Since we can't run docker exec from inside the container,
        # we'll simulate test execution with mock results for now
        execution.status = TestStatus.RUNNING

        # Send initial status
        await websocket_manager.broadcast_to_execution(
            execution_id,
            {
                "type": "status",
                "data": {"status": "running"},
                "message": "UI test execution started (simulated)",
                "timestamp": datetime.now().isoformat(),
            },
        )

        # Simulate test output
        test_output = [
            "Running React UI tests...",
            "",
            "✓ test/components.test.tsx (10 tests) 77ms",
            "✓ test/errors.test.tsx (5 tests) 45ms",
            "✓ test/pages.test.tsx (5 tests) 15ms",
            "✓ test/user_flows.test.tsx (10 tests) 66ms",
            "",
            "Test Files  4 passed (4)",
            "     Tests  30 passed (30)",
            "  Duration  203ms",
            "",
            "All tests passed!",
        ]

        # Stream output lines
        for line in test_output:
            execution.output_lines.append(line)
            await websocket_manager.broadcast_to_execution(
                execution_id,
                {"type": "output", "message": line, "timestamp": datetime.now().isoformat()},
            )
            await asyncio.sleep(0.1)  # Small delay to simulate real output

        # Mark as completed
        execution.status = TestStatus.COMPLETED
        execution.completed_at = datetime.now()
        execution.exit_code = 0
        execution.summary = {"result": "All React UI tests passed (simulated)", "exit_code": 0}

        logger.info("UI tests completed (simulated)")

        # NOTE: To properly run UI tests, you would need to either:
        # 1. Install Docker CLI in the server container
        # 2. Use a separate test runner service
        # 3. Expose a test endpoint in the UI container
        logger.warning(
            "UI tests are currently simulated. Real execution requires Docker-in-Docker setup."
        )

        execution.process = process
        execution.status = TestStatus.RUNNING

        # Stream output in real-time
        await stream_process_output(execution_id, process)

        # Wait for completion
        exit_code = await process.wait()
        execution.exit_code = exit_code
        execution.completed_at = datetime.now()

        # Copy coverage reports from frontend container to server directory
        if exit_code == 0:
            try:
                # Copy coverage summary JSON
                copy_cmd = [
                    "docker",
                    "cp",
                    "archon-frontend-1:/app/archon-ui-main/coverage/coverage-summary.json",
                    "/app/coverage_reports/vitest/",
                ]
                await asyncio.create_subprocess_exec(*copy_cmd)

                # Copy HTML coverage report directory
                copy_html_cmd = [
                    "docker",
                    "cp",
                    "archon-frontend-1:/app/archon-ui-main/coverage/",
                    "/app/coverage_reports/vitest/html",
                ]
                await asyncio.create_subprocess_exec(*copy_html_cmd)

            except Exception as e:
                logger.warning(f"Failed to copy coverage reports: {e}")

        if exit_code == 0:
            execution.status = TestStatus.COMPLETED
            execution.summary = {"result": "All React UI tests passed", "exit_code": exit_code}
        else:
            execution.status = TestStatus.FAILED
            execution.summary = {"result": "Some React UI tests failed", "exit_code": exit_code}

        logger.info(f"React UI tests completed with exit code: {exit_code}")

    except Exception as e:
        logger.error(f"Error executing React UI tests: {e}")
        execution.status = TestStatus.FAILED
        execution.completed_at = datetime.now()
        execution.summary = {"error": str(e)}

        # Broadcast error
        await websocket_manager.broadcast_to_execution(
            execution_id,
            {
                "type": "error",
                "message": f"Test execution failed: {str(e)}",
                "timestamp": datetime.now().isoformat(),
            },
        )

    # Broadcast completion
    await websocket_manager.broadcast_to_execution(
        execution_id,
        {
            "type": "completed",
            "status": execution.status.value,
            "exit_code": execution.exit_code,
            "summary": execution.summary,
            "timestamp": datetime.now().isoformat(),
        },
    )

    return execution


async def stream_process_output(execution_id: str, process: asyncio.subprocess.Process):
    """Stream process output to WebSocket clients with improved real-time handling."""
    execution = test_executions[execution_id]
    logger.info(f"[DEBUG] Starting stream_process_output for execution_id: {execution_id}")

    # Send initial status update
    await websocket_manager.broadcast_to_execution(
        execution_id,
        {
            "type": "status",
            "data": {"status": "running"},
            "message": "Test execution started",
            "timestamp": datetime.now().isoformat(),
        },
    )

    line_count = 0

    while True:
        try:
            # Use a timeout to prevent hanging
            line = await asyncio.wait_for(process.stdout.readline(), timeout=30.0)
            if not line:
                break

            decoded_line = line.decode("utf-8").rstrip()
            line_count += 1
            logger.info(
                f"[DEBUG] Line {line_count}: {decoded_line[:100]}..."
            )  # Log first 100 chars
            if decoded_line:  # Only add non-empty lines
                execution.output_lines.append(decoded_line)

                # Broadcast to WebSocket clients immediately
                await websocket_manager.broadcast_to_execution(
                    execution_id,
                    {
                        "type": "output",
                        "message": decoded_line,
                        "timestamp": datetime.now().isoformat(),
                    },
                )

        except TimeoutError:
            # Check if process is still alive
            if process.returncode is not None:
                break
            # Send heartbeat to keep connection alive
            await websocket_manager.broadcast_to_execution(
                execution_id,
                {
                    "type": "status",
                    "data": {"status": "running"},
                    "message": "Tests still running...",
                    "timestamp": datetime.now().isoformat(),
                },
            )
        except Exception as e:
            logger.error(f"Error streaming output: {e}")
            logger.error(f"[DEBUG] Exception type: {type(e).__name__}")
            logger.error(f"[DEBUG] Exception details: {str(e)}")
            break

    logger.info(f"[DEBUG] Stream ended. Total lines read: {line_count}")


async def execute_tests_background(execution_id: str, test_type: TestType):
    """Background task for test execution - removed ALL type."""
    try:
        if test_type == TestType.MCP:
            await execute_mcp_tests(execution_id)
        elif test_type == TestType.UI:
            await execute_ui_tests(execution_id)
        else:
            raise ValueError(f"Unknown test type: {test_type}")

    except Exception as e:
        logger.error(f"Background test execution failed: {e}")
        execution = test_executions[execution_id]
        execution.status = TestStatus.FAILED
        execution.completed_at = datetime.now()
        execution.summary = {"error": str(e)}


# API Endpoints


@router.post("/mcp/run", response_model=TestExecutionResponse)
async def run_mcp_tests(request: TestExecutionRequest, background_tasks: BackgroundTasks):
    """Execute Python tests using pytest with real-time streaming output."""
    execution_id = str(uuid.uuid4())

    logger.info("[DEBUG] /api/tests/mcp/run endpoint called")
    logger.info(f"[DEBUG] Request: {request}")
    logfire.info(f"Starting MCP test execution | execution_id={execution_id} | test_type=mcp")

    # Create test execution record
    execution = TestExecution(
        execution_id=execution_id,
        test_type=TestType.MCP,
        status=TestStatus.PENDING,
        started_at=datetime.now(),
    )

    test_executions[execution_id] = execution

    # Start background task
    background_tasks.add_task(execute_tests_background, execution_id, TestType.MCP)

    logfire.info(f"MCP test execution queued successfully | execution_id={execution_id}")

    return TestExecutionResponse(
        execution_id=execution_id,
        test_type=TestType.MCP,
        status=TestStatus.PENDING,
        started_at=execution.started_at,
        message="Python test execution started",
    )


@router.post("/ui/run", response_model=TestExecutionResponse)
async def run_ui_tests(request: TestExecutionRequest, background_tasks: BackgroundTasks):
    """Execute React UI tests using vitest with real-time streaming output."""
    execution_id = str(uuid.uuid4())

    logger.info("[DEBUG] /api/tests/ui/run endpoint called")
    logger.info(f"[DEBUG] Request: {request}")
    logfire.info(f"Starting UI test execution | execution_id={execution_id} | test_type=ui")

    # Create test execution record
    execution = TestExecution(
        execution_id=execution_id,
        test_type=TestType.UI,
        status=TestStatus.PENDING,
        started_at=datetime.now(),
    )

    test_executions[execution_id] = execution

    # Start background task
    background_tasks.add_task(execute_tests_background, execution_id, TestType.UI)

    logfire.info(f"UI test execution queued successfully | execution_id={execution_id}")

    return TestExecutionResponse(
        execution_id=execution_id,
        test_type=TestType.UI,
        status=TestStatus.PENDING,
        started_at=execution.started_at,
        message="React UI test execution started",
    )


@router.get("/status/{execution_id}", response_model=TestStatusResponse)
async def get_test_status(execution_id: str):
    """Get the status of a specific test execution."""
    try:
        logfire.info(f"Getting test execution status | execution_id={execution_id}")

        if execution_id not in test_executions:
            logfire.warning(f"Test execution not found | execution_id={execution_id}")
            raise HTTPException(status_code=404, detail="Test execution not found")

        execution = test_executions[execution_id]

        logfire.info(
            f"Test execution status retrieved | execution_id={execution_id} | status={execution.status} | test_type={execution.test_type}"
        )

        return TestStatusResponse(
            execution_id=execution.execution_id,
            test_type=execution.test_type,
            status=execution.status,
            started_at=execution.started_at,
            completed_at=execution.completed_at,
            duration_seconds=execution.duration_seconds,
            exit_code=execution.exit_code,
            summary=execution.summary,
        )

    except HTTPException:
        raise
    except Exception as e:
        logfire.error(f"Failed to get test status | error={str(e)} | execution_id={execution_id}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/history", response_model=TestHistoryResponse)
async def get_test_history(limit: int = 50, offset: int = 0):
    """Get test execution history."""
    try:
        logfire.info(f"Getting test execution history | limit={limit} | offset={offset}")

        executions = list(test_executions.values())

        # Sort by started_at descending
        executions.sort(key=lambda x: x.started_at, reverse=True)

        # Apply pagination
        total_count = len(executions)
        paginated_executions = executions[offset : offset + limit]

        # Convert to response models
        execution_responses = [
            TestStatusResponse(
                execution_id=exec.execution_id,
                test_type=exec.test_type,
                status=exec.status,
                started_at=exec.started_at,
                completed_at=exec.completed_at,
                duration_seconds=exec.duration_seconds,
                exit_code=exec.exit_code,
                summary=exec.summary,
            )
            for exec in paginated_executions
        ]

        logfire.info(
            f"Test execution history retrieved | total_count={total_count} | returned_count={len(execution_responses)}"
        )

        return TestHistoryResponse(executions=execution_responses, total_count=total_count)

    except Exception as e:
        logfire.error(
            f"Failed to get test history | error={str(e)} | limit={limit} | offset={offset}"
        )
        raise HTTPException(status_code=500, detail=str(e))


@router.delete("/execution/{execution_id}")
async def cancel_test_execution(execution_id: str):
    """Cancel a running test execution."""
    try:
        logfire.info(f"Cancelling test execution | execution_id={execution_id}")

        if execution_id not in test_executions:
            logfire.warning(
                f"Test execution not found for cancellation | execution_id={execution_id}"
            )
            raise HTTPException(status_code=404, detail="Test execution not found")

        execution = test_executions[execution_id]

        if execution.status not in [TestStatus.PENDING, TestStatus.RUNNING]:
            logfire.warning(
                f"Test execution cannot be cancelled | execution_id={execution_id} | status={execution.status}"
            )
            raise HTTPException(status_code=400, detail="Test execution cannot be cancelled")

        # Try to terminate the process
        if execution.process:
            try:
                execution.process.terminate()
                await asyncio.sleep(1)  # Give it a moment to terminate gracefully
                if execution.process.returncode is None:
                    execution.process.kill()
            except Exception as e:
                logfire.warning(
                    f"Error terminating test process | error={str(e)} | execution_id={execution_id}"
                )

        execution.status = TestStatus.CANCELLED
        execution.completed_at = datetime.now()
        execution.summary = {"result": "Test execution cancelled by user"}

        # Broadcast cancellation
        await websocket_manager.broadcast_to_execution(
            execution_id,
            {
                "type": "cancelled",
                "message": "Test execution cancelled",
                "timestamp": datetime.now().isoformat(),
            },
        )

        logfire.info(f"Test execution cancelled successfully | execution_id={execution_id}")

        return {"message": "Test execution cancelled", "execution_id": execution_id}

    except HTTPException:
        raise
    except Exception as e:
        logfire.error(
            f"Failed to cancel test execution | error={str(e)} | execution_id={execution_id}"
        )
        raise HTTPException(status_code=500, detail=str(e))


# WebSocket endpoint for real-time test output
@router.websocket("/stream/{execution_id}")
async def test_output_websocket(websocket: WebSocket, execution_id: str):
    """WebSocket endpoint for streaming test output in real-time."""
    await websocket_manager.connect(websocket, execution_id)

    try:
        # Send existing output if execution exists
        if execution_id in test_executions:
            execution = test_executions[execution_id]

            # Send current status
            await websocket.send_json({
                "type": "status",
                "status": execution.status.value,
                "started_at": execution.started_at.isoformat(),
                "timestamp": datetime.now().isoformat(),
            })

            # Send existing output lines
            for line in execution.output_lines:
                await websocket.send_json({
                    "type": "output",
                    "message": line,
                    "timestamp": datetime.now().isoformat(),
                })

            # If execution is already completed, send completion message
            if execution.status in [TestStatus.COMPLETED, TestStatus.FAILED, TestStatus.CANCELLED]:
                await websocket.send_json({
                    "type": "completed",
                    "status": execution.status.value,
                    "exit_code": execution.exit_code,
                    "summary": execution.summary,
                    "timestamp": datetime.now().isoformat(),
                })

        # Keep connection alive until client disconnects
        while True:
            try:
                # Just wait for client messages (we don't expect any, but this keeps the connection alive)
                await websocket.receive_text()
            except WebSocketDisconnect:
                break

    except WebSocketDisconnect:
        pass
    finally:
        websocket_manager.disconnect(websocket, execution_id)


# Test Results API endpoint


@router.get("/latest-results")
async def get_latest_test_results():
    """Get the latest test results from the most recent execution."""
    try:
        # Get the most recent completed execution
        if not test_executions:
            raise HTTPException(status_code=404, detail="No test results available")

        # Sort executions by started_at descending to get the latest
        executions = list(test_executions.values())
        executions.sort(key=lambda x: x.started_at, reverse=True)

        # Find the most recent completed execution
        latest_execution = None
        for exec in executions:
            if exec.status in [TestStatus.COMPLETED, TestStatus.FAILED]:
                latest_execution = exec
                break

        if not latest_execution:
            raise HTTPException(status_code=404, detail="No completed test results available")

        # Return execution details with output
        return {
            "execution_id": latest_execution.execution_id,
            "test_type": latest_execution.test_type.value,
            "status": latest_execution.status.value,
            "started_at": latest_execution.started_at.isoformat(),
            "completed_at": latest_execution.completed_at.isoformat()
            if latest_execution.completed_at
            else None,
            "duration_seconds": latest_execution.duration_seconds,
            "exit_code": latest_execution.exit_code,
            "summary": latest_execution.summary,
            "output": latest_execution.output_lines,
        }

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Failed to get latest test results: {e}")
        raise HTTPException(status_code=500, detail=str(e))
