#!/usr/bin/env python3
"""
Comprehensive Test Runner for Local Deep Research

This script orchestrates all test types and provides different test profiles
for various scenarios (CI/CD, development, full validation).

Usage:
    python tests/run_all_tests.py [profile]

Profiles:
    fast          - Quick health checks and unit tests (< 30s)
    standard      - Standard development testing (< 5min)
    full          - Optimized full testing, no redundant runs (< 10min)
    comprehensive - Legacy full testing, includes redundant runs (< 15min)
    ci            - CI/CD optimized tests
    unit-only     - Unit tests only, no server dependencies (< 2min)
"""

import argparse
import os
import subprocess
from subprocess import CalledProcessError
import sys
import time
from pathlib import Path
from typing import List, Tuple
from functools import cache


@cache
def find_exe(tool_name: str) -> Path:
    """
    Finds a particular command-line tool.

    Args:
        tool_name: The name of the tool.

    Returns:
        The path to the tool.

    """
    which_output = ""
    try:
        which_result = subprocess.run(
            ["/usr/bin/which", tool_name], check=True, capture_output=True
        )
        which_output = which_result.stdout.decode("utf8")
    except CalledProcessError as err:
        if err.returncode != 1:  # pragma: no cover
            # If it returns 1, it's probably because the executable does not
            # exist. Otherwise, something weird happened.
            raise err
        pass
    if not which_output:
        raise OSError(f"Could not find '{tool_name}'. Is it installed?")

    tool_path = Path(which_output.rstrip("\n"))
    return tool_path


