"""Test runner applet for Winston sandbox test framework.

This applet provides a GUI for running sandbox test scripts and viewing results.
TesterDialog extends ExplorerDialog to browse sandbox/examples directory and
execute selected test scripts.

Key Features:
    - Opens winston/sandbox/examples directory on launch
    - Executes Python test scripts on Enter/Return
    - Displays test results (speech, output, exceptions) in ViewDialog
    - Shows speech log, stdout, and exceptions in formatted view
    - Uses TesterElement (not FileElement) to override Return key behavior

Architecture:
    - TesterElement: Custom element for test files that executes test on Return
    - TesterDialog: Explorer dialog that yields TesterElement for .py files
    - execute_test(): Method that runs test and displays results in ViewDialog

Usage:
    Launch tester from root launcher. Navigate to test file and press Return
    to execute. Results appear in ViewDialog for review.
"""

from typing import Any, Generator
import os
import sys
from pathlib import Path
from ..key_handler import KeyHandler
from .explorer import ExplorerDialog, FileOrFolderElement, FolderElement, resolved_names
from .editor import ViewDialog, Source
import datetime


class TesterElement(FileOrFolderElement):
    """Element for test files that executes test on Return key.

    Replaces FileElement's Return behavior (open file in editor) with
    test execution behavior (run test and show results).
    """

    @KeyHandler.hotkey("Return")
    def open(self, modified_name: str) -> None:
        """Execute this test file and display results."""
        self.dialog.execute_test(self.name)  # type: ignore[attr-defined]


class TesterDialog(ExplorerDialog):
    """Test runner dialog for executing sandbox test scripts.

    Extends ExplorerDialog to provide test execution and result viewing.
    Opens sandbox/examples directory and executes selected test scripts.
    Yields TesterElement instead of FileElement to override Return key behavior.
    Only displays .py test files (not .json files).
    """

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize TesterDialog opening sandbox/examples directory."""
        # Determine sandbox/examples path relative to winston package
        winston_dir = Path(__file__).parent.parent
        sandbox_examples = winston_dir.parent / 'sandbox' / 'examples'

        # Debug: Print path information
        print(f"TesterDialog __init__: winston_dir = {winston_dir}")
        print(f"TesterDialog __init__: sandbox_examples = {sandbox_examples}")
        print(f"TesterDialog __init__: sandbox_examples exists? {sandbox_examples.exists()}")
        print(f"TesterDialog __init__: 'filler' in kwargs? {'filler' in kwargs}")

        # Set filler to sandbox examples directory if not already provided
        if 'filler' not in kwargs:
            kwargs['filler'] = str(sandbox_examples)
            print(f"TesterDialog __init__: Set filler to {kwargs['filler']}")

        super().__init__(*args, **kwargs)

    def generate_content(self, filler: str, element_type: type[FileOrFolderElement] | None = None) -> Generator[FileOrFolderElement, None, None]:  # type: ignore[override]
        """Generate content with TesterElement for .py files only (not .json files)."""
        self.full_name = filler if filler else os.getcwd()
        self.name = os.path.basename(self.full_name) + ' folder'

        # Yield folders first, then .py test files only
        for e_type, condition in [(FolderElement, True), (TesterElement, False)]:
            for path, f in resolved_names(self.full_name):
                is_dir = os.path.isdir(path)
                # For files, only show .py files
                if is_dir == condition:
                    if not is_dir and not f.endswith('.py'):
                        continue  # Skip non-.py files
                    try:
                        modified_time = datetime.datetime.fromtimestamp(os.path.getmtime(path)).strftime('%d/%m/%y %H:%M')
                    except:
                        modified_time = 'not available'
                    try:
                        size_bytes = os.path.getsize(path)
                        size_display: float | int = size_bytes
                        units = ' bytes'
                        if size_display >= 1000:
                            size_display, units = round(size_display / 1000, 1), 'K bytes'
                        if size_display >= 1000:
                            size_display, units = round(size_display / 1000, 1), 'M bytes'
                        size = str(size_display) + units
                    except:
                        size = 'not available'
                    yield e_type(self, path, f, values=[f, modified_time, size])

    def execute_test(self, file_path: str) -> None:
        """Execute a test file and display results in ViewDialog.

        Args:
            file_path: Full path to the test file to execute
        """
        # Only run .py files
        if not file_path.endswith('.py'):
            self.say_strings('not a Python test file')
            return

        # Execute the test script
        self.say_strings(f'running {os.path.basename(file_path)}')

        try:
            import subprocess

            # Determine repository root (parent of winston package directory)
            winston_dir = Path(__file__).parent.parent
            repo_root = winston_dir.parent

            # Set environment to include repo root in PYTHONPATH
            env = os.environ.copy()
            pythonpath = str(repo_root)
            if 'PYTHONPATH' in env:
                pythonpath = f"{repo_root}{os.pathsep}{env['PYTHONPATH']}"
            env['PYTHONPATH'] = pythonpath

            # Run the test script and capture output
            # Use repo root as cwd so sandbox package is importable
            result = subprocess.run(
                [sys.executable, file_path],
                capture_output=True,
                text=True,
                timeout=30,
                cwd=str(repo_root),
                env=env
            )

            # Build results text
            lines = []
            lines.append(f"=== Test Results: {os.path.basename(file_path)} ===")
            lines.append("")

            # Add stdout
            if result.stdout:
                lines.append("=== Standard Output ===")
                lines.append("")
                lines.extend(result.stdout.splitlines())
                lines.append("")

            # Add stderr
            if result.stderr:
                lines.append("=== Standard Error ===")
                lines.append("")
                lines.extend(result.stderr.splitlines())
                lines.append("")

            # Add return code
            lines.append(f"=== Exit Code: {result.returncode} ===")

            # Check for speech log
            speech_log_path = Path(file_path).parent.parent / 'speech.log'
            if speech_log_path.exists():
                lines.append("")
                lines.append("=== Speech Log ===")
                lines.append("")
                try:
                    with open(speech_log_path, 'r', encoding='utf-8') as f:
                        # Show last 100 lines of speech log
                        speech_lines = f.readlines()
                        lines.extend([line.rstrip('\n') for line in speech_lines[-100:]])
                except Exception as e:
                    lines.append(f"Error reading speech log: {e}")

            # Display results in ViewDialog
            ViewDialog(self, name_seed=os.path.basename(file_path), filler=lines).run()

        except subprocess.TimeoutExpired:
            self.say_strings('test timed out after 30 seconds')
        except Exception as e:
            error_lines = [
                f"=== Error Running Test ===",
                "",
                f"Exception: {type(e).__name__}",
                f"Message: {str(e)}",
                "",
                "See console for full traceback"
            ]
            import traceback
            print(traceback.format_exc())

            # Display error in ViewDialog
            ViewDialog(self, name_seed=os.path.basename(file_path), filler=error_lines).run()