"""
Synthetic OHLCV bar fixtures for indicator tests.

Each helper returns a deterministic pd.DataFrame with columns
[open, high, low, close, volume] and a 'time' column (UTC, 5-min spacing).
Reused across all analysis/indicators/* unit tests so each test
focuses on the assertion, not the data construction.
"""

from __future__ import annotations

from datetime import datetime, timedelta, timezone

import numpy as np
import pandas as pd


_TF_MINUTES = {"5min": 5, "1hour": 60, "4hour": 240}


def _times(n: int, timeframe: str = "5min",
           start: datetime | None = None) -> list[datetime]:
    if start is None:
        start = datetime(2026, 4, 28, 8, 0, tzinfo=timezone.utc)
    step = timedelta(minutes=_TF_MINUTES[timeframe])
    return [start + i * step for i in range(n)]


def _frame(opens, highs, lows, closes, volumes, timeframe="5min") -> pd.DataFrame:
    n = len(opens)
    return pd.DataFrame({
        "time":   _times(n, timeframe),
        "open":   list(opens),
        "high":   list(highs),
        "low":    list(lows),
        "close":  list(closes),
        "volume": list(volumes),
    })


# ============================================================
# Trend / sideways / spike series
# ============================================================

def uptrend(n: int = 50, start_price: float = 100.0,
            step: float = 0.5, body: float = 0.4,
            wick: float = 0.1, volume: float = 1000.0,
            timeframe: str = "5min") -> pd.DataFrame:
    """
    Monotonic uptrend: each bar closes above its open by `body`,
    next bar opens at previous close.
    """
    opens = [start_price + i * step for i in range(n)]
    closes = [o + body for o in opens]
    highs = [c + wick for c in closes]
    lows = [o - wick for o in opens]
    return _frame(opens, highs, lows, closes,
                  [volume] * n, timeframe)


def downtrend(n: int = 50, start_price: float = 100.0,
              step: float = 0.5, body: float = 0.4,
              wick: float = 0.1, volume: float = 1000.0,
              timeframe: str = "5min") -> pd.DataFrame:
    opens = [start_price - i * step for i in range(n)]
    closes = [o - body for o in opens]
    highs = [o + wick for o in opens]
    lows = [c - wick for c in closes]
    return _frame(opens, highs, lows, closes,
                  [volume] * n, timeframe)


def sideways(n: int = 50, mid: float = 100.0,
             amplitude: float = 0.2, volume: float = 1000.0,
             timeframe: str = "5min") -> pd.DataFrame:
    """
    Bars oscillate around `mid` with small amplitude — RSI ~50, ATR small.
    """
    rng = np.random.default_rng(42)
    closes = mid + rng.normal(0, amplitude, n)
    opens = closes + rng.normal(0, amplitude / 2, n)
    highs = np.maximum(opens, closes) + amplitude * 0.3
    lows = np.minimum(opens, closes) - amplitude * 0.3
    return _frame(opens, highs, lows, closes,
                  [volume] * n, timeframe)


def with_spike(base: pd.DataFrame, spike_idx: int = -1,
               spike_size: float = 5.0,
               spike_volume: float = 5000.0) -> pd.DataFrame:
    """
    Inject a large-range bar at `spike_idx` (default last). Used to
    verify ATR ratio / volatility-spike detection.
    """
    df = base.copy()
    idx = df.index[spike_idx]
    o = df.loc[idx, "open"]
    df.loc[idx, "high"] = o + spike_size
    df.loc[idx, "low"] = o - spike_size
    df.loc[idx, "close"] = o + spike_size * 0.8
    df.loc[idx, "volume"] = spike_volume
    return df


# ============================================================
# Candle pattern fixtures (V15 _calc_* parity)
# ============================================================

def hammer_at_end(prefix_n: int = 30, prefix_close: float = 100.0,
                  hammer_open: float = 99.5, hammer_close: float = 99.8,
                  hammer_low: float = 98.0, hammer_high: float = 100.0
                  ) -> pd.DataFrame:
    """
    Sequence ending in a hammer candle (small body near the top,
    long lower wick).
    """
    base = sideways(prefix_n, mid=prefix_close, amplitude=0.05)
    extra = _frame(
        [hammer_open], [hammer_high], [hammer_low], [hammer_close], [1500]
    )
    extra["time"] = _times(prefix_n + 1, "5min")[-1:]
    return pd.concat([base, extra], ignore_index=True)


def shooting_star_at_end(prefix_n: int = 30,
                         prefix_close: float = 100.0,
                         star_open: float = 100.5,
                         star_close: float = 100.2,
                         star_high: float = 102.0,
                         star_low: float = 100.0) -> pd.DataFrame:
    base = sideways(prefix_n, mid=prefix_close, amplitude=0.05)
    extra = _frame(
        [star_open], [star_high], [star_low], [star_close], [1500]
    )
    extra["time"] = _times(prefix_n + 1, "5min")[-1:]
    return pd.concat([base, extra], ignore_index=True)


