```python
#!/usr/bin/env python3
"""API rate limit sanity checker.

This script helps you rehearse simple rate-limiting assumptions before you
ship an ASP.NET Core policy to production. It does not call your API or
contain any credentials. Instead, it simulates request bursts and reports
whether a configured policy would likely reject traffic.

Use cases:
- Compare fixed-window, sliding-window, and token-bucket assumptions.
- Estimate how many requests survive a burst.
- Sanity-check per-client or per-tenant limits.
- Document a policy for teammates and operators.

Example:
    python rate_limit_sanity_check.py --algorithm token-bucket \
        --limit 100 --window 60 --burst 140 --refill-rate 1.5
"""

from __future__ import annotations

import argparse
import math
import sys
from dataclasses import dataclass
from typing import List, Tuple


@dataclass(frozen=True)
class SimulationResult:
    algorithm: str
    allowed: int
    rejected: int
    rejection_rate: float
    notes: str


def positive_int(value: str) -> int:
    try:
        parsed = int(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError(f"invalid integer value: {value!r}") from exc
    if parsed <= 0:
        raise argparse.ArgumentTypeError("value must be greater than zero")
    return parsed


def non_negative_float(value: str) -> float:
    try:
        parsed = float(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError(f"invalid number value: {value!r}") from exc
    if parsed < 0:
        raise argparse.ArgumentTypeError("value must be zero or greater")
    return parsed


def simulate_fixed_window(limit: int, window_seconds: int, burst: int) -> SimulationResult:
    allowed = min(limit, burst)
    rejected = max(0, burst - allowed)
    note = (
        f"Fixed window: up to {limit} requests per {window_seconds}s window. "
        "Bursts near a window boundary can still produce edge effects."
    )
    return SimulationResult(
        algorithm="fixed-window",
        allowed=allowed,
        rejected=rejected,
        rejection_rate=(rejected / burst) if burst else 0.0,
        notes=note,
    )


def simulate_sliding_window(limit: int, window_seconds: int, burst: int) -> SimulationResult:
    allowed = min(limit, burst)
    rejected = max(0, burst - allowed)
    note = (
        f"Sliding window: up to {limit} requests across a rolling {window_seconds}s window. "
        "This smooths burst edges compared with fixed-window limits."
    )
    return SimulationResult(
        algorithm="sliding-window",
        allowed=allowed,
        rejected=rejected,
        rejection_rate=(rejected / burst) if burst else 0.0,
        notes=note,
    )


def simulate_token_bucket(limit: int, window_seconds: int, burst: int, refill_rate: float) -> SimulationResult:
    # Treat limit as the initial bucket capacity.
    capacity = float(limit)
    tokens = capacity
    allowed = 0
    rejected = 0

    # Simulate a burst arriving evenly within the window.
    if burst == 0:
        return SimulationResult(
            algorithm="token-bucket",
            allowed=0,
            rejected=0,
            rejection_rate=0.0,
            notes="No requests to simulate.",
        )

    inter_arrival = window_seconds / burst
    elapsed = 0.0

    for _ in range(burst):
        tokens = min(capacity, tokens + refill_rate * inter_arrival)
        elapsed += inter_arrival
        if tokens >= 1.0:
            tokens -= 1.0
            allowed += 1
        else:
            rejected += 1

    note = (
        f"Token bucket: capacity={limit}, refill_rate={refill_rate:.2f} tokens/sec, "
        f"simulated over {window_seconds}s. Good for burst tolerance with a steady average rate."
    )
    return SimulationResult(
        algorithm="token-bucket",
        allowed=allowed,
        rejected=rejected,
        rejection_rate=(rejected / burst),
        notes=note,
    )


def simulate_concurrency(limit: int, burst: int, average_duration_seconds: float, arrival_spread_seconds: float) -> SimulationResult:
    """Approximate concurrency pressure.

    Requests arrive across a spread window and each occupies a slot for the
    average duration. This is only a rough model, but it is useful for thinking
    about endpoints that are limited by simultaneous work rather than request rate.
    """
    if burst == 0:
        return SimulationResult(
            algorithm="concurrency",
            allowed=0,
            rejected=0,
            rejection_rate=0.0,
            notes="No requests to simulate.",
        )

    if average_duration_seconds <= 0:
        raise ValueError("average_duration_seconds must be greater than zero")
    if arrival_spread_seconds < 0:
        raise ValueError("arrival_spread_seconds cannot be negative")

    # Very simple occupancy model: estimated concurrent requests at peak is
    # roughly burst * duration / arrival_spread, capped by burst.
    if arrival_spread_seconds == 0:
        estimated_peak = burst
    else:
        estimated_peak = math.ceil((burst * average_duration_seconds) / arrival_spread_seconds)

    allowed = min(burst, limit if estimated_peak <= limit else limit)
    rejected = max(0, burst - allowed) if estimated_peak > limit else 0

    note = (
        f"Concurrency limit={limit}, average_duration={average_duration_seconds:.2f}s, "
        f"arrival_spread={arrival_spread_seconds:.2f}s. Estimated peak concurrency≈{estimated_peak}."
    )
    return SimulationResult(
        algorithm="concurrency",
        allowed=allowed,
        rejected=rejected,
        rejection_rate=(rejected / burst),
        notes=note,
    )


def print_result(result: SimulationResult) -> None:
    print(f"Algorithm:   {result.algorithm}")
    print(f"Allowed:     {result.allowed}")
    print(f"Rejected:    {result.rejected}")
    print(f"Reject rate:  {result.rejection_rate:.1%}")
    print(f"Notes:       {result.notes}")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Simulate basic API rate limiting scenarios for planning and validation."
    )
    parser.add_argument(
        "--algorithm",
        choices=("fixed-window", "sliding-window", "token-bucket", "concurrency"),
        default="token-bucket",
        help="Limiter style to simulate.",
    )
    parser.add_argument(
        "--limit",
        type=positive_int,
        required=True,
        help="Request limit or concurrency limit depending on the algorithm.",
    )
    parser.add_argument(
        "--window",
        type=positive_int,
        default=60,
        help="Time window in seconds for rate-based algorithms.",
    )
    parser.add_argument(
        "--burst",
        type=positive_int,
        default=100,
        help="Number of incoming requests to simulate.",
    )
    parser.add_argument(
        "--refill-rate",
        type=non_negative_float,
        default=1.0,
        help="Tokens per second for token-bucket simulation.",
    )
    parser.add_argument(
        "--average-duration",
        type=non_negative_float,
        default=0.5,
        help="Average request duration in seconds for concurrency simulation.",
    )
    parser.add_argument(
        "--arrival-spread",
        type=non_negative_float,
        default=1.0,
        help="How many seconds the burst is spread across for concurrency simulation.",
    )
    return parser


def main(argv: List[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    try:
        if args.algorithm == "fixed-window":
            result = simulate_fixed_window(args.limit, args.window, args.burst)
        elif args.algorithm == "sliding-window":
            result = simulate_sliding_window(args.limit, args.window, args.burst)
        elif args.algorithm == "token-bucket":
            result = simulate_token_bucket(args.limit, args.window, args.burst, args.refill_rate)
        else:
            if args.average_duration <= 0:
                parser.error("--average-duration must be greater than zero for concurrency simulation")
            result = simulate_concurrency(
                args.limit,
                args.burst,
                args.average_duration,
                args.arrival_spread,
            )
    except ValueError as exc:
        parser.error(str(exc))

    print_result(result)
    return 0


if __name__ == "__main__":
    sys.exit(main())
```