# -*- coding: utf-8 -*-
"""Unit tests for backtest engine."""

import math
import unittest
from dataclasses import dataclass
from datetime import date, timedelta

from src.core.backtest_engine import BacktestEngine, EvaluationConfig


@dataclass
class Bar:
    date: date
    high: float
    low: float
    close: float


class BacktestEngineTestCase(unittest.TestCase):
    def _bars(self, start: date, closes, highs=None, lows=None):
        highs = highs or closes
        lows = lows or closes
        bars = []
        for i, c in enumerate(closes):
            bars.append(Bar(date=start + timedelta(days=i + 1), high=highs[i], low=lows[i], close=c))
        return bars

    def test_buy_win_when_up(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [102, 104, 105], highs=[103, 105, 106], lows=[101, 103, 104])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertEqual(res["eval_status"], "completed")
        self.assertEqual(res["outcome"], "win")
        self.assertTrue(res["direction_correct"])  # up

    def test_sell_win_when_down_cash(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [98, 97, 96], highs=[99, 98, 97], lows=[97, 96, 95])
        res = BacktestEngine.evaluate_single(
            operation_advice="卖出",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertEqual(res["position_recommendation"], "cash")
        self.assertEqual(res["outcome"], "win")
        self.assertEqual(res["simulated_return_pct"], 0.0)
        self.assertEqual(res["first_hit"], "not_applicable")

    def test_wait_maps_to_cash_and_flat_direction(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        # Stock drops ~5%: AI said wait (neutral), stock moved significantly → loss
        bars = self._bars(date(2024, 1, 1), [98, 96, 95], highs=[99, 97, 96], lows=[97, 95, 94])
        res = BacktestEngine.evaluate_single(
            operation_advice="观望",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertEqual(res["position_recommendation"], "cash")
        self.assertEqual(res["direction_expected"], "flat")
        self.assertEqual(res["outcome"], "loss")

    def test_bearish_like_phrases_match_keyword_substring(self):
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("建议买入"),
            "long",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("继续持有"),
            "not_down",
        )
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("建议持有"),
            "long",
        )
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("建议洗盘观察"),
            "long",
        )

    def test_range_bound_watch_is_treated_as_hold_long_path(self):
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("震荡观望"),
            "long",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("Range-bound watch"),
            "not_down",
        )
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("Range-bound watch"),
            "long",
        )

    def test_shakeout_watch_is_treated_as_hold_long_path(self):
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("洗盘观察"),
            "long",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("Shakeout watch"),
            "not_down",
        )
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("Hold and watch"),
            "long",
        )

    def test_hold_win_when_flat(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [100.5, 100.2, 101], highs=[101, 101, 101], lows=[99.8, 99.9, 100])
        res = BacktestEngine.evaluate_single(
            operation_advice="持有",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["outcome"], "win")

    def test_hold_win_when_up(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [102, 103, 104], highs=[103, 104, 105], lows=[101, 102, 103])
        res = BacktestEngine.evaluate_single(
            operation_advice="持有",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["outcome"], "win")

    def test_decision_signal_helper_classifies_structured_up_not_down_and_not_up(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0, engine_version="decision-signal-v1")

        up = BacktestEngine.evaluate_decision_signal(
            direction_expected="up",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=self._bars(date(2024, 1, 1), [101, 102, 103]),
            config=cfg,
        )
        not_down = BacktestEngine.evaluate_decision_signal(
            direction_expected="not_down",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=self._bars(date(2024, 1, 1), [99.5, 99, 99]),
            config=cfg,
        )
        not_up = BacktestEngine.evaluate_decision_signal(
            direction_expected="not_up",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=self._bars(date(2024, 1, 1), [100.5, 101, 101.5]),
            config=cfg,
        )
        not_up_miss = BacktestEngine.evaluate_decision_signal(
            direction_expected="not_up",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=self._bars(date(2024, 1, 1), [101, 102, 103]),
            config=cfg,
        )

        self.assertEqual(up["outcome"], "hit")
        self.assertEqual(not_down["outcome"], "neutral")
        self.assertEqual(not_up["outcome"], "hit")
        self.assertEqual(not_up_miss["outcome"], "miss")

    def test_decision_signal_helper_rejects_non_finite_prices(self):
        cfg = EvaluationConfig(eval_window_days=1, neutral_band_pct=2.0, engine_version="decision-signal-v1")

        bad_start = BacktestEngine.evaluate_decision_signal(
            direction_expected="up",
            anchor_date=date(2024, 1, 1),
            start_price=math.nan,
            forward_bars=self._bars(date(2024, 1, 1), [103]),
            config=cfg,
        )
        bad_end = BacktestEngine.evaluate_decision_signal(
            direction_expected="up",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=[Bar(date=date(2024, 1, 2), high=101, low=99, close=math.nan)],
            config=cfg,
        )
        bad_bounds = BacktestEngine.evaluate_decision_signal(
            direction_expected="up",
            anchor_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=[Bar(date=date(2024, 1, 2), high=math.inf, low=-math.inf, close=103)],
            config=cfg,
        )

        self.assertEqual(bad_start["eval_status"], "unable")
        self.assertEqual(bad_start["unable_reason"], "invalid_anchor_price")
        self.assertEqual(bad_end["eval_status"], "unable")
        self.assertEqual(bad_end["unable_reason"], "invalid_end_close")
        self.assertEqual(bad_bounds["eval_status"], "completed")
        self.assertEqual(bad_bounds["stock_return_pct"], 3.0)
        self.assertIsNone(bad_bounds["max_high"])
        self.assertIsNone(bad_bounds["min_low"])

    def test_decision_signal_helper_does_not_change_evaluate_single_hold_behavior(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        res = BacktestEngine.evaluate_single(
            operation_advice="持有",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=self._bars(date(2024, 1, 1), [100.2, 100.4, 100.6]),
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )

        self.assertEqual(res["direction_expected"], "not_down")
        self.assertEqual(res["outcome"], "win")

    def test_stop_loss_hit_first(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [99, 98, 97], highs=[101, 100, 99], lows=[94, 97, 96])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertTrue(res["hit_stop_loss"])
        self.assertEqual(res["first_hit"], "stop_loss")
        self.assertEqual(res["simulated_exit_reason"], "stop_loss")

    def test_take_profit_hit_first(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [105, 106, 107], highs=[111, 107, 108], lows=[103, 105, 106])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertTrue(res["hit_take_profit"])
        self.assertEqual(res["first_hit"], "take_profit")
        self.assertEqual(res["simulated_exit_reason"], "take_profit")

    def test_ambiguous_same_day(self):
        cfg = EvaluationConfig(eval_window_days=2, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [100, 100], highs=[111, 100], lows=[94, 99])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=95,
            take_profit=110,
            config=cfg,
        )
        self.assertEqual(res["first_hit"], "ambiguous")
        self.assertEqual(res["simulated_exit_reason"], "ambiguous_stop_loss")

    def test_buy_loss_when_down(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [98, 96, 95], highs=[99, 97, 96], lows=[97, 95, 94])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=93,
            take_profit=110,
            config=cfg,
        )
        self.assertEqual(res["eval_status"], "completed")
        self.assertEqual(res["outcome"], "loss")
        self.assertFalse(res["direction_correct"])

    def test_hold_loss_when_down(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [98, 96, 95], highs=[99, 97, 96], lows=[97, 95, 94])
        res = BacktestEngine.evaluate_single(
            operation_advice="持有",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["direction_expected"], "not_down")
        self.assertEqual(res["outcome"], "loss")
        self.assertFalse(res["direction_correct"])

    def test_sell_loss_when_up(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [102, 104, 106], highs=[103, 105, 107], lows=[101, 103, 105])
        res = BacktestEngine.evaluate_single(
            operation_advice="卖出",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["position_recommendation"], "cash")
        self.assertEqual(res["direction_expected"], "down")
        self.assertEqual(res["outcome"], "loss")
        self.assertFalse(res["direction_correct"])

    def test_neutral_outcome(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [100.5, 100.2, 100.8], highs=[101, 101, 101], lows=[100, 100, 100])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["direction_expected"], "up")
        self.assertEqual(res["outcome"], "neutral")
        self.assertIsNone(res["direction_correct"])

    def test_direction_correct_false_buy_down(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [97, 95, 94], highs=[98, 96, 95], lows=[96, 94, 93])
        res = BacktestEngine.evaluate_single(
            operation_advice="buy",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["direction_expected"], "up")
        self.assertEqual(res["outcome"], "loss")
        self.assertFalse(res["direction_correct"])

    def test_insufficient_data(self):
        cfg = EvaluationConfig(eval_window_days=5, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [100, 101])
        res = BacktestEngine.evaluate_single(
            operation_advice="买入",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["eval_status"], "insufficient_data")

    def test_unrecognized_advice_defaults_to_cash(self):
        cfg = EvaluationConfig(eval_window_days=3, neutral_band_pct=2.0)
        bars = self._bars(date(2024, 1, 1), [102, 104, 105], highs=[103, 105, 106], lows=[101, 103, 104])
        res = BacktestEngine.evaluate_single(
            operation_advice="some gibberish text",
            analysis_date=date(2024, 1, 1),
            start_price=100,
            forward_bars=bars,
            stop_loss=None,
            take_profit=None,
            config=cfg,
        )
        self.assertEqual(res["position_recommendation"], "cash")
        self.assertEqual(res["direction_expected"], "flat")

    def test_none_empty_advice_defaults_to_cash(self):
        for advice in [None, "", "   "]:
            pos = BacktestEngine.infer_position_recommendation(advice)
            direction = BacktestEngine.infer_direction_expected(advice)
            self.assertEqual(pos, "cash", f"Expected cash for advice={advice!r}")
            self.assertEqual(direction, "flat", f"Expected flat for advice={advice!r}")

    def test_negated_sell_not_classified_bearish(self):
        # "do not sell" negates "sell" — should NOT be direction=down
        self.assertNotEqual(BacktestEngine.infer_direction_expected("do not sell"), "down")

    def test_chinese_negated_sell_not_bearish(self):
        # "不要卖出" = "don't sell" — should NOT be direction=down
        self.assertNotEqual(BacktestEngine.infer_direction_expected("不要卖出"), "down")

    def test_conditional_support_phrase_not_negating_hold(self):
        # "不跌破支撑继续持有" means conditional support hold, not explicit negation of hold.
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("不跌破支撑继续持有"),
            "long",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("不跌破支撑继续持有"),
            "not_down",
        )

    def test_wait_then_buy_classified_as_cash(self):
        # "wait" matches first in priority order → cash
        pos = BacktestEngine.infer_position_recommendation("wait for a dip then buy")
        self.assertEqual(pos, "cash")

    def test_wait_phrase_before_bullish_phrases_stays_wait(self):
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("先观望再买入"),
            "cash",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("先观望再买入"),
            "flat",
        )
        self.assertEqual(
            BacktestEngine.infer_position_recommendation("观望后买入"),
            "cash",
        )
        self.assertEqual(
            BacktestEngine.infer_direction_expected("观望后买入"),
            "flat",
        )


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