"""
Comparison tests: ferro_ta vs pandas-ta (Priority 4 - requires pandas-ta).

This module validates ferro_ta against pandas-ta for indicators, using 500-bar data
for proper convergence of EMA-seeded indicators. Documents known formula differences
and expected tolerances.

Requirements
------------
Install pandas-ta before running these tests::

    pip install pandas-ta

The tests are automatically skipped when pandas-ta is not installed.
"""

from __future__ import annotations

import numpy as np
import pytest

# ---------------------------------------------------------------------------
# Skip the whole module when pandas-ta is not available
# ---------------------------------------------------------------------------

pandas_ta = pytest.importorskip(
    "pandas_ta", reason="pandas-ta not installed; skipping comparison tests"
)
pd = pytest.importorskip("pandas", reason="pandas required for pandas-ta")

import ferro_ta  # noqa: E402

# ---------------------------------------------------------------------------
# Shared test data from conftest.py
# ---------------------------------------------------------------------------

# Use shared 500-bar fixture from conftest.py


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _nan_count(arr: np.ndarray) -> int:
    """Return count of NaN values."""
    return int(np.sum(np.isnan(arr)))


def _valid_mask(*arrays: np.ndarray) -> np.ndarray:
    """Return boolean mask for positions where *all* arrays are finite."""
    mask = np.ones(len(arrays[0]), dtype=bool)
    for a in arrays:
        mask &= ~np.isnan(a)
    return mask


def _allclose(
    a: np.ndarray, b: np.ndarray, atol: float = 1e-6, tail_fraction: float = 1.0
) -> bool:
    """Compare arrays within tolerance, optionally only comparing tail.

    Parameters
    ----------
    a, b : np.ndarray
        Arrays to compare
    atol : float
        Absolute tolerance
    tail_fraction : float
        Fraction of tail to compare (1.0 = all, 0.3 = last 30%)

    Returns
    -------
    bool
        True if arrays match within tolerance
    """
    mask = _valid_mask(a, b)
    if not mask.any():
        return False

    if tail_fraction < 1.0:
        # Only compare last tail_fraction of data
        n = len(a)
        start_idx = int(n * (1 - tail_fraction))
        mask[:start_idx] = False

    if not mask.any():
        return False

    return bool(np.allclose(a[mask], b[mask], atol=atol))


# ---------------------------------------------------------------------------
# Overlap Studies
# ---------------------------------------------------------------------------


