"""
Core functionality for MCP Server Tester Python wrapper.
"""

import json
import subprocess
import shutil
from pathlib import Path
from typing import Dict, List, Optional, Any, Union
from dataclasses import dataclass, asdict
import yaml

from .exceptions import (
    NodeJSNotFoundError,
    NPMPackageNotFoundError,
    MCPTestExecutionError,
    MCPConfigurationError,
)


@dataclass
class MCPTestResult:
    """Result of MCP server test execution."""

    success: bool
    passed_tests: int
    total_tests: int
    failed_tests: List[Dict[str, Any]]
    execution_time: float
    output: str
    error: str = ""


@dataclass
class MCPTestConfig:
    """Configuration for MCP server testing."""

    server_config_path: str
    test_path: str
    server_name: Optional[str] = None
    timeout: int = 30000  # milliseconds
    verbose: bool = False
    format: str = "json"  # json, junit, tap
    output_file: Optional[str] = None


class MCPTester:
    """Python wrapper for mcp-server-tester-sse-http-stdio NPM package."""

    def __init__(self, npm_package_name: str = "mcp-server-tester-sse-http-stdio"):
        self.npm_package_name = npm_package_name
        self._check_dependencies()

    def _check_dependencies(self) -> None:
        """Check if Node.js and NPM package are available."""
        # Check Node.js
        if not shutil.which("node"):
            raise NodeJSNotFoundError()

        # Check NPM package
        try:
            result = subprocess.run(
                ["npx", self.npm_package_name, "--help"],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode != 0:
                raise NPMPackageNotFoundError()
        except (subprocess.TimeoutExpired, FileNotFoundError):
            raise NPMPackageNotFoundError()

    def test_server(
        self,
        server_config: Union[str, Path, Dict[str, Any]],
        test_config: Union[str, Path, Dict[str, Any]],
        server_name: Optional[str] = None,
        timeout: int = 30000,
        verbose: bool = False,
        format: str = "json",
        output_file: Optional[str] = None,
    ) -> MCPTestResult:
        """
        Test MCP server with given configuration.

        Args:
            server_config: Path to server config file or config dict
            test_config: Path to test config file or config dict
            server_name: Name of server to test (required if multiple servers in config)
            timeout: Timeout in milliseconds
            verbose: Enable verbose output
            format: Output format (json, junit, tap)
            output_file: Path to save output file

        Returns:
            MCPTestResult with test execution results

        Raises:
            MCPTestExecutionError: If test execution fails
            MCPConfigurationError: If configuration is invalid
        """

        # Handle config files vs dicts
        server_config_path = self._prepare_config(server_config, "server_config")
        test_config_path = self._prepare_config(test_config, "test_config")

        # Build command
        cmd = ["npx", self.npm_package_name, "tools", test_config_path]
        cmd.extend(["--server-config", server_config_path])

        if server_name:
            cmd.extend(["--server-name", server_name])
        if timeout != 30000:
            cmd.extend(["--timeout", str(timeout)])
        if verbose:
            cmd.append("--verbose")
        if format != "json":
            cmd.extend(["--format", format])
        if output_file:
            cmd.extend(["--output", output_file])

        # Execute command
        try:
            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout / 1000.0,  # Convert to seconds
                # Don't specify cwd - let it use the current directory
            )

            # Parse results
            return self._parse_result(result, format)

        except subprocess.TimeoutExpired:
            raise MCPTestExecutionError(f"Test execution timed out after {timeout}ms")
        except Exception as e:
            raise MCPTestExecutionError(f"Test execution failed: {str(e)}")

    def run_tools_test(
        self,
        test_config: Union[str, Path, Dict[str, Any]],
        server_config: Union[str, Path, Dict[str, Any]],
        server_name: Optional[str] = None,
        timeout: int = 30000,
        debug: bool = False,
        junit_xml: Optional[str] = None,
        verbose: bool = False,
    ) -> MCPTestResult:
        """
        Run MCP server tools tests (alias for test_server with tools-specific options).

        Args:
            test_config: Path to test configuration file or config dict
            server_config: Path to server configuration file or config dict
            server_name: Name of server to test (required if multiple servers)
            timeout: Test timeout in milliseconds
            debug: Enable debug output
            junit_xml: Generate JUnit XML output file
            verbose: Enable verbose output

        Returns:
            MCPTestResult: Test execution results

        Raises:
            NodeJSNotFoundError: If Node.js is not installed
            NPMPackageNotFoundError: If NPM package is not available
            MCPTestExecutionError: If test execution fails
            MCPConfigurationError: If configuration is invalid
        """
        # Map to test_server call with appropriate format
        format = "junit" if junit_xml else "json"
        output_file = junit_xml if junit_xml else None

        return self.test_server(
            server_config=server_config,
            test_config=test_config,
            server_name=server_name,
            timeout=timeout,
            verbose=verbose or debug,
            format=format,
            output_file=output_file,
        )

    def _prepare_config(
        self, config: Union[str, Path, Dict[str, Any]], config_type: str
    ) -> str:
        """Prepare configuration file path or create temporary file from dict."""
        if isinstance(config, (str, Path)):
            config_path = Path(config)
            if not config_path.exists():
                raise MCPConfigurationError(
                    f"{config_type} file not found: {config_path}"
                )
            return str(config_path)

        elif isinstance(config, dict):
            # Create temporary config file
            import tempfile

            suffix = ".yaml" if config_type == "test_config" else ".json"

            with tempfile.NamedTemporaryFile(
                mode="w", suffix=suffix, delete=False
            ) as tmp_file:
                if suffix == ".yaml":
                    yaml.dump(config, tmp_file, default_flow_style=False)
                else:
                    json.dump(config, tmp_file, indent=2)
                return tmp_file.name

        else:
            raise MCPConfigurationError(
                f"Invalid {config_type} type. Expected str, Path, or dict."
            )

    def _parse_result(
        self, result: subprocess.CompletedProcess, format: str
    ) -> MCPTestResult:
        """Parse command execution result into MCPTestResult."""

        if result.returncode != 0:
            raise MCPTestExecutionError(
                f"Test execution failed with return code {result.returncode}",
                return_code=result.returncode,
                stderr=result.stderr,
            )

        # NPM package returns text output, parse it
        success = result.returncode == 0

        # Parse number of passed tests from string "📊 Results: 8/8 tests passed"
        passed_tests = 0
        total_tests = 0
        execution_time = 0.0

        for line in result.stdout.split("\n"):
            if "Results:" in line and "tests passed" in line:
                # Parse string "📊 Results: 8/8 tests passed (5.8s)"
                import re

                match = re.search(r"(\d+)/(\d+)\s+tests\s+passed.*?\(([\d.]+)s\)", line)
                if match:
                    passed_tests = int(match.group(1))
                    total_tests = int(match.group(2))
                    execution_time = float(match.group(3))
                break

        # If parsing failed, use fallback
        if total_tests == 0:
            passed_tests = 1 if success else 0
            total_tests = 1

        return MCPTestResult(
            success=success,
            passed_tests=passed_tests,
            total_tests=total_tests,
            failed_tests=[] if success else [{"error": "Test execution failed"}],
            execution_time=execution_time,
            output=result.stdout,
            error=result.stderr,
        )

    def run_evals(
        self,
        test_config: Union[str, Path, Dict[str, Any]],
        server_config: Union[str, Path, Dict[str, Any]],
        server_name: Optional[str] = None,
        timeout: int = 30000,
        verbose: bool = False,
    ) -> MCPTestResult:
        """Run LLM evaluation tests (requires ANTHROPIC_API_KEY)."""
        server_config_path = self._prepare_config(server_config, "server_config")
        test_config_path = self._prepare_config(test_config, "test_config")

        cmd = ["npx", self.npm_package_name, "evals", test_config_path]
        cmd.extend(["--server-config", server_config_path])

        if server_name:
            cmd.extend(["--server-name", server_name])
        if timeout != 30000:
            cmd.extend(["--timeout", str(timeout)])
        if verbose:
            cmd.append("--verbose")

        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=timeout / 1000.0
            )
            return self._parse_result(result, "json")
        except subprocess.TimeoutExpired:
            raise MCPTestExecutionError(f"Eval execution timed out after {timeout}ms")
        except Exception as e:
            raise MCPTestExecutionError(f"Eval execution failed: {str(e)}")

    def run_compliance_check(
        self,
        server_config: Union[str, Path, Dict[str, Any]],
        server_name: Optional[str] = None,
        categories: Optional[str] = None,
        output: Optional[str] = None,
        timeout: int = 30000,
    ) -> MCPTestResult:
        """Run MCP protocol compliance checks."""
        server_config_path = self._prepare_config(server_config, "server_config")

        cmd = ["npx", self.npm_package_name, "compliance"]
        cmd.extend(["--server-config", server_config_path])

        if server_name:
            cmd.extend(["--server-name", server_name])
        if categories:
            cmd.extend(["--categories", categories])
        if output:
            cmd.extend(["--output", output])
        if timeout != 30000:
            cmd.extend(["--timeout", str(timeout)])

        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=timeout / 1000.0
            )
            return self._parse_result(result, "json")
        except subprocess.TimeoutExpired:
            raise MCPTestExecutionError(f"Compliance check timed out after {timeout}ms")
        except Exception as e:
            raise MCPTestExecutionError(f"Compliance check failed: {str(e)}")

    def get_schema(self) -> Dict[str, Any]:
        """Display JSON schema for test configuration files."""
        cmd = ["npx", self.npm_package_name, "schema"]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise MCPTestExecutionError(
                "Failed to get schema", result.returncode, result.stderr
            )

        try:
            return json.loads(result.stdout)
        except json.JSONDecodeError:
            return {"schema": result.stdout}

    def get_documentation(self) -> str:
        """Get full documentation for MCP Server Tester."""
        cmd = ["npx", self.npm_package_name, "documentation"]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise MCPTestExecutionError(
                "Failed to get documentation", result.returncode, result.stderr
            )

        return result.stdout

    # Keep backward compatibility methods with deprecation warnings
    def list_tools(
        self,
        server_config: Union[str, Path, Dict[str, Any]],
        server_name: Optional[str] = None,
    ) -> List[Dict[str, Any]]:
        """
        DEPRECATED: Use test_server() with appropriate test configuration instead.
        This method is kept for backward compatibility.
        """
        import warnings

        warnings.warn(
            "list_tools() is deprecated. Use test_server() with tool discovery tests instead.",
            DeprecationWarning,
            stacklevel=2,
        )

        # Create a basic tool discovery test
        test_config: Dict[str, Any] = {
            "tools": {"expected_tool_list": [], "tests": []}  # Will discover all tools
        }

        try:
            result = self.test_server(server_config, test_config, server_name)
            # Extract tool names from result
            return [{"name": tool} for tool in getattr(result, "discovered_tools", [])]
        except Exception:
            return []

    def validate_config(self, config_path: Union[str, Path]) -> bool:
        """
        DEPRECATED: Use run_compliance_check() instead.
        Basic validation of server configuration.
        """
        import warnings

        warnings.warn(
            "validate_config() is deprecated. Use run_compliance_check() for thorough validation.",
            DeprecationWarning,
            stacklevel=2,
        )

        try:
            # Basic JSON validation
            with open(config_path) as f:
                json.load(f)
            return True
        except Exception:
            return False

    @staticmethod
    def create_server_config(
        servers: Dict[str, Dict[str, Any]],
        output_path: Optional[Union[str, Path]] = None,
    ) -> str:
        """Create server configuration file."""
        config = {"mcpServers": servers}

        if output_path:
            config_path = Path(output_path)
            with open(config_path, "w") as f:
                json.dump(config, f, indent=2)
            return str(config_path)
        else:
            return json.dumps(config, indent=2)

    @staticmethod
    def create_test_config(
        tests: List[Dict[str, Any]], output_path: Optional[Union[str, Path]] = None
    ) -> str:
        """Create test configuration file."""
        config = {"tools": {"tests": tests}}

        if output_path:
            config_path = Path(output_path)
            with open(config_path, "w") as f:
                yaml.dump(config, f, default_flow_style=False)
            return str(config_path)
        else:
            return yaml.dump(config, default_flow_style=False)
