"""pytest plugin for Hikaflow no-code test quarantine.

This plugin integrates with pytest to:
1. Skip quarantined tests automatically (NO CODE CHANGES NEEDED)
2. Record test results for flakiness tracking
3. Support quarantine-aware exit codes

## No-Code Quarantine

Tests can be quarantined via the Hikaflow API or CLI without modifying test code:

    # Via CLI
    hikaflow quarantine add "test_module.py::test_flaky"

    # Via API
    POST /v1/projects/{project_id}/quarantine
    {"test_id": "test_module.py::test_flaky", "reason": "Flaky - investigating"}

The pytest plugin automatically fetches quarantined tests and skips them.

## Usage

Enable quarantine mode in any of these ways:

1. Environment variable (recommended for CI):
    HIKAFLOW_QUARANTINE=1 pytest tests/

2. Command line:
    pytest --hikaflow-quarantine tests/

3. Configuration (pytest.ini or pyproject.toml):
    [tool.pytest.ini_options]
    hikaflow_project_id = "your-project-id"
    hikaflow_quarantine = true

## Exit Codes

With --hikaflow-exit-code:
- 0: All tests passed (quarantined tests that fail are ignored)
- 1: Real test failure (non-quarantined test failed)
- 2: Only quarantined tests failed (CI should pass but signal activity)
"""

from __future__ import annotations

import os
from datetime import datetime, timezone
from typing import Any, List, Optional, Set

import pytest

from autodebug.flakiness import (
    QuarantineManager,
    TestResultRecord,
    determine_exit_code,
    format_quarantine_summary,
)


def _is_quarantine_enabled(config: pytest.Config) -> bool:
    """Check if quarantine mode should be enabled.

    Quarantine is enabled if any of:
    1. --hikaflow-quarantine flag is passed
    2. HIKAFLOW_QUARANTINE env var is set (1, true, yes)
    3. hikaflow_quarantine = true in config file
    """
    # Check command line option
    if config.getoption("--hikaflow-quarantine", default=False):
        return True

    # Check environment variable
    env_val = os.environ.get("HIKAFLOW_QUARANTINE", "").lower()
    if env_val in ("1", "true", "yes", "on"):
        return True

    # Check config file
    ini_val = config.getini("hikaflow_quarantine")
    if ini_val and str(ini_val).lower() in ("1", "true", "yes", "on"):
        return True

    return False


def pytest_addoption(parser: pytest.Parser) -> None:
    """Add Hikaflow-specific command line options."""
    group = parser.getgroup("hikaflow", "Hikaflow no-code quarantine options")

    group.addoption(
        "--hikaflow-quarantine",
        action="store_true",
        default=False,
        help="Enable Hikaflow quarantine mode (also via HIKAFLOW_QUARANTINE=1 env var)",
    )

    group.addoption(
        "--hikaflow-project-id",
        action="store",
        default=None,
        help="Hikaflow project ID (overrides config/env)",
    )

    group.addoption(
        "--hikaflow-record-results",
        action="store_true",
        default=True,
        help="Record test results for flakiness tracking (default: true)",
    )

    group.addoption(
        "--hikaflow-exit-code",
        action="store_true",
        default=False,
        help="Use quarantine-aware exit codes (0=pass, 1=real fail, 2=quarantine fail)",
    )

    group.addoption(
        "--hikaflow-capture",
        action="store_true",
        default=False,
        help="Enable I/O capture mode (records HTTP, SQL, Redis, etc.)",
    )

    group.addoption(
        "--hikaflow-replay",
        action="store",
        default=None,
        metavar="RUN_ID",
        help="Enable replay mode using transcripts from specified run ID",
    )

    group.addoption(
        "--hikaflow-quarantine-mode",
        action="store",
        default="shadow",
        choices=["skip", "shadow"],
        help="Quarantine mode: 'shadow' runs tests as xfail (default), 'skip' hard-skips them",
    )

    group.addoption(
        "--hikaflow-chaos",
        action="store_true",
        default=False,
        help="Enable chaos mode for flakiness testing",
    )

    # Config file options
    parser.addini(
        "hikaflow_quarantine",
        "Enable Hikaflow quarantine mode",
        type="bool",
        default=False,
    )

    parser.addini(
        "hikaflow_project_id",
        "Hikaflow project ID",
        default=None,
    )


