"""Dynamic order-dependency detection for flaky tests.

Detects tests whose pass/fail outcome depends on execution order by running
them in shuffled orders and comparing results. Inspired by iDFlakies/iPFlakies.
"""

from __future__ import annotations

import logging
import random
import subprocess
import sys
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


@dataclass
class OrderDependencyResult:
    """Result of order-dependency detection for a single test."""

    test_id: str
    is_order_dependent: bool
    original_result: str  # "pass" or "fail"
    shuffled_results: List[str] = field(default_factory=list)
    suspected_polluters: List[str] = field(default_factory=list)


def detect_order_dependent(
    test_ids: List[str],
    project_root: str = ".",
    num_shuffles: int = 3,
    timeout_per_run: int = 120,
) -> List[OrderDependencyResult]:
    """Run tests in shuffled orders to detect order-dependent flakiness.

    Args:
        test_ids: List of test node IDs to check.
        project_root: Project root directory.
        num_shuffles: Number of shuffled runs to perform.
        timeout_per_run: Timeout in seconds per test run.

    Returns:
        List of results, one per test, flagging order-dependent tests.
    """
    if len(test_ids) < 2:
        return []

    # Step 1: Run in original order to get baseline
    baseline = _run_tests(test_ids, project_root, timeout_per_run)
    if baseline is None:
        return []

    # Step 2: Run in shuffled orders
    shuffled_runs: List[Dict[str, str]] = []
    for _ in range(num_shuffles):
        shuffled = test_ids.copy()
        random.shuffle(shuffled)
        result = _run_tests(shuffled, project_root, timeout_per_run)
        if result is not None:
            shuffled_runs.append(result)

    # Step 3: Compare results — flag tests whose outcome flipped
    results = []
    for tid in test_ids:
        original = baseline.get(tid, "unknown")
        shuffled_results = [r.get(tid, "unknown") for r in shuffled_runs]
        flipped = any(s != original for s in shuffled_results)
        results.append(
            OrderDependencyResult(
                test_id=tid,
                is_order_dependent=flipped,
                original_result=original,
                shuffled_results=shuffled_results,
            )
        )

    return results


def _run_tests(
    test_ids: List[str],
    project_root: str,
    timeout: int,
) -> Optional[Dict[str, str]]:
    """Run a list of tests and return per-test pass/fail results."""
    try:
        cmd = [
            sys.executable, "-m", "pytest",
            "--tb=no", "-q", "--no-header",
            *test_ids,
        ]
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            cwd=project_root,
        )
        return _parse_pytest_output(proc.stdout, test_ids)
    except (subprocess.TimeoutExpired, Exception) as e:
        logger.warning("Test run failed: %s", e)
        return None


def _parse_pytest_output(output: str, test_ids: List[str]) -> Dict[str, str]:
    """Parse pytest -q output to extract per-test results."""
    results: Dict[str, str] = {}
    for line in output.splitlines():
        line = line.strip()
        if not line:
            continue
        # pytest -q outputs lines like "test_file.py::test_name PASSED"
        for tid in test_ids:
            if tid in line:
                if "PASSED" in line:
                    results[tid] = "pass"
                elif "FAILED" in line:
                    results[tid] = "fail"
                elif "ERROR" in line:
                    results[tid] = "fail"
                break
    # Default unknown tests to their baseline
    for tid in test_ids:
        if tid not in results:
            results[tid] = "unknown"
    return results
