"""Pytest plugin for AutoDebug exception capture and flaky test quarantine.

This plugin hooks into pytest's test execution to:
1. Capture test failures (AssertionError) and errors
2. Skip quarantined flaky tests
3. Provide full stack traces with source context

Usage:
    pytest --autodebug  # Enable AutoDebug capture
    pytest --skip-quarantined  # Skip tests in .flaky-quarantine
    # Or set AUTODEBUG_RUN_ID environment variable
"""

from __future__ import annotations

import json
import os
import sys
import traceback
from pathlib import Path
from typing import Any, Dict, List, Optional, Set

import pytest
from _pytest.reports import TestReport
from _pytest.runner import CallInfo


# Avoid dual-plugin conflict: track whether the probe plugin is active so
# that autodebug/pytest_plugin.py (the main Hikaflow plugin) can detect it
# and avoid registering duplicate hooks.  This flag is checked by the
# other_plugin before registering its own capture hooks.
_hikaflow_probe_active = False

QUARANTINE_FILE = ".flaky-quarantine"


def _is_xdist_worker(config) -> bool:
    """Detect if running as a pytest-xdist worker.

    When pytest-xdist is used, worker processes have a ``workerinput``
    attribute on the config object.  The probe should only initialize
    in the controller process to avoid duplicate recordings.
    """
    return hasattr(config, "workerinput")


def _load_quarantined_tests(path: Optional[Path] = None) -> Set[str]:
    """Load quarantined test nodeids from file.

    Args:
        path: Path to quarantine file. If None, searches current directory.

    Returns:
        Set of quarantined test nodeids
    """
    if path is None:
        path = Path.cwd() / QUARANTINE_FILE

    if not path.exists():
        return set()

    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        return set(data.get("tests", {}).keys())
    except (json.JSONDecodeError, KeyError):
        return set()


def _format_stack_frames(tb) -> List[Dict[str, Any]]:
    """Format traceback into structured frames."""
    frames = []
    for frame in traceback.extract_tb(tb):
        frame_data = {
            "file": frame.filename,
            "line": frame.lineno,
            "function": frame.name,
        }
        if frame.line:
            frame_data["code"] = frame.line
        frames.append(frame_data)
    return frames


