"""Test manager for orchestrating installation tests."""

import json
import tempfile
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Optional, Callable, List

from .config import TestConfig, TestLevel
from .comfy_env import get_cuda_packages, get_node_reqs
from .node_discovery import discover_nodes, discover_nodes_subprocess
from .platform import get_platform, TestPlatform, TestPaths
from ..comfyui.server import ComfyUIServer
from ..comfyui.validator import WorkflowValidator
from ..comfyui.workflow import WorkflowRunner
from ..errors import TestError, VerificationError, WorkflowValidationError


@dataclass
class TestState:
    """State persisted between CLI invocations for multi-step CI.

    This allows running test levels in separate CI steps while sharing
    the ComfyUI environment setup between them.
    """
    comfyui_dir: str
    python: str
    custom_nodes_dir: str
    venv_dir: Optional[str]
    expected_nodes: List[str]
    cuda_packages: List[str]
    platform_name: str


def save_state(state: TestState, work_dir: Path) -> None:
    """Save test state to work directory for later resumption."""
    state_file = work_dir / "state.json"
    work_dir.mkdir(parents=True, exist_ok=True)
    with open(state_file, "w") as f:
        json.dump(asdict(state), f, indent=2)


def load_state(work_dir: Path) -> TestState:
    """Load test state from work directory."""
    state_file = work_dir / "state.json"
    if not state_file.exists():
        raise TestError(
            "No state file found",
            f"Expected {state_file}. Run install level first with --work-dir."
        )
    with open(state_file) as f:
        data = json.load(f)
    return TestState(**data)


class TestResult:
    """Result of a test run.

    Attributes:
        platform: Platform name
        success: Whether the test passed
        error: Error message if failed
        details: Additional details
    """

    def __init__(
        self,
        platform: str,
        success: bool,
        error: Optional[str] = None,
        details: Optional[str] = None,
    ):
        self.platform = platform
        self.success = success
        self.error = error
        self.details = details

    def __repr__(self) -> str:
        status = "PASS" if self.success else "FAIL"
        return f"TestResult({self.platform}: {status})"