class TestSMAVsPandasTA:
    """SMA — Exact match (deterministic)."""

    def test_sma_exact_match(self, ohlcv_500):
        """SMA should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        period = 20

        ft = ferro_ta.SMA(close, timeperiod=period)
        pt = pandas_ta.sma(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestEMAVsPandasTA:
    """EMA — Tail 30% match (seed difference).

    ferro_ta starts EMA from bar 0, pandas-ta may use SMA seed.
    After 350+ bars of decay, values should converge.
    """

    def test_ema_tail_convergence(self, ohlcv_500):
        """EMA should converge in tail 30% of data."""
        close = ohlcv_500["close"]
        period = 20

        ft = ferro_ta.EMA(close, timeperiod=period)
        pt = pandas_ta.ema(pd.Series(close), length=period).to_numpy()

        # Compare only last 30%
        assert _allclose(ft, pt, atol=1e-4, tail_fraction=0.3)

    def test_ema_shorter_period_tighter(self, ohlcv_500):
        """Shorter period EMA should have tighter convergence."""
        close = ohlcv_500["close"]
        period = 10

        ft = ferro_ta.EMA(close, timeperiod=period)
        pt = pandas_ta.ema(pd.Series(close), length=period).to_numpy()

        # Shorter period converges faster
        assert _allclose(ft, pt, atol=1e-5, tail_fraction=0.3)


class TestWMAVsPandasTA:
    """WMA — Exact match (deterministic)."""

    def test_wma_exact_match(self, ohlcv_500):
        """WMA should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        period = 20

        ft = ferro_ta.WMA(close, timeperiod=period)
        pt = pandas_ta.wma(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestBBANDSVsPandasTA:
    """BBANDS — Approximate match (ferro_ta uses population std; pandas-ta uses sample std)."""

    def test_bbands_approximate_match(self, ohlcv_500):
        """BBANDS middle band matches exactly; upper/lower match within std-formula tolerance.

        ferro_ta follows TA-Lib convention: std = population std (ddof=0).
        pandas-ta uses sample std (ddof=1).  Middle band (SMA) is identical.
        Upper/lower differ by a sqrt(N/(N-1)) factor (~0.5% for N=20), capped at atol=0.1.
        """
        close = ohlcv_500["close"]
        period = 20

        ft_upper, ft_middle, ft_lower = ferro_ta.BBANDS(
            close, timeperiod=period, nbdevup=2.0, nbdevdn=2.0
        )

        # pandas-ta >= 0.3 returns columns named BBL_{period}_{std}_{std}
        pt_bbands = pandas_ta.bbands(pd.Series(close), length=period, std=2.0)
        # Locate columns robustly (column names vary across pandas-ta versions)
        lower_col = next(c for c in pt_bbands.columns if c.startswith("BBL_"))
        middle_col = next(c for c in pt_bbands.columns if c.startswith("BBM_"))
        upper_col = next(c for c in pt_bbands.columns if c.startswith("BBU_"))
        pt_lower = pt_bbands[lower_col].to_numpy()
        pt_middle = pt_bbands[middle_col].to_numpy()
        pt_upper = pt_bbands[upper_col].to_numpy()

        # Middle band (SMA) must be identical
        assert _allclose(ft_middle, pt_middle, atol=1e-8), (
            "BBands middle (SMA) must match"
        )
        # Upper/lower: differ due to ddof=0 vs ddof=1
        assert _allclose(ft_upper, pt_upper, atol=0.1)
        assert _allclose(ft_lower, pt_lower, atol=0.1)


class TestTRIMAVsPandasTA:
    """TRIMA — Approximate match (implementations differ slightly in boundary handling)."""

    def test_trima_approximate_match(self, ohlcv_500):
        """TRIMA should be close to pandas-ta (both are SMA-of-SMA but boundary handling differs).

        Note: ferro_ta follows TA-Lib's TRIMA formula while pandas-ta uses a slightly
        different implementation.  Observed max difference is ~0.4 price units on
        typical equity prices (~100), which is < 0.5%.  We verify tail convergence
        with atol=0.5 and confirm correct NaN warm-up length.
        """
        close = ohlcv_500["close"]
        period = 20

        ft = ferro_ta.TRIMA(close, timeperiod=period)
        pt = pandas_ta.trima(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=0.5, tail_fraction=0.5)


class TestMACDVsPandasTA:
    """MACD — Tail 30% match (EMA seed difference)."""

    def test_macd_tail_convergence(self, ohlcv_500):
        """MACD should converge in tail 30% of data."""
        close = ohlcv_500["close"]

        ft_macd, ft_signal, ft_hist = ferro_ta.MACD(
            close, fastperiod=12, slowperiod=26, signalperiod=9
        )

        # pandas-ta returns DataFrame
        pt_macd = pandas_ta.macd(pd.Series(close), fast=12, slow=26, signal=9)
        pt_macd_line = pt_macd["MACD_12_26_9"].to_numpy()
        pt_signal_line = pt_macd["MACDs_12_26_9"].to_numpy()
        pt_hist = pt_macd["MACDh_12_26_9"].to_numpy()

        # Compare tail 30%
        assert _allclose(ft_macd, pt_macd_line, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_signal, pt_signal_line, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_hist, pt_hist, atol=1e-2, tail_fraction=0.3)


# ---------------------------------------------------------------------------
# Momentum Indicators
# ---------------------------------------------------------------------------


class TestRSIVsPandasTA:
    """RSI — Tail 30% match (Wilder seed difference)."""

    def test_rsi_tail_convergence(self, ohlcv_500):
        """RSI should converge in tail 30% of data."""
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.RSI(close, timeperiod=period)
        pt = pandas_ta.rsi(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-3, tail_fraction=0.3)


class TestSTOCHVsPandasTA:
    """STOCH — Tail 30% match."""

    def test_stoch_tail_convergence(self, ohlcv_500):
        """Stochastic should converge in tail 30% of data."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]

        ft_slowk, ft_slowd = ferro_ta.STOCH(
            high, low, close, fastk_period=14, slowk_period=3, slowd_period=3
        )

        # pandas-ta returns DataFrame
        pt_stoch = pandas_ta.stoch(
            pd.Series(high), pd.Series(low), pd.Series(close), k=14, d=3, smooth_k=3
        )
        pt_slowk = pt_stoch["STOCHk_14_3_3"].to_numpy()
        pt_slowd = pt_stoch["STOCHd_14_3_3"].to_numpy()

        assert _allclose(ft_slowk, pt_slowk, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_slowd, pt_slowd, atol=1e-2, tail_fraction=0.3)


class TestCCIVsPandasTA:
    """CCI — Exact match (deterministic rolling formula)."""

    def test_cci_exact_match(self, ohlcv_500):
        """CCI should match manually-computed reference (pandas-ta CCI has a formula bug)."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.CCI(high, low, close, timeperiod=period)

        # Compute CCI manually: (TP - SMA(TP)) / (0.015 * MeanAbsDev(TP))
        tp = (pd.Series(high) + pd.Series(low) + pd.Series(close)) / 3.0
        mean_tp = tp.rolling(period).mean()
        mad_tp = tp.rolling(period).apply(
            lambda x: np.mean(np.abs(x - x.mean())), raw=True
        )
        pt = ((tp - mean_tp) / (0.015 * mad_tp)).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestWILLRVsPandasTA:
    """WILLR — Exact match (deterministic)."""

    def test_willr_exact_match(self, ohlcv_500):
        """Williams %R should match pandas-ta exactly."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.WILLR(high, low, close, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt = df.ta.willr(length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestMOMVsPandasTA:
    """MOM — Exact match."""

    def test_mom_exact_match(self, ohlcv_500):
        """MOM should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        period = 10

        ft = ferro_ta.MOM(close, timeperiod=period)
        pt = pandas_ta.mom(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestROCVsPandasTA:
    """ROC — Exact match."""

    def test_roc_exact_match(self, ohlcv_500):
        """ROC should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        period = 10

        ft = ferro_ta.ROC(close, timeperiod=period)
        pt = pandas_ta.roc(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestMFIVsPandasTA:
    """MFI — Exact match."""

    def test_mfi_exact_match(self, ohlcv_500):
        """MFI should match pandas-ta exactly."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        volume = ohlcv_500["volume"]
        period = 14

        ft = ferro_ta.MFI(high, low, close, volume, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": close, "volume": volume})
        pt = df.ta.mfi(length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestAROONVsPandasTA:
    """AROON — Exact match."""

    def test_aroon_exact_match(self, ohlcv_500):
        """AROON should match pandas-ta exactly."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        period = 14

        ft_down, ft_up = ferro_ta.AROON(high, low, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low})
        pt_aroon = df.ta.aroon(length=period)
        pt_down = pt_aroon[f"AROOND_{period}"].to_numpy()
        pt_up = pt_aroon[f"AROONU_{period}"].to_numpy()

        assert _allclose(ft_down, pt_down, atol=1e-8)
        assert _allclose(ft_up, pt_up, atol=1e-8)


# ---------------------------------------------------------------------------
# Volume/Volatility
# ---------------------------------------------------------------------------


class TestOBVVsPandasTA:
    """OBV — Incremental match (offset constant, verify diffs)."""

    def test_obv_incremental_match(self, ohlcv_500):
        """OBV differences should match (absolute values may have offset)."""
        close = ohlcv_500["close"]
        volume = ohlcv_500["volume"]

        ft = ferro_ta.OBV(close, volume)

        df = pd.DataFrame({"close": close, "volume": volume})
        pt = df.ta.obv().to_numpy()

        # OBV can have different starting values, compare differences
        ft_diff = np.diff(ft)
        pt_diff = np.diff(pt)

        # Remove NaN values from comparison
        mask = ~np.isnan(ft_diff) & ~np.isnan(pt_diff)
        assert np.allclose(ft_diff[mask], pt_diff[mask], atol=1e-8)


class TestATRVsPandasTA:
    """ATR — Tail 30% match (Wilder seed difference)."""

    def test_atr_tail_convergence(self, ohlcv_500):
        """ATR should converge in tail 30% of data."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.ATR(high, low, close, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt = df.ta.atr(length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-2, tail_fraction=0.3)


class TestADXVsPandasTA:
    """ADX — Tail 30% match (two levels of Wilder smoothing)."""

    def test_adx_tail_convergence(self, ohlcv_500):
        """ADX should converge in tail 30% of data."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.ADX(high, low, close, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt = df.ta.adx(length=period)[f"ADX_{period}"].to_numpy()

        assert _allclose(ft, pt, atol=5e-2, tail_fraction=0.3)


# ---------------------------------------------------------------------------
# Extended Indicators (no prior validation)
# ---------------------------------------------------------------------------


class TestVWAPVsPandasTA:
    """VWAP — Validate rolling VWAP against a reference numpy implementation."""

    def test_vwap_rolling_match(self, ohlcv_500):
        """Rolling VWAP should match a reference implementation using numpy."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        volume = ohlcv_500["volume"]
        period = 20

        ft = ferro_ta.VWAP(high, low, close, volume, timeperiod=period)

        # Reference: rolling VWAP = sum(typical_price * volume, N) / sum(volume, N)
        tp = (np.array(high) + np.array(low) + np.array(close)) / 3.0
        vol = np.array(volume)
        n = len(tp)
        ref = np.full(n, np.nan)
        for i in range(period - 1, n):
            w = tp[i - period + 1 : i + 1]
            v = vol[i - period + 1 : i + 1]
            ref[i] = np.dot(w, v) / v.sum()

        assert _allclose(ft, ref, atol=1e-8)


class TestDONCHIANVsPandasTA:
    """DONCHIAN — Exact match (rolling max(H), min(L), mean)."""

    def test_donchian_exact_match(self, ohlcv_500):
        """Donchian Channels should match pandas-ta exactly."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        period = 20

        ft_upper, ft_middle, ft_lower = ferro_ta.DONCHIAN(high, low, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": ohlcv_500["close"]})
        pt_donchian = df.ta.donchian(lower_length=period, upper_length=period)
        pt_lower = pt_donchian[f"DCL_{period}_{period}"].to_numpy()
        pt_middle = pt_donchian[f"DCM_{period}_{period}"].to_numpy()
        pt_upper = pt_donchian[f"DCU_{period}_{period}"].to_numpy()

        assert _allclose(ft_upper, pt_upper, atol=1e-8)
        assert _allclose(ft_middle, pt_middle, atol=1e-8)
        assert _allclose(ft_lower, pt_lower, atol=1e-8)


class TestHULL_MAVsPandasTA:
    """HULL_MA — Exact match (WMA composition: deterministic)."""

    def test_hull_ma_exact_match(self, ohlcv_500):
        """Hull MA should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        period = 16

        ft = ferro_ta.HULL_MA(close, timeperiod=period)
        pt = pandas_ta.hma(pd.Series(close), length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestICHIMOKUVsPandasTA:
    """ICHIMOKU — Exact match for tenkan/kijun (rolling midpoint formula)."""

    def test_ichimoku_tenkan_kijun_match(self, ohlcv_500):
        """Ichimoku tenkan and kijun should match pandas-ta."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]

        ft_tenkan, ft_kijun, ft_senkou_a, ft_senkou_b, ft_chikou = ferro_ta.ICHIMOKU(
            high,
            low,
            close,
            tenkan_period=9,
            kijun_period=26,
            senkou_b_period=52,
            displacement=26,
        )

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt_ichimoku = df.ta.ichimoku(tenkan=9, kijun=26, senkou=52)[0]
        pt_tenkan = pt_ichimoku["ITS_9"].to_numpy()
        pt_kijun = pt_ichimoku["IKS_26"].to_numpy()

        assert _allclose(ft_tenkan, pt_tenkan, atol=1e-8)
        assert _allclose(ft_kijun, pt_kijun, atol=1e-8)


class TestKELTNER_CHANNELSVsPandasTA:
    """KELTNER_CHANNELS — Tail 30% match (Middle=EMA, bands=EMA±mult*ATR)."""

    def test_keltner_tail_convergence(self, ohlcv_500):
        """Keltner Channels should converge in tail 30%."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 20
        atr_period = 10
        multiplier = 2.0

        ft_upper, ft_middle, ft_lower = ferro_ta.KELTNER_CHANNELS(
            high,
            low,
            close,
            timeperiod=period,
            atr_period=atr_period,
            multiplier=multiplier,
        )

        # Compute manually using pandas_ta EMA and ATR to match ferro_ta's exact formula
        pt_ema = pandas_ta.ema(pd.Series(close), length=period).to_numpy()
        pt_atr = pandas_ta.atr(
            pd.Series(high), pd.Series(low), pd.Series(close), length=atr_period
        ).to_numpy()
        pt_upper = pt_ema + multiplier * pt_atr
        pt_middle = pt_ema
        pt_lower = pt_ema - multiplier * pt_atr

        assert _allclose(ft_upper, pt_upper, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_middle, pt_middle, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_lower, pt_lower, atol=1e-2, tail_fraction=0.3)


class TestVWMAVsPandasTA:
    """VWMA — Exact match (sum(c*v)/sum(v))."""

    def test_vwma_exact_match(self, ohlcv_500):
        """VWMA should match pandas-ta exactly."""
        close = ohlcv_500["close"]
        volume = ohlcv_500["volume"]
        period = 20

        ft = ferro_ta.VWMA(close, volume, timeperiod=period)

        df = pd.DataFrame({"close": close, "volume": volume})
        pt = df.ta.vwma(length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-8)


class TestCHOPPINESS_INDEXVsPandasTA:
    """CHOPPINESS_INDEX — Close match (log10-based formula)."""

    def test_choppiness_index_close_match(self, ohlcv_500):
        """Choppiness Index should match pandas-ta closely."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 14

        ft = ferro_ta.CHOPPINESS_INDEX(high, low, close, timeperiod=period)

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt = df.ta.chop(length=period).to_numpy()

        assert _allclose(ft, pt, atol=1e-4)


class TestSUPERTRENDVsPandasTA:
    """SUPERTREND — Direction >80% agreement (path-dependent, ATR seeding differs)."""

    def test_supertrend_direction_agreement(self, ohlcv_500):
        """SUPERTREND direction should agree >80% of the time."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 7
        multiplier = 3.0

        ft_line, ft_dir = ferro_ta.SUPERTREND(
            high, low, close, timeperiod=period, multiplier=multiplier
        )

        df = pd.DataFrame({"high": high, "low": low, "close": close})
        pt_supertrend = df.ta.supertrend(length=period, multiplier=multiplier)
        pt_dir = pt_supertrend[f"SUPERTd_{period}_{multiplier}"].to_numpy()

        # Convert directions to same format (1 = up, -1 = down)
        # pandas-ta: 1 = uptrend, -1 = downtrend
        # ferro_ta: 1 = uptrend, -1 = downtrend (assuming same convention)

        # Remove NaN values
        mask = ~np.isnan(ft_dir) & ~np.isnan(pt_dir)
        agreement_rate = np.mean(ft_dir[mask] == pt_dir[mask])

        assert agreement_rate > 0.80, f"Direction agreement rate: {agreement_rate:.2%}"


class TestCHANDELIER_EXITVsPandasTA:
    """CHANDELIER_EXIT — Exact structure (rolling_max(H)-mult*ATR)."""

    def test_chandelier_exit_structure_match(self, ohlcv_500):
        """Chandelier Exit should match pandas-ta structure."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]
        period = 22
        multiplier = 3.0

        ft_long, ft_short = ferro_ta.CHANDELIER_EXIT(
            high, low, close, timeperiod=period, multiplier=multiplier
        )

        # Compute manually: long = rolling_max(H, n) - mult*ATR; short = rolling_min(L, n) + mult*ATR
        pt_atr = pandas_ta.atr(
            pd.Series(high), pd.Series(low), pd.Series(close), length=period
        ).to_numpy()
        rolling_high = pd.Series(high).rolling(period).max().to_numpy()
        rolling_low = pd.Series(low).rolling(period).min().to_numpy()
        pt_long = rolling_high - multiplier * pt_atr
        pt_short = rolling_low + multiplier * pt_atr

        assert _allclose(ft_long, pt_long, atol=1e-2, tail_fraction=0.3)
        assert _allclose(ft_short, pt_short, atol=1e-2, tail_fraction=0.3)


class TestPIVOT_POINTSVsPandasTA:
    """PIVOT_POINTS — Exact match for Classic (arithmetic formula)."""

    def test_pivot_points_classic_exact(self, ohlcv_500):
        """Classic Pivot Points should match manually-computed reference."""
        high = ohlcv_500["high"]
        low = ohlcv_500["low"]
        close = ohlcv_500["close"]

        ft_pivot, ft_r1, ft_s1, ft_r2, ft_s2 = ferro_ta.PIVOT_POINTS(
            high, low, close, method="classic"
        )

        # ferro_ta PIVOT_POINTS uses previous bar's H/L/C (1-bar forward shift).
        # Reference values are computed from bar i-1 to match index i output.
        pivot = np.empty_like(high, dtype=float)
        pivot[0] = np.nan
        pivot[1:] = (high[:-1] + low[:-1] + close[:-1]) / 3.0

        r1 = np.empty_like(high, dtype=float)
        r1[0] = np.nan
        r1[1:] = 2 * pivot[1:] - low[:-1]

        s1 = np.empty_like(high, dtype=float)
        s1[0] = np.nan
        s1[1:] = 2 * pivot[1:] - high[:-1]

        r2 = np.empty_like(high, dtype=float)
        r2[0] = np.nan
        r2[1:] = pivot[1:] + (high[:-1] - low[:-1])

        s2 = np.empty_like(high, dtype=float)
        s2[0] = np.nan
        s2[1:] = pivot[1:] - (high[:-1] - low[:-1])

        assert _allclose(ft_pivot, pivot, atol=1e-8)
        assert _allclose(ft_r1, r1, atol=1e-8)
        assert _allclose(ft_s1, s1, atol=1e-8)
        assert _allclose(ft_r2, r2, atol=1e-8)
        assert _allclose(ft_s2, s2, atol=1e-8)