def pytest_configure(config: pytest.Config) -> None:
    """Configure Hikaflow plugin."""
    # Register markers
    config.addinivalue_line(
        "markers",
        "quarantined: mark test as quarantined (skipped but tracked)",
    )
    config.addinivalue_line(
        "markers",
        "flaky: mark test as known to be flaky (tracked but not skipped)",
    )
    config.addinivalue_line(
        "markers",
        "visual: mark test as a visual regression test",
    )
    config.addinivalue_line(
        "markers",
        "chaos: mark test for chaos testing",
    )
    config.addinivalue_line(
        "markers",
        "replay: mark test to use replay mode",
    )
    config.addinivalue_line(
        "markers",
        "capture: mark test to capture I/O operations",
    )

    # Store whether quarantine is enabled (checks env var too)
    config._hikaflow_enabled = _is_quarantine_enabled(config)

    # Get project ID from options, ini, or environment
    project_id = (
        config.getoption("--hikaflow-project-id", default=None)
        or config.getini("hikaflow_project_id")
        or os.environ.get("HIKAFLOW_PROJECT_ID")
    )

    # Initialize quarantine manager
    if project_id:
        config._hikaflow_quarantine_mgr = QuarantineManager(project_id=project_id)
    else:
        config._hikaflow_quarantine_mgr = None

    # Store results for exit code calculation
    config._hikaflow_results: List[TestResultRecord] = []
    config._hikaflow_quarantined: Set[str] = set()

    # Log status if enabled
    if config._hikaflow_enabled:
        if project_id:
            print(f"\n[Hikaflow] No-code quarantine enabled for project: {project_id}")
        else:
            print("\n[Hikaflow] Quarantine enabled but no project ID set")


@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(
    session: pytest.Session,
    config: pytest.Config,
    items: List[pytest.Item],
) -> None:
    """Modify collected tests to skip quarantined ones (no code changes needed)."""
    if not getattr(config, "_hikaflow_enabled", False):
        return

    mgr: Optional[QuarantineManager] = getattr(config, "_hikaflow_quarantine_mgr", None)
    if not mgr:
        return

    # Get quarantined tests from API
    quarantined = mgr.get_quarantined_tests()
    config._hikaflow_quarantined = quarantined

    if not quarantined:
        return

    quarantine_mode = config.getoption("--hikaflow-quarantine-mode", default="shadow")
    env_mode = os.environ.get("HIKAFLOW_QUARANTINE_MODE", "").lower()
    if env_mode in ("skip", "shadow"):
        quarantine_mode = env_mode

    quarantined_count = 0
    for item in items:
        test_id = item.nodeid

        # Check if this test is quarantined via API
        if test_id in quarantined:
            # Get flakiness score if available
            health = mgr.get_test_health(test_id)
            score = health.flakiness_score if health else None

            reason = "Quarantined by Hikaflow API"
            if score is not None:
                reason += f" (flakiness_score={score:.2f})"

            if quarantine_mode == "shadow":
                # Shadow mode: run the test but mark as xfail so failures don't block CI.
                # This accumulates stability data for auto-unquarantine decisions.
                item.add_marker(pytest.mark.xfail(reason=reason, strict=False))
            else:
                # Skip mode: hard-skip quarantined tests (legacy behavior)
                item.add_marker(pytest.mark.skip(reason=reason))
            quarantined_count += 1

    if quarantined_count > 0:
        mode_label = "shadow-running" if quarantine_mode == "shadow" else "skipping"
        print(f"[Hikaflow] {mode_label.capitalize()} {quarantined_count} quarantined test(s)")


@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(
    item: pytest.Item,
    call: pytest.CallInfo,
) -> None:
    """Record test results for flakiness tracking."""
    outcome = yield
    report = outcome.get_result()

    # Only record call phase (not setup/teardown)
    if report.when != "call":
        return

    config = item.config

    # Check if recording is enabled
    if not config.getoption("--hikaflow-record-results", default=True):
        return

    mgr: Optional[QuarantineManager] = getattr(config, "_hikaflow_quarantine_mgr", None)

    # Create test result
    result = TestResultRecord(
        test_id=item.nodeid,
        passed=report.passed,
        duration_ms=int(report.duration * 1000) if report.duration else None,
        failure_message=str(report.longrepr)[:1000] if report.longrepr else None,
        created_at=datetime.now(timezone.utc),
    )

    # Store for exit code calculation
    results: List[TestResultRecord] = getattr(config, "_hikaflow_results", [])
    results.append(result)

    # Record to API if manager available
    if mgr:
        mgr.record_result(result)


def pytest_terminal_summary(
    terminalreporter: pytest.TerminalReporter,
    exitstatus: int,
    config: pytest.Config,
) -> None:
    """Print quarantine summary at end of test run."""
    if not getattr(config, "_hikaflow_enabled", False):
        return

    results: List[TestResultRecord] = getattr(config, "_hikaflow_results", [])
    quarantined: Set[str] = getattr(config, "_hikaflow_quarantined", set())

    if not results:
        return

    # Print summary
    terminalreporter.ensure_newline()
    terminalreporter.section("Hikaflow Quarantine Summary")

    summary = format_quarantine_summary(results, quarantined)
    for line in summary.split("\n"):
        terminalreporter.line(line)

    # Show quarantine-aware exit code
    if config.getoption("--hikaflow-exit-code", default=False):
        new_exit = determine_exit_code(results, quarantined)
        if new_exit != exitstatus:
            terminalreporter.line("")
            terminalreporter.line(
                f"Quarantine exit code: {new_exit} "
                f"(original: {exitstatus})"
            )