class TestManager:
    """Orchestrates installation tests across platforms.

    Args:
        config: Test configuration
        node_dir: Path to custom node directory (default: current directory)
        log_callback: Optional callback for logging

    Example:
        >>> manager = TestManager(config)
        >>> results = manager.run_all()
        >>> for result in results:
        ...     print(f"{result.platform}: {'PASS' if result.success else 'FAIL'}")
    """

    # All possible levels in order
    ALL_LEVELS = [
        TestLevel.SYNTAX, TestLevel.INSTALL, TestLevel.REGISTRATION,
        TestLevel.INSTANTIATION, TestLevel.VALIDATION, TestLevel.EXECUTION
    ]

    def __init__(
        self,
        config: TestConfig,
        node_dir: Optional[Path] = None,
        log_callback: Optional[Callable[[str], None]] = None,
    ):
        self.config = config
        self.node_dir = Path(node_dir) if node_dir else Path.cwd()
        self._log = log_callback or (lambda msg: print(msg))
        self._level_index = 0
        self._total_levels = 0

    def _log_level_start(self, level: TestLevel, in_config: bool) -> None:
        """Log the start of a test level with clear formatting."""
        self._level_index += 1
        level_name = level.value.upper()
        status = "" if in_config else " (implicit)"
        self._log(f"\n[{self._level_index}/{self._total_levels}] {level_name}{status}")
        self._log("-" * 40)

    def _log_level_skip(self, level: TestLevel) -> None:
        """Log a skipped level."""
        self._level_index += 1
        level_name = level.value.upper()
        self._log(f"\n[{self._level_index}/{self._total_levels}] {level_name}: SKIPPED")

    def _log_level_done(self, level: TestLevel, message: str = "OK") -> None:
        """Log successful completion of a level."""
        level_name = level.value.upper()
        self._log(f"[{level_name}] {message}")

    def run_all(
        self,
        dry_run: bool = False,
        level: Optional[TestLevel] = None,
    ) -> List[TestResult]:
        """Run tests on all enabled platforms.

        Args:
            dry_run: If True, only show what would be done
            level: Maximum test level to run (None = all levels + workflows)

        Returns:
            List of TestResult for each platform
        """
        results = []

        platforms = [
            ("linux", self.config.linux),
            ("windows", self.config.windows),
            ("windows_portable", self.config.windows_portable),
        ]

        for platform_name, platform_config in platforms:
            if not platform_config.enabled:
                self._log(f"Skipping {platform_name} (disabled)")
                continue

            result = self.run_platform(platform_name, dry_run, level)
            results.append(result)

        return results

    def run_platform(
        self,
        platform_name: str,
        dry_run: bool = False,
        level: Optional[TestLevel] = None,
    ) -> TestResult:
        """Run tests on a specific platform.

        Args:
            platform_name: Platform to test ('linux', 'windows', 'windows_portable')
            dry_run: If True, only show what would be done
            level: Maximum test level to run (CLI override, None = use config levels)

        Returns:
            TestResult for the platform
        """
        # Determine which levels to run
        # If CLI --level specified, filter to only levels up to that point
        requested_levels = self.config.levels
        if level:
            order = [TestLevel.SYNTAX, TestLevel.INSTALL, TestLevel.REGISTRATION,
                     TestLevel.INSTANTIATION, TestLevel.VALIDATION, TestLevel.EXECUTION]
            max_idx = order.index(level)
            requested_levels = [l for l in requested_levels if order.index(l) <= max_idx]

        # Resolve dependencies (e.g., validation needs install)
        config_levels = TestLevel.resolve_dependencies(requested_levels)

        # Calculate total levels for progress display
        self._level_index = 0
        self._total_levels = len([l for l in self.ALL_LEVELS if l in config_levels])

        self._log(f"\n{'='*60}")
        self._log(f"Testing: {platform_name}")
        self._log(f"Levels: {', '.join(l.value for l in config_levels)}")
        self._log(f"{'='*60}")

        if dry_run:
            return self._dry_run(platform_name, config_levels)

        try:
            # === SYNTAX LEVEL ===
            if TestLevel.SYNTAX not in config_levels:
                self._log_level_skip(TestLevel.SYNTAX)
            else:
                self._log_level_start(TestLevel.SYNTAX, TestLevel.SYNTAX in requested_levels)
                self._check_syntax()
                self._log_level_done(TestLevel.SYNTAX, "PASSED")

            # Check if we need install level for later levels
            needs_install = any(l in config_levels for l in [
                TestLevel.INSTALL, TestLevel.REGISTRATION,
                TestLevel.INSTANTIATION, TestLevel.VALIDATION
            ])
            run_workflows = self.config.workflow.run

            if not needs_install and not run_workflows:
                # Only syntax was requested and no workflows
                self._log(f"\n{platform_name}: PASSED")
                return TestResult(platform_name, True)

            # Get platform provider
            platform = get_platform(platform_name, self._log)
            platform_config = self.config.get_platform_config(platform_name)

            # Create temporary work directory
            with tempfile.TemporaryDirectory(prefix="comfy_test_") as work_dir:
                work_path = Path(work_dir)

                # === INSTALL LEVEL ===
                # Always run install if any later level needs it
                self._log_level_start(TestLevel.INSTALL, TestLevel.INSTALL in requested_levels)
                self._log("Setting up ComfyUI...")
                paths = platform.setup_comfyui(self.config, work_path)

                self._log("Installing custom node...")
                platform.install_node(paths, self.node_dir)

                node_reqs = get_node_reqs(self.node_dir)
                if node_reqs:
                    self._log(f"Installing {len(node_reqs)} node dependency(ies)...")
                    for name, repo in node_reqs:
                        self._log(f"  {name} from {repo}")
                        platform.install_node_from_repo(paths, repo, name)

                self._log_level_done(TestLevel.INSTALL, "PASSED")

                # Check if we need server for remaining levels
                needs_server = any(l in config_levels for l in [
                    TestLevel.REGISTRATION, TestLevel.INSTANTIATION,
                    TestLevel.VALIDATION, TestLevel.EXECUTION
                ])

                if not needs_server:
                    self._log(f"\n{platform_name}: PASSED")
                    return TestResult(platform_name, True)

                # Get CUDA packages to mock from comfy-env.toml
                cuda_packages = get_cuda_packages(self.node_dir)
                if cuda_packages:
                    self._log(f"Found CUDA packages to mock: {', '.join(cuda_packages)}")

                # Discover nodes from NODE_CLASS_MAPPINGS before starting server
                # Use subprocess to run in test venv where dependencies are installed
                expected_nodes = discover_nodes_subprocess(
                    paths.custom_nodes_dir / self.node_dir.name,
                    paths.python,
                    cuda_packages,
                    paths.comfyui_dir,
                )
                self._log(f"Discovered {len(expected_nodes)} node(s): {', '.join(expected_nodes)}")

                # === Start server for remaining levels ===
                self._log("\nStarting ComfyUI server...")
                with ComfyUIServer(
                    platform, paths, self.config,
                    cuda_mock_packages=cuda_packages,
                    log_callback=self._log,
                ) as server:
                    api = server.get_api()

                    # === REGISTRATION LEVEL ===
                    if TestLevel.REGISTRATION not in config_levels:
                        self._log_level_skip(TestLevel.REGISTRATION)
                    else:
                        self._log_level_start(TestLevel.REGISTRATION, TestLevel.REGISTRATION in requested_levels)
                        self._log("Verifying node registration...")
                        api.verify_nodes(expected_nodes)
                        self._log(f"All {len(expected_nodes)} expected nodes found!")
                        self._log_level_done(TestLevel.REGISTRATION, "PASSED")

                    # === INSTANTIATION LEVEL ===
                    if TestLevel.INSTANTIATION not in config_levels:
                        self._log_level_skip(TestLevel.INSTANTIATION)
                    else:
                        self._log_level_start(TestLevel.INSTANTIATION, TestLevel.INSTANTIATION in requested_levels)
                        self._log("Testing node constructors...")
                        self._test_instantiation(platform, paths, expected_nodes, cuda_packages)
                        self._log(f"All {len(expected_nodes)} node(s) instantiated successfully!")
                        self._log_level_done(TestLevel.INSTANTIATION, "PASSED")

                    # === VALIDATION LEVEL ===
                    if TestLevel.VALIDATION not in config_levels:
                        self._log_level_skip(TestLevel.VALIDATION)
                    else:
                        self._log_level_start(TestLevel.VALIDATION, TestLevel.VALIDATION in requested_levels)
                        # Always auto-discover ALL workflows in workflows/ folder
                        workflows_dir = self.node_dir / "workflows"
                        workflow_files = sorted(workflows_dir.glob("*.json")) if workflows_dir.exists() else []

                        if workflow_files:
                            self._log(f"Validating {len(workflow_files)} workflow(s)...")
                            object_info = api.get_object_info()
                            validator = WorkflowValidator(
                                object_info,
                                cuda_packages=cuda_packages,
                                cuda_node_types=set(),
                            )

                            all_errors = []
                            for workflow_path in workflow_files:
                                self._log(f"  {workflow_path.name}:")
                                validation_result = validator.validate_file(workflow_path)

                                if not validation_result.is_valid:
                                    for err in validation_result.errors:
                                        self._log(f"    [ERROR] {err}")
                                        all_errors.append((workflow_path.name, err))
                                else:
                                    self._log(f"    Schema: OK")
                                    self._log(f"    Graph: OK")
                                    self._log(f"    Introspection: OK")

                                # Try partial execution of non-CUDA prefix
                                if validation_result.executable_nodes:
                                    with open(workflow_path) as f:
                                        workflow = json.load(f)
                                    exec_result = validator.execute_prefix(workflow, api, timeout=self.config.workflow.timeout)

                                    if exec_result.executed_nodes:
                                        self._log(f"    Execution: {len(exec_result.executed_nodes)} nodes executed")
                                    else:
                                        self._log(f"    Execution: OK (no non-CUDA nodes)")
                                    if exec_result.execution_errors:
                                        for node_id, error in exec_result.execution_errors.items():
                                            self._log(f"      [WARN] Node {node_id}: {error}")
                                else:
                                    self._log(f"    Execution: Skipped (all nodes require CUDA)")

                            if all_errors:
                                raise WorkflowValidationError(
                                    f"Workflow validation failed ({len(all_errors)} error(s))",
                                    [err for _, err in all_errors]
                                )
                            self._log(f"All {len(workflow_files)} workflow(s) validated!")
                            self._log_level_done(TestLevel.VALIDATION, "PASSED")
                        else:
                            self._log("No workflows configured")
                            self._log_level_done(TestLevel.VALIDATION, "PASSED (no workflows)")

                    # === EXECUTION LEVEL ===
                    if TestLevel.EXECUTION not in config_levels:
                        self._log_level_skip(TestLevel.EXECUTION)
                    elif not self.config.workflow.run:
                        self._log_level_start(TestLevel.EXECUTION, TestLevel.EXECUTION in requested_levels)
                        self._log("No workflows configured for execution")
                        self._log_level_done(TestLevel.EXECUTION, "PASSED (no workflows)")
                    elif platform_config.skip_workflow:
                        self._log_level_start(TestLevel.EXECUTION, TestLevel.EXECUTION in requested_levels)
                        self._log("Skipped per platform config")
                        self._log_level_done(TestLevel.EXECUTION, "SKIPPED")
                    else:
                        self._log_level_start(TestLevel.EXECUTION, TestLevel.EXECUTION in requested_levels)
                        self._log(f"Running {len(self.config.workflow.run)} workflow(s)...")
                        runner = WorkflowRunner(api, self._log)
                        for workflow_file in self.config.workflow.run:
                            self._log(f"  Running: {workflow_file.name}")
                            result = runner.run_workflow(
                                workflow_file,
                                timeout=self.config.workflow.timeout,
                            )
                            self._log(f"    Status: {result['status']}")
                        self._log_level_done(TestLevel.EXECUTION, "PASSED")

            self._log(f"\n{platform_name}: PASSED")
            return TestResult(platform_name, True)

        except TestError as e:
            self._log(f"\n{platform_name}: FAILED")
            self._log(f"Error: {e.message}")
            if e.details:
                self._log(f"Details: {e.details}")
            return TestResult(platform_name, False, str(e.message), e.details)

        except Exception as e:
            self._log(f"\n{platform_name}: FAILED (unexpected error)")
            self._log(f"Error: {e}")
            return TestResult(platform_name, False, str(e))

    def _dry_run(self, platform_name: str, levels: List[TestLevel]) -> TestResult:
        """Show what would be done without doing it."""
        self._log("\n[DRY RUN] Would execute the following levels:\n")

        level_num = 0
        total = len([l for l in self.ALL_LEVELS if l in levels])

        for test_level in self.ALL_LEVELS:
            level_name = test_level.value.upper()
            if test_level in levels:
                level_num += 1
                self._log(f"[{level_num}/{total}] {level_name}")
                self._log("-" * 40)

                if test_level == TestLevel.SYNTAX:
                    self._log("  Check pyproject.toml vs requirements.txt")
                elif test_level == TestLevel.INSTALL:
                    self._log(f"  Setup ComfyUI ({self.config.comfyui_version})")
                    self._log(f"  Install node: {self.node_dir.name}")
                    self._log("  Install node dependencies (from comfy-env.toml)")
                elif test_level == TestLevel.REGISTRATION:
                    self._log("  Verify nodes in object_info")
                elif test_level == TestLevel.INSTANTIATION:
                    self._log("  Test node constructors")
                elif test_level == TestLevel.VALIDATION:
                    self._log("  Validate workflows (schema + graph + types)")
                elif test_level == TestLevel.EXECUTION:
                    if self.config.workflow.run:
                        self._log(f"  Run {len(self.config.workflow.run)} workflow(s):")
                        for wf in self.config.workflow.run:
                            self._log(f"    - {wf}")
                    else:
                        self._log("  No workflows configured for execution")
                self._log("")
            else:
                self._log(f"[ ] {level_name}: SKIPPED\n")

        return TestResult(platform_name, True, details="Dry run")

    def _check_syntax(self) -> None:
        """Check project structure - pyproject.toml vs requirements.txt.

        Raises:
            TestError: If neither pyproject.toml nor requirements.txt exists
        """
        pyproject = self.node_dir / "pyproject.toml"
        requirements = self.node_dir / "requirements.txt"

        has_pyproject = pyproject.exists()
        has_requirements = requirements.exists()

        if has_pyproject:
            self._log("Found pyproject.toml (modern format)")
        if has_requirements:
            self._log("Found requirements.txt (legacy format)")

        if has_pyproject and has_requirements:
            self._log("WARNING: Both pyproject.toml and requirements.txt exist")
            self._log("Consider migrating fully to pyproject.toml")
        elif not has_pyproject and not has_requirements:
            raise TestError(
                "No dependency file found",
                "Expected pyproject.toml or requirements.txt in node directory"
            )
        elif has_requirements and not has_pyproject:
            self._log("WARNING: Consider migrating to pyproject.toml")

        # Check for problematic unicode characters in Python files
        self._check_unicode_characters()

    def _check_unicode_characters(self) -> None:
        """Check Python files for problematic unicode characters.

        Scans all .py files in the node directory for characters that can
        cause issues on Windows, such as curly quotes (often copy-pasted
        from documentation).

        Raises:
            TestError: If problematic characters are found
        """
        # Problematic characters and their safe replacements
        problematic_chars = {
            '\u2018': "'",  # Left single quote
            '\u2019': "'",  # Right single quote
            '\u201c': '"',  # Left double quote
            '\u201d': '"',  # Right double quote
            '\u2013': '-',  # En dash
            '\u2014': '-',  # Em dash
            '\u2026': '...',  # Ellipsis
        }

        issues = []

        for py_file in self.node_dir.rglob("*.py"):
            # Skip common non-source directories
            rel_path = py_file.relative_to(self.node_dir)
            parts = rel_path.parts
            skip_dirs = {'.git', '__pycache__', '.venv', 'venv', 'node_modules', 'site-packages', 'lib', 'Lib'}
            if any(p in skip_dirs or p.startswith('_env_') or p.startswith('.') for p in parts):
                continue

            try:
                content = py_file.read_text(encoding='utf-8')
            except UnicodeDecodeError as e:
                issues.append(f"{rel_path}: Failed to decode as UTF-8: {e}")
                continue

            file_issues = []
            for line_num, line in enumerate(content.splitlines(), 1):
                for char, replacement in problematic_chars.items():
                    if char in line:
                        col = line.index(char) + 1
                        char_name = {
                            '\u2018': 'left single quote',
                            '\u2019': 'right single quote',
                            '\u201c': 'left double quote',
                            '\u201d': 'right double quote',
                            '\u2013': 'en dash',
                            '\u2014': 'em dash',
                            '\u2026': 'ellipsis',
                        }.get(char, f'U+{ord(char):04X}')
                        file_issues.append(
                            f"  Line {line_num}, col {col}: {char_name} ({repr(char)}) - use {repr(replacement)}"
                        )

            if file_issues:
                issues.append(f"{rel_path}:\n" + "\n".join(file_issues))

        if issues:
            raise TestError(
                "Problematic unicode characters found in Python files",
                "These characters can cause issues on Windows:\n\n" + "\n\n".join(issues)
            )

        self._log("Unicode check: OK (no problematic characters found)")

    def _get_workflow_files(self) -> List[Path]:
        """Get workflow files configured for execution.

        Note: Validation always auto-discovers from workflows/ directory.
        This method returns files configured in config.workflow.run.

        Deprecated: Use config.workflow.run directly instead.
        """
        return self.config.workflow.run

    def _test_instantiation(
        self,
        platform,
        paths,
        expected_nodes: List[str],
        cuda_packages: List[str],
    ) -> None:
        """Test that all node constructors can be called without errors.

        This runs a subprocess in the test venv that imports NODE_CLASS_MAPPINGS
        and calls each node's constructor.

        Args:
            platform: Platform provider
            paths: Test paths
            expected_nodes: List of expected node names
            cuda_packages: CUDA packages to mock

        Raises:
            TestError: If any node fails to instantiate
        """
        # Build the test script
        script = '''
import sys
import json

# Mock CUDA packages if needed
cuda_packages = {cuda_packages_json}
for pkg in cuda_packages:
    if pkg not in sys.modules:
        import types
        sys.modules[pkg] = types.ModuleType(pkg)

# Import ComfyUI's folder_paths to set up paths
import folder_paths

# Find and import the node module
import importlib.util
from pathlib import Path

node_dir = Path("{node_dir}")
init_file = node_dir / "__init__.py"

if not init_file.exists():
    print(json.dumps({{"success": False, "error": "No __init__.py found"}}))
    sys.exit(1)

spec = importlib.util.spec_from_file_location("test_node", init_file)
module = importlib.util.module_from_spec(spec)
sys.modules["test_node"] = module
spec.loader.exec_module(module)

# Get NODE_CLASS_MAPPINGS
mappings = getattr(module, "NODE_CLASS_MAPPINGS", {{}})

errors = []
instantiated = []

for name, cls in mappings.items():
    try:
        instance = cls()
        instantiated.append(name)
    except Exception as e:
        errors.append({{"node": name, "error": str(e)}})

result = {{
    "success": len(errors) == 0,
    "instantiated": instantiated,
    "errors": errors,
}}
print(json.dumps(result))
'''.format(
            node_dir=str(paths.custom_nodes_dir / self.node_dir.name).replace("\\", "/"),
            cuda_packages_json=json.dumps(cuda_packages),
        )

        # Run the script in the test venv
        import subprocess

        result = subprocess.run(
            [str(paths.python), "-c", script],
            cwd=str(paths.comfyui_dir),
            capture_output=True,
            text=True,
            timeout=60,
        )

        if result.returncode != 0:
            raise TestError(
                "Instantiation test failed",
                f"stdout: {result.stdout}\nstderr: {result.stderr}"
            )

        try:
            # Extract JSON from stdout (may have log messages before it)
            stdout = result.stdout.strip()
            json_line = None
            for line in stdout.splitlines():
                line = line.strip()
                if line.startswith("{"):
                    json_line = line
            if json_line is None:
                raise json.JSONDecodeError("No JSON found in output", stdout, 0)
            data = json.loads(json_line)
        except json.JSONDecodeError:
            raise TestError(
                "Instantiation test returned invalid JSON",
                f"stdout: {result.stdout}\nstderr: {result.stderr}"
            )

        if not data.get("success"):
            error_details = "\n".join(
                f"  - {e['node']}: {e['error']}" for e in data.get("errors", [])
            )
            raise TestError(
                f"Node instantiation failed for {len(data.get('errors', []))} node(s)",
                error_details
            )

    def verify_only(self, platform_name: Optional[str] = None) -> List[TestResult]:
        """Verify node registration without running workflows.

        Args:
            platform_name: Specific platform, or None for current platform

        Returns:
            List of TestResult

        Note:
            This is equivalent to running with level=TestLevel.REGISTRATION
        """
        if platform_name is None:
            import sys
            if sys.platform == "linux":
                platform_name = "linux"
            elif sys.platform == "win32":
                platform_name = "windows"
            else:
                raise TestError(f"Unsupported platform: {sys.platform}")

        result = self.run_platform(platform_name, level=TestLevel.REGISTRATION)
        return [result]

    def _resolve_workflow_path(self, workflow_file: str) -> Path:
        """Resolve workflow file path relative to node directory.

        Args:
            workflow_file: Workflow filename or relative path

        Returns:
            Absolute Path to workflow file
        """
        workflow_path = Path(workflow_file)
        if not workflow_path.is_absolute():
            workflow_path = self.node_dir / workflow_file
        return workflow_path

    def run_single_level(
        self,
        platform_name: str,
        level: TestLevel,
        work_dir: Optional[Path] = None,
        skip_setup: bool = False,
    ) -> TestResult:
        """Run a single test level (for multi-step CI).

        This method is designed for CI workflows where each test level
        runs as a separate step. State is persisted between steps via
        the work_dir.

        Args:
            platform_name: Platform to test ('linux', 'windows', 'windows_portable')
            level: Specific level to run
            work_dir: Persistent directory for state (required for install and later levels)
            skip_setup: If True, load state from work_dir instead of running install

        Returns:
            TestResult for this level

        Example:
            # Step 1: Run syntax (no setup needed)
            manager.run_single_level('linux', TestLevel.SYNTAX)

            # Step 2: Run install (saves state)
            manager.run_single_level('linux', TestLevel.INSTALL, work_dir=Path('/tmp/ct'))

            # Step 3: Run registration (loads state)
            manager.run_single_level('linux', TestLevel.REGISTRATION,
                                     work_dir=Path('/tmp/ct'), skip_setup=True)
        """
        # Check if level is in config
        if level not in self.config.levels:
            self._log(f"[{level.value.upper()}] SKIPPED (not in config)")
            return TestResult(platform_name, True, details="Skipped - not in config")

        self._log(f"\n{'='*60}")
        self._log(f"Testing: {platform_name}")
        self._log(f"Level: {level.value}")
        self._log(f"{'='*60}")

        try:
            # === SYNTAX LEVEL ===
            if level == TestLevel.SYNTAX:
                self._log(f"\n[1/1] {level.value.upper()}")
                self._log("-" * 40)
                self._check_syntax()
                self._log(f"[{level.value.upper()}] PASSED")
                return TestResult(platform_name, True)

            # === INSTALL LEVEL ===
            if level == TestLevel.INSTALL:
                if not work_dir:
                    raise TestError(
                        "work_dir required for install level",
                        "Use --work-dir to specify a persistent directory"
                    )

                self._log(f"\n[1/1] {level.value.upper()}")
                self._log("-" * 40)

                platform = get_platform(platform_name, self._log)
                work_dir.mkdir(parents=True, exist_ok=True)

                self._log("Setting up ComfyUI...")
                paths = platform.setup_comfyui(self.config, work_dir)

                self._log("Installing custom node...")
                platform.install_node(paths, self.node_dir)

                node_reqs = get_node_reqs(self.node_dir)
                if node_reqs:
                    self._log(f"Installing {len(node_reqs)} node dependency(ies)...")
                    for name, repo in node_reqs:
                        self._log(f"  {name} from {repo}")
                        platform.install_node_from_repo(paths, repo, name)

                # Discover nodes and get CUDA packages for state
                cuda_packages = get_cuda_packages(self.node_dir)
                if cuda_packages:
                    self._log(f"Found CUDA packages to mock: {', '.join(cuda_packages)}")

                expected_nodes = discover_nodes_subprocess(
                    paths.custom_nodes_dir / self.node_dir.name,
                    paths.python,
                    cuda_packages,
                    paths.comfyui_dir,
                )
                self._log(f"Discovered {len(expected_nodes)} node(s): {', '.join(expected_nodes)}")

                # Save state for later levels
                state = TestState(
                    comfyui_dir=str(paths.comfyui_dir),
                    python=str(paths.python),
                    custom_nodes_dir=str(paths.custom_nodes_dir),
                    venv_dir=str(paths.venv_dir) if paths.venv_dir else None,
                    expected_nodes=list(expected_nodes),
                    cuda_packages=cuda_packages,
                    platform_name=platform_name,
                )
                save_state(state, work_dir)
                self._log(f"State saved to {work_dir / 'state.json'}")

                self._log(f"[{level.value.upper()}] PASSED")
                return TestResult(platform_name, True)

            # === LEVELS REQUIRING SERVER (registration, instantiation, validation, execution) ===
            if not work_dir:
                raise TestError(
                    "work_dir required",
                    "Use --work-dir to specify the directory containing state.json"
                )

            if not skip_setup:
                raise TestError(
                    "skip_setup required for levels after install",
                    "Use --skip-setup to load state from work_dir"
                )

            # Load state from previous install
            state = load_state(work_dir)

            # Reconstruct paths from state
            paths = TestPaths(
                work_dir=work_dir,
                comfyui_dir=Path(state.comfyui_dir),
                python=Path(state.python),
                custom_nodes_dir=Path(state.custom_nodes_dir),
                venv_dir=Path(state.venv_dir) if state.venv_dir else None,
            )

            platform = get_platform(platform_name, self._log)
            platform_config = self.config.get_platform_config(platform_name)

            self._log(f"\n[1/1] {level.value.upper()}")
            self._log("-" * 40)

            # Start server for this level
            self._log("Starting ComfyUI server...")
            with ComfyUIServer(
                platform, paths, self.config,
                cuda_mock_packages=state.cuda_packages,
                log_callback=self._log,
            ) as server:
                api = server.get_api()

                if level == TestLevel.REGISTRATION:
                    self._log("Verifying node registration...")
                    api.verify_nodes(state.expected_nodes)
                    self._log(f"All {len(state.expected_nodes)} expected nodes found!")

                elif level == TestLevel.INSTANTIATION:
                    self._log("Testing node constructors...")
                    self._test_instantiation(platform, paths, state.expected_nodes, state.cuda_packages)
                    self._log(f"All {len(state.expected_nodes)} node(s) instantiated successfully!")

                elif level == TestLevel.VALIDATION:
                    workflows_dir = self.node_dir / "workflows"
                    workflow_files = sorted(workflows_dir.glob("*.json")) if workflows_dir.exists() else []

                    if workflow_files:
                        self._log(f"Validating {len(workflow_files)} workflow(s)...")
                        object_info = api.get_object_info()
                        validator = WorkflowValidator(
                            object_info,
                            cuda_packages=state.cuda_packages,
                            cuda_node_types=set(),
                        )

                        all_errors = []
                        for workflow_path in workflow_files:
                            self._log(f"  {workflow_path.name}:")
                            validation_result = validator.validate_file(workflow_path)

                            if not validation_result.is_valid:
                                for err in validation_result.errors:
                                    self._log(f"    [ERROR] {err}")
                                    all_errors.append((workflow_path.name, err))
                            else:
                                self._log(f"    Schema: OK")
                                self._log(f"    Graph: OK")
                                self._log(f"    Introspection: OK")

                            # Try partial execution of non-CUDA prefix
                            if validation_result.executable_nodes:
                                with open(workflow_path) as f:
                                    workflow = json.load(f)
                                exec_result = validator.execute_prefix(workflow, api, timeout=self.config.workflow.timeout)

                                if exec_result.executed_nodes:
                                    self._log(f"    Execution: {len(exec_result.executed_nodes)} nodes executed")
                                else:
                                    self._log(f"    Execution: OK (no non-CUDA nodes)")
                                if exec_result.execution_errors:
                                    for node_id, error in exec_result.execution_errors.items():
                                        self._log(f"      [WARN] Node {node_id}: {error}")
                            else:
                                self._log(f"    Execution: Skipped (all nodes require CUDA)")

                        if all_errors:
                            raise WorkflowValidationError(
                                f"Workflow validation failed ({len(all_errors)} error(s))",
                                [err for _, err in all_errors]
                            )
                        self._log(f"All {len(workflow_files)} workflow(s) validated!")
                    else:
                        self._log("No workflows configured")

                elif level == TestLevel.EXECUTION:
                    if not self.config.workflow.run:
                        self._log("No workflows configured for execution")
                    elif platform_config.skip_workflow:
                        self._log("Skipped per platform config")
                    else:
                        self._log(f"Running {len(self.config.workflow.run)} workflow(s)...")
                        runner = WorkflowRunner(api, self._log)
                        for workflow_file in self.config.workflow.run:
                            self._log(f"  Running: {workflow_file.name}")
                            result = runner.run_workflow(
                                workflow_file,
                                timeout=self.config.workflow.timeout,
                            )
                            self._log(f"    Status: {result['status']}")

            self._log(f"[{level.value.upper()}] PASSED")
            return TestResult(platform_name, True)

        except TestError as e:
            self._log(f"\n{platform_name}: FAILED")
            self._log(f"Error: {e.message}")
            if e.details:
                self._log(f"Details: {e.details}")
            return TestResult(platform_name, False, str(e.message), e.details)

        except Exception as e:
            self._log(f"\n{platform_name}: FAILED (unexpected error)")
            self._log(f"Error: {e}")
            return TestResult(platform_name, False, str(e))
