"""
RED→GREEN acceptance test for the trailing-stop time-gating parity fix.

Pine gates BOTH `strategy.close` calls (score-exit and trailing-stop) on
`timeCondition = time >= startDate and time <= endDate`. So a position that is
open when the window ends is held open to the chart end — Pine cannot close it
after `endDate`.

Python's `_apply_trailing_stop` previously fired the trailing stop regardless of
the time window, so a trade open across `endDate` would trail-out in OOS while
TV held it open (the 4H "TV#106 open trade" parity break).

Given/When/Then:
  Given a trade entered just before `endDate` whose price drops below the trail
        stop only on bars AFTER `endDate`,
  When  the signals are generated with a Pine time gate (`_pine_time_end`),
  Then  no exit (trail or score) fires after `endDate` — the position is held
        open, matching Pine.

A second case (no time gate) asserts the trailing stop still fires normally, so
the fix does not disable the trail inside the trading window.
"""

import numpy as np
import pandas as pd
import pytest

from strategies.strategy_mlp_scores import _score_to_signals


END = pd.Timestamp("2026-02-28")  # window end (= config TRAIN_END in real runs)


def _fixture_df():
    # 8 daily bars straddling END: idx0..4 are in-window (02-24..02-28),
    # idx5..7 are OOS (03-01..03-03).
    times = pd.date_range("2026-02-24", periods=8, freq="D")
    #               idx:   0    1    2    3    4     5    6    7
    score = np.array([10, 150,  40,  40,  40,   40,  40,  40], dtype=float)
    # Entry crossunder of 100 fires at idx2 (score[1]=150>=100, score[2]=40<100).
    # Price rises to 100 by the END bar (idx4), then crashes to 70 in OOS (idx5+),
    # which is <= trade_high*(1-0.20)=80 → trailing stop would fire at idx5.
    close = np.array([50,  50,  60,  60, 100,   70,  70,  70], dtype=float)
    high = np.array([50,  50,  60,  60, 100,   70,  70,  70], dtype=float)
    return pd.DataFrame({"time": times, "activation_score": score,
                         "close": close, "high": high})


def _base_params():
    # use_exit_conf with conf well below any score so the score-exit never fires;
    # only the trailing stop can close the trade. entry/exit windows = 1 (no smoothing).
    return {
        "i_long_entry_activation_threshold": 100.0,
        "i_long_exit_activation_threshold": 50.0,
        "i_long_exit_activation_confirmation_threshold": -10000.0,
        "i_use_long_exit_confirmation": 1.0,
        "i_use_long_entry_confirmation": False,
        "i_trailing_stop_threshold": 20.0,
        "i_exit_score_window": 1,
        "i_entry_score_window": 1,
    }


def _run(params):
    df = _fixture_df()
    stoch_peak = np.zeros(len(df), dtype=np.float64)
    out = _score_to_signals(
        df, params, stoch_peak,
        df["close"].to_numpy(dtype=np.float64),
        df["high"].to_numpy(dtype=np.float64),
    )
    return out


def test_trail_does_not_fire_after_endDate_when_time_gated():
    """Given a Pine time gate, the trailing stop must NOT fire in OOS."""
    params = _base_params()
    params["_pine_time_start"] = pd.Timestamp("2017-12-01")
    params["_pine_time_end"] = END
    out = _run(params)

    # Trade enters at idx2 (in-window).
    assert out["execute_entry"].iloc[2], "expected entry at idx2"

    # No exit may fire after END — Pine holds the position open past endDate.
    after_end = out["time"] > END
    assert not out.loc[after_end, "execute_exit"].any(), (
        "trailing stop fired after endDate — Python should hold the position "
        "open to match Pine's timeCondition-gated strategy.close"
    )
    # And the position is still open on the last bar (held open like TV).
    assert out["position"].iloc[-1] == 1, "position should remain open past endDate"


def test_trail_still_fires_when_not_time_gated():
    """Sanity: without a time gate, the trailing stop fires normally at idx5."""
    params = _base_params()  # no _pine_time_* keys
    out = _run(params)

    assert out["execute_entry"].iloc[2], "expected entry at idx2"
    # The OOS crash bar (idx5) trips the 20% trailing stop.
    assert out["execute_exit"].iloc[5], "trailing stop should fire at idx5 without a time gate"
    assert out["position"].iloc[-1] == 0, "position should be closed after the trail fires"