class TestRunner:
    def __init__(self):
        self.project_root = Path(__file__).parent.parent.absolute()
        self.tests_dir = self.project_root / "tests"
        self.results: List[Tuple[str, bool, float]] = []
        self.server_process = None

    def log(self, message: str, level: str = "INFO"):
        """Log a message with timestamp."""
        timestamp = time.strftime("%H:%M:%S")
        prefix = (
            "✅" if level == "SUCCESS" else "❌" if level == "ERROR" else "📋"
        )
        print(f"[{timestamp}] {prefix} {message}")

    def run_command(
        self, cmd: List[str], name: str, cwd: str = None, timeout: int = 300
    ) -> bool:
        """Run a command and track its result."""
        self.log(f"Running {name}...")
        start_time = time.time()

        try:
            # Set environment
            env = os.environ.copy()
            env["PYTHONPATH"] = str(self.project_root)

            result = subprocess.run(
                cmd,
                cwd=cwd or self.project_root,
                env=env,
                capture_output=True,
                text=True,
                timeout=timeout,
            )

            duration = time.time() - start_time
            success = result.returncode == 0

            if success:
                self.log(
                    f"{name} completed successfully ({duration:.1f}s)",
                    "SUCCESS",
                )
            else:
                self.log(f"{name} failed ({duration:.1f}s)", "ERROR")
                print(f"\n{'=' * 60}")
                print(f"DETAILED ERROR OUTPUT FOR: {name}")
                print(f"{'=' * 60}")

                # Print detailed output for failures with full context
                if result.stdout:
                    print("STDOUT:")
                    lines = result.stdout.strip().split("\n")

                    # Show test summary/results
                    for line in lines[-15:]:  # Check last 15 lines for summary
                        if any(
                            keyword in line.lower()
                            for keyword in [
                                "failed",
                                "passed",
                                "error",
                                "collected",
                                "warnings",
                            ]
                        ):
                            print(f"  {line}")

                    # Show specific failed tests with more context
                    print("\nFAILED TESTS WITH CONTEXT:")
                    for i, line in enumerate(lines):
                        if "FAILED" in line and "::" in line:
                            print(f"  {line.strip()}")
                            # Show next few lines for error details
                            for j in range(i + 1, min(i + 10, len(lines))):
                                if lines[j].strip() and not lines[j].startswith(
                                    "===="
                                ):
                                    print(f"    {lines[j].strip()}")
                                if (
                                    "KeyError" in lines[j]
                                    or "Exception" in lines[j]
                                ):
                                    break

                    # Show full stack traces for exceptions
                    print("\nFULL STACK TRACES:")
                    in_traceback = False
                    traceback_lines = []
                    for line in lines:
                        if "Traceback (most recent call last):" in line:
                            in_traceback = True
                            traceback_lines = [line]
                        elif in_traceback:
                            traceback_lines.append(line)
                            if line.strip() and not line.startswith(
                                (" ", "\t", "File ", "  ")
                            ):
                                # End of traceback
                                for tb_line in traceback_lines:
                                    print(f"  {tb_line}")
                                print()
                                in_traceback = False
                                traceback_lines = []
                        elif any(
                            error_type in line
                            for error_type in [
                                "KeyError:",
                                "ImportError:",
                                "ModuleNotFoundError:",
                                "AttributeError:",
                                "Exception:",
                            ]
                        ):
                            print(f"  ERROR: {line.strip()}")

                    # Show import errors with context
                    print("\nIMPORT ERRORS:")
                    for i, line in enumerate(lines):
                        if (
                            "ImportError" in line
                            or "ModuleNotFoundError" in line
                        ):
                            print(f"  {line.strip()}")
                            # Show preceding line for context
                            if i > 0:
                                print(f"    Context: {lines[i - 1].strip()}")

                    # Show KeyError details specifically
                    print("\nKEYERROR DETAILS:")
                    for i, line in enumerate(lines):
                        if "KeyError" in line:
                            print(f"  {line.strip()}")
                            # Show surrounding context
                            for j in range(
                                max(0, i - 3), min(len(lines), i + 4)
                            ):
                                if j != i:
                                    print(
                                        f"    [{j - i:+d}] {lines[j].strip()}"
                                    )

                    # Show pytest short test summary if available
                    print("\nPYTEST SHORT SUMMARY:")
                    in_summary = False
                    for line in lines:
                        if "short test summary info" in line.lower():
                            in_summary = True
                        elif in_summary:
                            if line.startswith("="):
                                break
                            print(f"  {line.strip()}")

                if result.stderr:
                    print("\nSTDERR:")
                    stderr_lines = result.stderr.strip().split("\n")
                    for line in stderr_lines[-10:]:  # Last 10 lines
                        print(f"  {line}")

                print(f"{'=' * 60}\n")

            self.results.append((name, success, duration))
            return success

        except subprocess.TimeoutExpired:
            duration = time.time() - start_time
            self.log(f"{name} timed out after {timeout}s", "ERROR")
            self.results.append((name, False, duration))
            return False
        except Exception as e:
            duration = time.time() - start_time
            self.log(f"{name} failed with exception: {e}", "ERROR")
            self.results.append((name, False, duration))
            return False

    def check_server_running(self) -> bool:
        """Check if the web server is running."""
        try:
            import requests

            response = requests.get(
                "http://127.0.0.1:5000/api/v1/health", timeout=5
            )
            return response.status_code == 200
        except Exception:
            return False

    def start_server(self) -> bool:
        """Start the web server using pdm run ldr-web."""
        if self.check_server_running():
            self.log("Server already running at http://127.0.0.1:5000")
            return True

        self.log("Starting web server with 'pdm run ldr-web'...")
        try:
            import subprocess
            import time

            # Start server in background
            self.server_process = subprocess.Popen(
                ["pdm", "run", "ldr-web"],
                cwd=self.project_root,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            # Wait for server to start (max 45 seconds)
            for i in range(45):
                time.sleep(1)  # allow: unmarked-sleep
                if self.check_server_running():
                    self.log(
                        f"Server started successfully after {i + 1} seconds"
                    )
                    return True
                if self.server_process.poll() is not None:
                    # Process died
                    stdout, stderr = self.server_process.communicate()
                    self.log(
                        f"Server failed to start. Exit code: {self.server_process.returncode}",
                        "ERROR",
                    )
                    if stderr:
                        print("STDERR:", stderr[-500:])
                    return False

            self.log("Server startup timed out after 45 seconds", "ERROR")
            # Try to get output for debugging
            if self.server_process.poll() is None:
                self.server_process.terminate()
                try:
                    stdout, stderr = self.server_process.communicate(timeout=5)
                    if stderr:
                        print("STDERR after timeout:", stderr[-500:])
                except Exception:
                    pass
            return False

        except Exception as e:
            self.log(f"Failed to start server: {e}", "ERROR")
            return False

    def stop_server(self):
        """Stop the web server if we started it."""
        if hasattr(self, "server_process") and self.server_process:
            self.log("Stopping web server...")
            self.server_process.terminate()
            try:
                self.server_process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                self.server_process.kill()
                self.server_process.wait()

    def run_health_checks(self, start_server_if_needed: bool = True) -> bool:
        """Run health check tests."""
        self.log("=== HEALTH CHECKS ===")

        # Start server if needed for health checks
        if start_server_if_needed and not self.check_server_running():
            if not self.start_server():
                self.log("⚠️  Could not start server for health checks", "ERROR")
                return False

        # Try Python version first
        python_success = self.run_command(
            [sys.executable, "tests/health_check/run_quick_health_check.py"],
            "Health Check (Python)",
            timeout=120,
        )

        # Try shell version as backup
        if not python_success:
            shell_script = (
                self.tests_dir / "health_check" / "test_endpoints_health.sh"
            )
            if shell_script.exists():
                self.run_command(
                    ["bash", str(shell_script)],
                    "Health Check (Shell)",
                    timeout=120,
                )

        return python_success

    def run_unit_tests(self) -> bool:
        """Run unit and feature tests."""
        self.log("=== UNIT & FEATURE TESTS ===")

        # Add extra debugging for CI environments
        pytest_args = [
            find_exe("pdm"),
            "run",
            "pytest",
            "-v",  # Verbose output
            "tests/test_settings_manager.py",
            "tests/test_google_pse.py",
            "tests/test_wikipedia_url_security.py",
            "tests/test_search_engines_enhanced.py",
            "tests/test_utils.py",
            "tests/rate_limiting/",  # Rate limiting test suite
            "tests/retriever_integration/",  # LangChain retriever integration tests
            "tests/feature_tests/",
            "tests/fix_tests/",
            "tests/infrastructure_tests/",  # Infrastructure and architecture tests
            "tests/content_fetcher/",  # Content fetcher tests (URL classifier, HTML downloader)
            # Critical stability tests - prevent breaking changes
            "tests/database/test_encryption_constants.py",  # Encryption params MUST NOT change
            "tests/database/test_schema_stability.py",  # Table names MUST NOT change
            "tests/performance/database/test_backwards_compatibility.py",  # DB compatibility tests
            "--cov=src",
            "--cov-report=term-missing",
        ]

        # In CI, add even more debugging
        if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
            pytest_args.extend(
                [
                    "--tb=short",  # Short traceback format
                    "--durations=10",  # Show 10 slowest tests
                ]
            )
            self.log("Running with enhanced CI debugging options")

            # Log environment info for debugging
            self.log(
                f"CI Environment detected: CI={os.environ.get('CI')}, GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS')}"
            )
            self.log(f"Python path: {sys.executable}")
            self.log(f"Working directory: {os.getcwd()}")

        return self.run_command(
            pytest_args,
            "Unit & Feature Tests",
            timeout=300,
        )

    def run_integration_tests(self) -> bool:
        """Run integration tests (external dependencies)."""
        self.log("=== INTEGRATION TESTS ===")

        return self.run_command(
            [
                sys.executable,
                "-m",
                "pytest",
                "-v",
                "tests/searxng/",
                "-k",
                "not slow",
            ],
            "Integration Tests",
            timeout=120,
        )

    def run_ui_tests(self, start_server_if_needed: bool = True) -> bool:
        """Run UI tests with Puppeteer."""
        self.log("=== UI TESTS ===")

        if start_server_if_needed and not self.check_server_running():
            if not self.start_server():
                self.log(
                    "⚠️  Could not start server - skipping UI tests", "ERROR"
                )
                return False
        elif not self.check_server_running():
            self.log(
                "⚠️  Warning: Server not running at http://127.0.0.1:5000",
                "ERROR",
            )
            self.log("Please start the server with: pdm run ldr-web")
            return False

        # Check for Node.js and run UI tests
        try:
            subprocess.run(
                ["node", "--version"], capture_output=True, check=True
            )
        except (subprocess.CalledProcessError, FileNotFoundError):
            self.log("Node.js not found - skipping UI tests", "ERROR")
            return False

        # Check if we're in a headless environment (CI)
        cmd = ["node", "run_all_tests.js"]
        if os.environ.get("CI") or not os.environ.get("DISPLAY"):
            # If no display is available, use xvfb-run if available
            try:
                subprocess.run(
                    ["which", "xvfb-run"], capture_output=True, check=True
                )
                cmd = ["xvfb-run", "-a", "-s", "-screen 0 1920x1080x24"] + cmd
                self.log(
                    "Running UI tests with xvfb-run for headless environment"
                )
            except (subprocess.CalledProcessError, FileNotFoundError):
                self.log("Warning: No display available and xvfb-run not found")
                # Set headless environment variable for Puppeteer
                os.environ["PUPPETEER_HEADLESS"] = "true"

        return self.run_command(
            cmd,
            "UI Tests (Puppeteer)",
            cwd=self.tests_dir / "ui_tests",
            timeout=300,
        )

    def run_all_pytest_tests(self) -> bool:
        """Run all pytest tests with coverage (unit, integration, feature tests)."""
        self.log("=== ALL PYTEST TESTS ===")

        # Run all tests with coverage
        cmd = [
            sys.executable,
            "-m",
            "pytest",
            "--verbose",
            "--color=yes",
            "--cov=src",
            "--cov-report=term",
            "--cov-report=html:coverage_html",
            "--cov-config=.coveragerc",
        ]
        timeout = 600  # 10 minutes for all tests

        if os.environ.get("CI"):
            cmd.extend(
                [
                    "--tb=line",  # Show one line per failure for easier debugging
                    "--timeout=60",  # Per-test timeout of 60 seconds
                    # Removed -x and --maxfail to show all test results
                ]
            )
            self.log(
                "Running in CI mode with verbose output - showing all test results"
            )

        return self.run_command(cmd, "All Pytest Tests", timeout=timeout)

    def print_summary(self):
        """Print test execution summary."""
        self.log("=" * 60)
        self.log("TEST EXECUTION SUMMARY")
        self.log("=" * 60)

        total_tests = len(self.results)
        passed = sum(1 for _, success, _ in self.results if success)
        failed = total_tests - passed
        total_time = sum(duration for _, _, duration in self.results)

        for name, success, duration in self.results:
            status = "✅ PASS" if success else "❌ FAIL"
            self.log(f"{status} {name} ({duration:.1f}s)")

        self.log("-" * 60)
        self.log(
            f"📊 Total: {total_tests} | ✅ Passed: {passed} | ❌ Failed: {failed}"
        )
        self.log(f"⏱️  Total Time: {total_time:.1f}s")
        self.log(f"📈 Success Rate: {(passed / total_tests * 100):.1f}%")

        if failed == 0:
            self.log("🎉 All tests passed!", "SUCCESS")
        else:
            self.log(f"⚠️  {failed} test(s) failed", "ERROR")

        return failed == 0


def main():
    parser = argparse.ArgumentParser(
        description="Run Local Deep Research tests"
    )
    parser.add_argument(
        "profile",
        nargs="?",
        default="standard",
        choices=[
            "fast",
            "standard",
            "full",
            "comprehensive",
            "ci",
            "unit-only",
        ],
        help="Test profile to run",
    )
    parser.add_argument(
        "--no-server-start",
        action="store_true",
        help="Skip automatic server startup - assume server is already running",
    )

    args = parser.parse_args()
    runner = TestRunner()

    runner.log(f"🚀 Starting {args.profile.upper()} test profile")
    start_time = time.time()

    success = True

    try:
        if args.profile == "fast":
            # Quick tests for rapid feedback (< 30s)
            success &= runner.run_health_checks(
                start_server_if_needed=not args.no_server_start
            )
            success &= runner.run_unit_tests()

        elif args.profile == "standard":
            # Standard development testing (< 5min)
            success &= runner.run_health_checks(
                start_server_if_needed=not args.no_server_start
            )
            success &= runner.run_unit_tests()
            success &= runner.run_ui_tests(
                start_server_if_needed=not args.no_server_start
            )

        elif args.profile == "full":
            # Optimized full testing - no duplicate test runs (< 10min)
            success &= runner.run_health_checks(
                start_server_if_needed=not args.no_server_start
            )
            # Run all pytest tests (includes unit, integration, feature tests)
            success &= runner.run_all_pytest_tests()
            # UI tests require special setup so run separately
            success &= runner.run_ui_tests(
                start_server_if_needed=not args.no_server_start
            )

        elif args.profile == "comprehensive":
            # Legacy comprehensive testing - runs some tests multiple times (< 15min)
            # Kept for backwards compatibility
            success &= runner.run_health_checks(
                start_server_if_needed=not args.no_server_start
            )
            success &= runner.run_all_pytest_tests()
            success &= (
                runner.run_integration_tests()
            )  # Redundant but kept for compatibility
            success &= runner.run_ui_tests(
                start_server_if_needed=not args.no_server_start
            )

        elif args.profile == "ci":
            # CI/CD optimized tests
            # Skip health checks in CI as they require a running server
            # which is not available in the CI environment
            success &= runner.run_unit_tests()
            # Skip UI tests in CI unless explicitly configured

        elif args.profile == "unit-only":
            # Unit tests only - no server dependencies
            success &= runner.run_unit_tests()

    finally:
        # Always cleanup server if we started it
        runner.stop_server()

    total_duration = time.time() - start_time
    runner.log(f"🏁 Test suite completed in {total_duration:.1f}s")

    overall_success = runner.print_summary()

    sys.exit(0 if overall_success else 1)


if __name__ == "__main__":
    main()