def _get_source_context(filename: str, lineno: int, context_lines: int = 5) -> Optional[Dict[str, Any]]:
    """Get source code context around the failure line."""
    try:
        with open(filename, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()

        start = max(0, lineno - context_lines - 1)
        end = min(len(lines), lineno + context_lines)

        return {
            "start_line": start + 1,
            "end_line": end,
            "lines": [line.rstrip() for line in lines[start:end]],
            "failure_line": lineno,
        }
    except Exception:
        return None


class AutoDebugPlugin:
    """Pytest plugin that captures test failures for AutoDebug."""

    def __init__(self):
        self._recorder = None
        self._enabled = False

    def _get_recorder(self):
        """Lazy-load recorder to avoid import issues."""
        if self._recorder is None:
            try:
                from autodebug_probe.recorder import get_recorder
                self._recorder = get_recorder()
            except Exception as e:
                print(f"[autodebug] Warning: Could not initialize recorder: {e}", file=sys.stderr)
                return None
        return self._recorder

    @pytest.hookimpl(tryfirst=True)
    def pytest_configure(self, config):
        """Check if AutoDebug is enabled."""
        # Enable if --autodebug flag or AUTODEBUG_RUN_ID is set
        self._enabled = (
            config.getoption("--autodebug", default=False) or
            os.environ.get("AUTODEBUG_RUN_ID") is not None
        )
        if self._enabled:
            print("[autodebug] Exception capture enabled", file=sys.stderr)

    @pytest.hookimpl(hookwrapper=True, tryfirst=True)
    def pytest_runtest_makereport(self, item, call: CallInfo):
        """Capture test failures and errors."""
        outcome = yield
        report: TestReport = outcome.get_result()

        if not self._enabled:
            return

        # Only capture failures and errors (not passes or skips)
        if report.when == "call" and report.failed:
            self._record_failure(item, call, report)

    def _record_failure(self, item, call: CallInfo, report: TestReport):
        """Record a test failure to the AutoDebug events stream."""
        recorder = self._get_recorder()
        if recorder is None:
            return

        exc_info = call.excinfo
        if exc_info is None:
            return

        # Extract exception details
        exc_type = exc_info.type
        exc_value = exc_info.value
        exc_tb = exc_info.tb

        # Get the exception type name
        exc_type_name = getattr(exc_type, "__name__", str(exc_type))

        # Format the exception message
        exc_message = str(exc_value)

        # Build stack trace
        stack_frames = _format_stack_frames(exc_tb)

        # Get source context for the failure location
        source_context = None
        if stack_frames:
            last_frame = stack_frames[-1]
            source_context = _get_source_context(
                last_frame["file"],
                last_frame["line"]
            )

        # Build the exception record
        payload = {
            "type": "EXCEPTION",
            "exc_type": exc_type_name,
            "message": exc_message,
            "stack": stack_frames,
            "test_name": item.name,
            "test_file": str(item.fspath) if hasattr(item, "fspath") else None,
            "test_nodeid": item.nodeid,
        }

        # Add source context if available
        if source_context:
            payload["source_context"] = source_context

        # Add comparison info for AssertionError
        if exc_type_name == "AssertionError" and hasattr(report, "longrepr"):
            try:
                # Extract comparison values from pytest's repr
                longrepr_str = str(report.longrepr)
                if "assert" in longrepr_str.lower():
                    payload["assertion_repr"] = longrepr_str[:2000]  # Limit size
            except Exception:
                pass

        # Record the exception
        try:
            recorder.record_event(payload)
            print(f"[autodebug] Captured exception: {exc_type_name}: {exc_message[:100]}", file=sys.stderr)
        except Exception as e:
            print(f"[autodebug] Failed to record exception: {e}", file=sys.stderr)


class QuarantinePlugin:
    """Pytest plugin that skips quarantined flaky tests."""

    def __init__(self, quarantine_file: Optional[Path] = None):
        self._quarantine_file = quarantine_file
        self._quarantined: Set[str] = set()
        self._skip_count = 0

    @pytest.hookimpl(tryfirst=True)
    def pytest_configure(self, config):
        """Load quarantined tests."""
        self._quarantined = _load_quarantined_tests(self._quarantine_file)
        if self._quarantined:
            print(f"[autodebug] Loaded {len(self._quarantined)} quarantined tests", file=sys.stderr)

    @pytest.hookimpl(tryfirst=True)
    def pytest_collection_modifyitems(self, config, items):
        """Skip quarantined tests during collection."""
        if not self._quarantined:
            return

        remaining = []
        for item in items:
            if item.nodeid in self._quarantined:
                # Mark as skipped with reason
                skip_marker = pytest.mark.skip(reason="Quarantined: flaky test")
                item.add_marker(skip_marker)
                self._skip_count += 1

            remaining.append(item)

        # Replace items in-place
        items[:] = remaining

    def pytest_terminal_summary(self, terminalreporter):
        """Report quarantined test count in summary."""
        if self._skip_count > 0:
            terminalreporter.write_line(
                f"[autodebug] Skipped {self._skip_count} quarantined flaky tests",
                yellow=True,
            )


def pytest_addoption(parser):
    """Add AutoDebug command line options."""
    parser.addoption(
        "--autodebug",
        action="store_true",
        default=False,
        help="Enable AutoDebug exception capture",
    )
    parser.addoption(
        "--skip-quarantined",
        action="store_true",
        default=False,
        help="Skip tests listed in .flaky-quarantine file",
    )
    parser.addoption(
        "--quarantine-file",
        type=str,
        default=None,
        help="Path to quarantine file (default: .flaky-quarantine)",
    )


def pytest_configure(config):
    """Register AutoDebug plugins."""
    global _hikaflow_probe_active

    # Register exception capture plugin (only if not already registered via entry point)
    if config.getoption("--autodebug", default=False) or os.environ.get("AUTODEBUG_RUN_ID"):
        if not config.pluginmanager.has_plugin("autodebug_capture"):
            config.pluginmanager.register(AutoDebugPlugin(), "autodebug_capture")
            _hikaflow_probe_active = True

    # Register quarantine plugin
    skip_quarantined = config.getoption("--skip-quarantined", default=False)
    quarantine_file_opt = config.getoption("--quarantine-file", default=None)

    # Also enable if AUTODEBUG_SKIP_QUARANTINED env var is set
    if skip_quarantined or os.environ.get("AUTODEBUG_SKIP_QUARANTINED"):
        quarantine_file = Path(quarantine_file_opt) if quarantine_file_opt else None
        config.pluginmanager.register(QuarantinePlugin(quarantine_file), "autodebug_quarantine")
