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


SCRIPTS_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(SCRIPTS_DIR))

import unfilled_entry_opp_cost as mod  # noqa: E402


CANCEL_LINE = (
    "2026-07-15 12:40:12,556 - freqtrade.freqtradebot - INFO - Sell order fully "
    "cancelled. Removing Trade(id=12, pair=POWER/USDT:USDT, amount=0, is_short=True, "
    "leverage=2, open_rate=0.0786, open_since=2026-07-15 12:30:09) from database."
)


class ParseCancelledTest(unittest.TestCase):
    """The fill-rate denominator is only truthful if deleted timeouts are recovered."""

    def test_recovers_side_pair_and_submission_time(self):
        (event,) = mod.parse_cancelled(CANCEL_LINE)
        self.assertEqual(event["pair"], "POWER/USDT:USDT")
        self.assertEqual(event["side"], "short")
        self.assertEqual(event["intended_rate"], 0.0786)
        # The counterfactual anchors on submission, not on the cancel 10 minutes later.
        self.assertEqual(
            event["order_ts"], datetime(2026, 7, 15, 12, 30, 9, tzinfo=timezone.utc)
        )
        self.assertEqual(
            event["cancel_ts"], datetime(2026, 7, 15, 12, 40, 12, tzinfo=timezone.utc)
        )

    def test_ignores_partially_filled_cancellations(self):
        """amount>0 means the entry did fill — it is not a missed opportunity."""
        partial = CANCEL_LINE.replace("amount=0,", "amount=3572,")
        self.assertEqual(mod.parse_cancelled(partial), [])

    def test_ignores_trailing_stop_replacement_noise(self):
        line = (
            "2026-07-27 23:00:22,337 - freqtrade.freqtradebot - INFO - Cancelling current "
            "stoploss on exchange for pair ADA/USDT:USDT (orderid:dry_run_buy_x) in order "
            "to add another one ..."
        )
        self.assertEqual(mod.parse_cancelled(line), [])


class AnchorTest(unittest.TestCase):
    def test_floor5_snaps_to_containing_candle(self):
        """Backtest enters at the candle open, so the counterfactual must too."""
        self.assertEqual(
            mod.floor5(datetime(2026, 7, 15, 12, 34, 9, tzinfo=timezone.utc)),
            datetime(2026, 7, 15, 12, 30, 0, tzinfo=timezone.utc),
        )


class PathMetricsTest(unittest.TestCase):
    @staticmethod
    def _klines(rows):
        return [
            {"ts": datetime(2026, 7, 15, 12, m, tzinfo=timezone.utc), "open": o, "high": h, "low": lo, "close": c}
            for m, o, h, lo, c in rows
        ]

    def test_short_favourable_excursion_is_downward(self):
        klines = self._klines([(30, 100.0, 101.0, 96.0, 97.0), (35, 97.0, 98.0, 95.0, 95.5)])
        metrics = mod.path_metrics(
            klines, datetime(2026, 7, 15, 12, 30, tzinfo=timezone.utc), "short", 6
        )
        self.assertAlmostEqual(metrics["mfe"], 0.05)  # low 95 vs entry 100
        self.assertAlmostEqual(metrics["mae"], 0.01)  # high 101 vs entry 100
        self.assertAlmostEqual(metrics["end_ret"], 0.045)

    def test_stop_hit_uses_price_distance_not_equity(self):
        """2x leverage: the -5% equity stop is 2.5% of price."""
        klines = self._klines([(30, 100.0, 102.6, 99.0, 102.0)])
        metrics = mod.path_metrics(
            klines, datetime(2026, 7, 15, 12, 30, tzinfo=timezone.utc), "short", 6
        )
        self.assertTrue(metrics["stop_hit"])
        self.assertFalse(metrics["trail_armed"])

    def test_returns_none_when_window_has_no_candles(self):
        """A miss too recent for a full horizon must not be scored as flat."""
        klines = self._klines([(30, 100.0, 101.0, 99.0, 100.0)])
        self.assertIsNone(
            mod.path_metrics(
                klines, datetime(2026, 7, 16, 12, 30, tzinfo=timezone.utc), "short", 6
            )
        )


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