import sys
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path

STRATEGY_DIR = Path(__file__).resolve().parents[2] / "strategies" / "volatility_breakout"
sys.path.insert(0, str(STRATEGY_DIR))

from entry_block_observability import (  # noqa: E402
    candidate_signal_snapshot,
    occupied_trade_snapshots,
    replacement_shadow_decision,
)


class FakeTrade:
    def __init__(self, pair, trade_id, opened, profit_ratio):
        self.pair = pair
        self.id = trade_id
        self.open_date_utc = opened
        self._profit_ratio = profit_ratio

    def calc_profit_ratio(self, _rate):
        return self._profit_ratio


class EntryBlockObservabilityTest(unittest.TestCase):
    def test_short_candidate_snapshot_uses_causal_boundary(self):
        row = candidate_signal_snapshot(
            {"close": 97, "donchian_low": 98, "atr": 2, "adx": 31}, "short"
        )
        self.assertEqual(row["breakout_distance"], 1)
        self.assertEqual(row["breakout_atr"], 0.5)
        self.assertEqual(row["adx"], 31)

    def test_long_candidate_snapshot_is_symmetric(self):
        row = candidate_signal_snapshot(
            {"close": 103, "donchian_high": 102, "atr": 4, "adx": 35}, "long"
        )
        self.assertEqual(row["breakout_distance"], 1)
        self.assertEqual(row["breakout_atr"], 0.25)

    def test_occupied_state_records_age_and_current_r(self):
        now = datetime(2026, 8, 3, 12, tzinfo=timezone.utc)
        trades = [FakeTrade("BTC/USDT:USDT", 7, now - timedelta(hours=30), -0.025)]
        rows = occupied_trade_snapshots(
            trades,
            current_time=now,
            mark_lookup=lambda _pair: 60000,
            initial_risk=0.05,
        )
        self.assertEqual(rows[0]["age_h"], 30)
        self.assertEqual(rows[0]["current_r"], -0.5)

    def test_shadow_replacement_requires_strong_candidate_and_weak_incumbent(self):
        decision = replacement_shadow_decision(
            {"breakout_atr": 0.2, "adx": 34},
            [
                {"pair": "BTC/USDT:USDT", "trade_id": 1, "current_r": 0.4},
                {"pair": "ETH/USDT:USDT", "trade_id": 2, "current_r": -0.2},
            ],
            min_breakout_atr=0.1,
            min_adx=30,
            max_weakest_r=0,
        )
        self.assertTrue(decision["eligible"])
        self.assertEqual(decision["weakest"]["trade_id"], 2)

    def test_shadow_replacement_fails_closed_on_missing_r(self):
        decision = replacement_shadow_decision(
            {"breakout_atr": 0.2, "adx": 34},
            [{"pair": "BTC/USDT:USDT", "trade_id": 1, "current_r": None}],
            min_breakout_atr=0.1,
            min_adx=30,
            max_weakest_r=0,
        )
        self.assertFalse(decision["eligible"])
        self.assertEqual(decision["reason"], "occupied_snapshot_incomplete")


if __name__ == "__main__":
    unittest.main()