@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(
    session: pytest.Session,
    exitstatus: int,
) -> None:
    """Modify exit status based on quarantine rules."""
    config = session.config

    if not config.getoption("--hikaflow-exit-code", default=False):
        return

    results: List[TestResultRecord] = getattr(config, "_hikaflow_results", [])
    quarantined: Set[str] = getattr(config, "_hikaflow_quarantined", set())

    if not results:
        return

    # Calculate quarantine-aware exit code
    new_exit = determine_exit_code(results, quarantined)

    # Only change if we're lowering the severity (don't hide errors)
    if new_exit < exitstatus:
        session.exitstatus = new_exit


# Entrypoint for pytest plugin registration
def pytest_load_initial_conftests(parser: pytest.Parser) -> None:
    """Register plugin early for configuration."""
    pass


# ============================================================================
# FIXTURES
# ============================================================================


@pytest.fixture
def capture(request: pytest.FixtureRequest):
    """Fixture for manual I/O capture control.

    Usage:
        def test_api_call(capture):
            capture.start()
            response = requests.get("https://api.example.com")
            capture.stop()

            # Access captured data
            for call in capture.http_calls:
                print(call.url, call.status)
    """
    from dataclasses import dataclass, field

    @dataclass
    class CaptureContext:
        """Context for capturing I/O operations."""

        http_calls: List[Any] = field(default_factory=list)
        sql_calls: List[Any] = field(default_factory=list)
        redis_calls: List[Any] = field(default_factory=list)
        _active: bool = False

        def start(self) -> None:
            """Start capturing I/O."""
            self._active = True
            # Install HTTP/SQL/Redis patches for capture
            try:
                from autodebug_probe import runtime
                runtime.initialize()
            except ImportError:
                pass  # autodebug_probe not available

        def stop(self) -> None:
            """Stop capturing I/O."""
            self._active = False
            # Would uninstall patches

        def clear(self) -> None:
            """Clear captured data."""
            self.http_calls.clear()
            self.sql_calls.clear()
            self.redis_calls.clear()

        @property
        def is_active(self) -> bool:
            return self._active

    ctx = CaptureContext()

    # Auto-start if --hikaflow-capture flag is set
    if request.config.getoption("--hikaflow-capture", default=False):
        ctx.start()

    yield ctx

    # Cleanup
    if ctx.is_active:
        ctx.stop()


@pytest.fixture
def replay(request: pytest.FixtureRequest):
    """Fixture for replay mode.

    Replays recorded I/O from a previous run.

    Usage:
        def test_api_call(replay):
            # I/O operations will use recorded responses
            response = requests.get("https://api.example.com")
            assert response.status_code == 200  # Replayed
    """
    from dataclasses import dataclass

    @dataclass
    class ReplayContext:
        """Context for replaying I/O operations."""

        run_id: Optional[str] = None
        _active: bool = False
        _divergences: List[str] = None

        def __post_init__(self):
            self._divergences = []

        def activate(self, run_id: str) -> None:
            """Activate replay from a specific run."""
            self.run_id = run_id
            self._active = True
            # Would install replay patches from autodebug_replay

        def deactivate(self) -> None:
            """Deactivate replay mode."""
            self._active = False
            # Would uninstall replay patches

        @property
        def is_active(self) -> bool:
            return self._active

        @property
        def divergences(self) -> List[str]:
            return self._divergences.copy()

    ctx = ReplayContext()

    # Auto-activate if --hikaflow-replay flag is set
    run_id = request.config.getoption("--hikaflow-replay", default=None)
    if run_id:
        ctx.activate(run_id)

    yield ctx

    # Cleanup
    if ctx.is_active:
        ctx.deactivate()


@pytest.fixture
def chaos(request: pytest.FixtureRequest):
    """Fixture for chaos testing.

    Injects controlled failures to test resilience.

    Usage:
        def test_retry_logic(chaos):
            with chaos.inject(latency_ms=500):
                response = api_client.get("/data")
                assert response.status_code == 200
    """
    try:
        from autodebug.chaos import ChaosConfig, ChaosInjector, chaos_mode
    except ImportError:
        # Fallback if chaos module not available
        from contextlib import contextmanager
        from dataclasses import dataclass

        @dataclass
        class ChaosConfig:
            timing_enabled: bool = False
            max_jitter_ms: int = 100

            @classmethod
            def gentle(cls):
                return cls(timing_enabled=True, max_jitter_ms=50)

            @classmethod
            def aggressive(cls):
                return cls(timing_enabled=True, max_jitter_ms=500)

        class ChaosInjector:
            def __init__(self, config=None):
                self.config = config or ChaosConfig()

            @contextmanager
            def inject(self, **kwargs):
                yield

        chaos_mode = None

    injector = ChaosInjector()

    # Auto-enable if --hikaflow-chaos flag is set
    if request.config.getoption("--hikaflow-chaos", default=False):
        injector = ChaosInjector(ChaosConfig.gentle())

    yield injector


# Re-export visual fixture from visual plugin if available
try:
    from autodebug.visual.pytest_plugin import visual, visual_fixture
except ImportError:
    # Provide stub if visual module not available
    @pytest.fixture
    def visual(request: pytest.FixtureRequest):
        """Visual regression testing fixture (stub).

        Install autodebug[visual] for full support.
        """
        raise pytest.skip("Visual testing requires autodebug[visual]")
