"""Adaptive N calculation for statistically rigorous flakiness detection.

Instead of a fixed N=10 runs, calculates the optimal number of runs
needed to detect flakiness at a given power level.

Formula: P(detect) = 1 - (1-p)^N >= target_power
         N >= log(1-target_power) / log(1-p)

Examples:
- p=0.10, power=0.95 -> N=29  (rare flaky, need many runs)
- p=0.50, power=0.95 -> N=5   (frequent flaky, few runs needed)
- p=0.01, power=0.95 -> N=299 (very rare, impractical)
"""

from __future__ import annotations

import math
from typing import Optional


def calculate_adaptive_n(
    estimated_flake_rate: float,
    target_power: float = 0.95,
    min_n: int = 5,
    max_n: int = 100,
    budget_seconds: Optional[float] = None,
    avg_test_duration_seconds: Optional[float] = None,
) -> int:
    """Calculate the optimal number of runs for flakiness detection.

    Args:
        estimated_flake_rate: Estimated probability of failure per run (0-1).
            Use 0.10 as default if unknown.
        target_power: Desired probability of detecting flakiness (0-1).
            Default 0.95 (95% detection power).
        min_n: Minimum number of runs (always run at least this many).
        max_n: Maximum number of runs (budget/time cap).
        budget_seconds: Optional time budget. If set, caps N so that
            total time <= budget.
        avg_test_duration_seconds: Average test duration for budget calc.

    Returns:
        Optimal number of runs N.
    """
    if min_n > max_n:
        raise ValueError(f"min_n ({min_n}) must be <= max_n ({max_n})")

    if estimated_flake_rate <= 0 or estimated_flake_rate >= 1:
        return min_n

    if target_power <= 0:
        return min_n
    if target_power >= 1:
        return max_n

    # N >= log(1 - target_power) / log(1 - p)
    try:
        n = math.ceil(
            math.log(1 - target_power) / math.log(1 - estimated_flake_rate)
        )
    except (ValueError, ZeroDivisionError):
        return min_n

    # Apply bounds
    n = max(min_n, min(n, max_n))

    # Apply time budget constraint
    if budget_seconds and avg_test_duration_seconds and avg_test_duration_seconds > 0:
        max_from_budget = int(budget_seconds / avg_test_duration_seconds)
        n = min(n, max(min_n, max_from_budget))

    return n


def estimate_flake_rate_from_history(
    total_runs: int,
    inconsistent_runs: int,
) -> float:
    """Estimate flake rate from historical data.

    Args:
        total_runs: Total number of runs observed
        inconsistent_runs: Number of runs with unexpected outcomes

    Returns:
        Estimated flake rate (0-1), or 0.10 if insufficient data
    """
    if total_runs < 0 or inconsistent_runs < 0:
        raise ValueError(
            f"total_runs ({total_runs}) and inconsistent_runs ({inconsistent_runs}) must be >= 0"
        )

    if total_runs < 3:
        return 0.10  # Default estimate

    rate = inconsistent_runs / total_runs

    # Clamp to reasonable range
    return max(0.01, min(0.50, rate))


def detection_power(flake_rate: float, n: int) -> float:
    """Calculate the probability of detecting a flaky test.

    Args:
        flake_rate: Probability of failure per run
        n: Number of runs

    Returns:
        Detection power (0-1)
    """
    if flake_rate <= 0 or flake_rate >= 1:
        return 0.0
    if n <= 0:
        return 0.0

    # P(detect) = 1 - (1-p)^N
    return 1 - (1 - flake_rate) ** n