def bull_engulfing_at_end(prefix_n: int = 30,
                          prefix_close: float = 100.0) -> pd.DataFrame:
    """Last two bars: small bearish then a larger bullish engulfing."""
    base = sideways(prefix_n - 1, mid=prefix_close, amplitude=0.05)
    bearish = _frame([100.4], [100.5], [99.8], [99.9], [1000])
    bullish = _frame([99.7], [101.0], [99.6], [100.6], [1500])
    out = pd.concat([base, bearish, bullish], ignore_index=True)
    out["time"] = _times(len(out), "5min")
    return out


def bear_engulfing_at_end(prefix_n: int = 30,
                          prefix_close: float = 100.0) -> pd.DataFrame:
    base = sideways(prefix_n - 1, mid=prefix_close, amplitude=0.05)
    bullish = _frame([99.6], [100.2], [99.5], [100.1], [1000])
    bearish = _frame([100.3], [100.4], [99.0], [99.4], [1500])
    out = pd.concat([base, bullish, bearish], ignore_index=True)
    out["time"] = _times(len(out), "5min")
    return out


def doji_at_end(prefix_n: int = 30, prefix_close: float = 100.0,
                doji_high: float = 100.5,
                doji_low: float = 99.5) -> pd.DataFrame:
    """Last bar: open ≈ close (doji), wide range."""
    base = sideways(prefix_n, mid=prefix_close, amplitude=0.05)
    doji = _frame([100.0], [doji_high], [doji_low], [100.0], [1200])
    out = pd.concat([base, doji], ignore_index=True)
    out["time"] = _times(len(out), "5min")
    return out


# ============================================================
# Pivot helpers (for swing-detection tests)
# ============================================================

def with_pivot_low(n: int = 40, pivot_idx: int = 20,
                   base_price: float = 100.0,
                   pivot_drop: float = 1.0) -> pd.DataFrame:
    """
    Sideways series with a clear pivot LOW at `pivot_idx`
    (5-bar pivot: lower than 2 left + 2 right neighbors).
    """
    df = sideways(n, mid=base_price, amplitude=0.05)
    pl = base_price - pivot_drop
    df.loc[df.index[pivot_idx], "low"] = pl
    df.loc[df.index[pivot_idx], "close"] = pl + 0.05
    df.loc[df.index[pivot_idx], "open"] = pl + 0.10
    return df


def with_pivot_high(n: int = 40, pivot_idx: int = 20,
                    base_price: float = 100.0,
                    pivot_jump: float = 1.0) -> pd.DataFrame:
    df = sideways(n, mid=base_price, amplitude=0.05)
    ph = base_price + pivot_jump
    df.loc[df.index[pivot_idx], "high"] = ph
    df.loc[df.index[pivot_idx], "close"] = ph - 0.05
    df.loc[df.index[pivot_idx], "open"] = ph - 0.10
    return df


# ============================================================
# Structure fixtures (HH+HL / LH+LL on H1)
# ============================================================

def hh_hl_h1(n: int = 30) -> pd.DataFrame:
    """Bullish structure: each bar's high & low above previous."""
    base_open = 100.0
    rows = []
    for i in range(n):
        o = base_open + i * 1.0
        c = o + 0.5
        h = c + 0.3
        l = o - 0.3
        rows.append((o, h, l, c, 2000))
    df = pd.DataFrame(rows, columns=["open", "high", "low", "close", "volume"])
    df["time"] = _times(n, "1hour")
    return df


def lh_ll_h1(n: int = 30) -> pd.DataFrame:
    """Bearish structure: HH/LL inverted."""
    base_open = 100.0
    rows = []
    for i in range(n):
        o = base_open - i * 1.0
        c = o - 0.5
        h = o + 0.3
        l = c - 0.3
        rows.append((o, h, l, c, 2000))
    df = pd.DataFrame(rows, columns=["open", "high", "low", "close", "volume"])
    df["time"] = _times(n, "1hour")
    return df


def ranging_h1(n: int = 30, mid: float = 100.0,
               amplitude: float = 0.5) -> pd.DataFrame:
    """Bars confined within a band — no HH/HL nor LH/LL."""
    rng = np.random.default_rng(7)
    closes = mid + rng.uniform(-amplitude, amplitude, n)
    opens = closes + rng.uniform(-amplitude / 2, amplitude / 2, n)
    highs = np.maximum(opens, closes) + amplitude * 0.2
    lows = np.minimum(opens, closes) - amplitude * 0.2
    df = _frame(opens, highs, lows, closes, [1500] * n, "1hour")
    return df
