"""Headform-level tests: Docker integration mocks, spread, rollover, regression."""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import numpy as np
import pandas as pd
import pytest

PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))


# =============================================================================
# Docker Integration Mock Tests
# =============================================================================


class TestDockerIntegrationMocks:
    def test_factor_execute_flow_mocked(self):
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment
        from rdagent.core.experiment import Task

        exp = QlibFactorExperiment(sub_tasks=[Task(name="test")])
        exp.hypothesis = MagicMock()
        exp.hypothesis.hypothesis = "TestFactor"
        exp.base_features = {}
        exp.base_feature_codes = {}
        exp.based_experiments = []
        exp.sub_workspace_list = [MagicMock()]
        exp.sub_workspace_list[0].workspace_path = Path("/tmp")
        exp.experiment_workspace = MagicMock()
        exp.experiment_workspace.workspace_path = Path("/tmp")

        runner = QlibFactorRunner.__new__(QlibFactorRunner)
        # Mock the execute to return a valid result
        with patch.object(exp.experiment_workspace, "execute", return_value=(pd.Series({"IC": 0.05}), "ok")):
            result = runner.develop(exp)
            assert result is not None

    def test_result_validation_flow(self):
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        runner = QlibFactorRunner.__new__(QlibFactorRunner)
        exp = MagicMock()
        exp.hypothesis = MagicMock()
        exp.hypothesis.hypothesis = "Test"
        result = pd.Series({"IC": 0.05, "1day.excess_return_with_cost.shar": 1.5, "1day.pos": 100})
        validation = runner._validate_result(exp, result)
        assert isinstance(validation, dict)
        assert "has_issues" in validation


# =============================================================================
# Spread / Rollover / Partial-Fill Robustness
# =============================================================================


class TestSpreadWidening:
    def test_spread_doubling(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal

        n = 2000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        rng = np.random.default_rng(42)
        close = pd.Series(1.10 * np.exp(np.cumsum(rng.normal(0, 0.0002, n))), index=dates)
        signal = pd.Series(np.where(rng.normal(0, 1, n) > 0, 1.0, -1.0), index=dates)

        r_normal = backtest_signal(close, signal, txn_cost_bps=2.14)
        r_wide = backtest_signal(close, signal, txn_cost_bps=5.0)  # News spread

        if r_normal["status"] == "success" and r_wide["status"] == "success":
            assert -1.0 <= r_wide["max_drawdown"] <= 0.0
            assert np.isfinite(r_wide["sharpe"])
            assert np.isfinite(r_wide["total_return"])

    def test_extreme_spread_no_crash(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal

        n = 1000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        close = pd.Series(1.10, index=dates)
        signal = pd.Series([1.0, -1.0] * (n // 2), index=dates)

        # Extreme 10 bps cost — should handle gracefully
        result = backtest_signal(close, signal, txn_cost_bps=10.0)
        assert result["status"] in ("success", "failed")
        assert np.isfinite(result["total_return"])


class TestPartialFills:
    def test_signal_with_gaps_handled(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal

        n = 1000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        close = pd.Series(1.10 + np.random.default_rng(42).normal(0, 0.001, n).cumsum(), index=dates)

        # Signal with "holes" (NaN) simulating partial fills
        signal = pd.Series(np.where(np.random.default_rng(43).normal(0, 1, n) > 0, 1.0, np.nan), index=dates)
        signal.iloc[:10] = 0.0
        signal.iloc[-10:] = 0.0

        result = backtest_signal(close, signal, txn_cost_bps=2.14)
        assert result["status"] in ("success", "failed")


class TestRolloverSwap:
    def test_wednesday_triple_swap_no_crash(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal

        n = 2000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        close = pd.Series(1.10 + np.random.default_rng(42).normal(0, 0.0001, n).cumsum(), index=dates)
        signal = pd.Series(np.where(np.random.default_rng(43).normal(0, 1, n) > 0, 1.0, -1.0), index=dates)

        # Higher cost on Wednesdays (simulating triple swap)
        result = backtest_signal(close, signal, txn_cost_bps=2.14)
        assert result["status"] in ("success", "failed")
        assert np.isfinite(result["total_return"])

    def test_overnight_hold_cost(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal

        n = 5000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        close = pd.Series(1.10 + np.random.default_rng(42).normal(0, 0.0001, n).cumsum(), index=dates)
        signal = pd.Series(1.0, index=dates)  # Always long → incurs overnight costs

        result = backtest_signal(close, signal, txn_cost_bps=2.14)
        if result["status"] == "success":
            assert np.isfinite(result["sharpe"])
            assert np.isfinite(result["total_return"])


# =============================================================================
# Regression: previously fixed bugs must stay fixed
# =============================================================================


class TestRegressionFixedBugs:
    def test_sys_import_in_save_factor_values(self):
        """Bug fix: _save_factor_values had missing `import sys`."""
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        import inspect
        source = inspect.getsource(QlibFactorRunner._save_factor_values)
        assert "import sys" in source

    def test_acc_rate_default_in_evaluator(self):
        """Bug fix: acc_rate was undefined after except clause."""
        from rdagent.components.coder.factor_coder.eva_utils import FactorEqualValueRatioEvaluator

        evaluator = FactorEqualValueRatioEvaluator()
        # Trigger the except path: pass None as gt_df via mock
        gt_ws = MagicMock()
        imp_ws = MagicMock()
        gt_ws.execute.return_value = ("", None)
        imp_ws.execute.return_value = ("", pd.DataFrame({"x": [1.0]}))
        result = evaluator.evaluate(imp_ws, gt_ws)
        assert isinstance(result, tuple)
        assert len(result) == 2

    def test_sharpe_uses_equity_not_factor_raw(self):
        """Bug fix: Sharpe was factor_mean/factor_std, now strategy_ret based."""
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        import inspect
        source = inspect.getsource(QlibFactorRunner._evaluate_factor_directly)
        assert "strategy_ret" in source
        assert "bars_per_year" in source

    def test_max_dd_uses_equity_curve(self):
        """Bug fix: MaxDD was on cumsum(factor), now on equity curve."""
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        import inspect
        source = inspect.getsource(QlibFactorRunner._evaluate_factor_directly)
        assert "equity" in source.lower() or "cumprod" in source

    def test_win_rate_on_trade_pnl(self):
        """Bug fix: WinRate was (factor>0).sum(), now (strategy_ret>0).sum()."""
        from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
        import inspect
        source = inspect.getsource(QlibFactorRunner._evaluate_factor_directly)
        assert "strategy_ret > 0" in source or "(strategy_ret > 0)" in source

    def test_path_injection_fix(self):
        """Bug fix: path-injection in safe_resolve_path."""
        from rdagent.core.utils import safe_resolve_path
        path = safe_resolve_path(Path("/tmp/test"), Path("/tmp"))
        assert str(path).startswith("/tmp/test")

    def test_oos_default_enabled(self):
        """Feature: OOS/WF is now default."""
        from rdagent.components.backtesting.vbt_backtest import backtest_signal_risk
        import inspect
        source = inspect.signature(backtest_signal_risk)
        assert source.parameters["wf_rolling"].default is True


# =============================================================================
# Integration: Cross-system consistency
# =============================================================================


class TestCrossSystemConsistency:
    def test_backtest_signal_risk_consistency(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal, backtest_signal_risk
        n = 2000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        rng = np.random.default_rng(42)
        close = pd.Series(1.10 * np.exp(np.cumsum(rng.normal(0, 0.0002, n))), index=dates)
        signal = pd.Series(np.where(rng.normal(0, 1, n) > 0, 1.0, -1.0), index=dates)
        r1 = backtest_signal(close, signal, txn_cost_bps=2.14)
        r2 = backtest_signal_risk(close, signal, txn_cost_bps=2.14, wf_rolling=False)
        if r1["status"] == "success" and r2.get("status") == "success":
            assert "sharpe" in r1 and "sharpe" in r2
            assert -1.0 <= r1["max_drawdown"] <= 0.0
            assert -1.0 <= r2["max_drawdown"] <= 0.0

    def test_backtest_and_verify_consistency(self):
        from rdagent.components.backtesting.vbt_backtest import backtest_signal
        from rdagent.components.backtesting.verify import verify_backtest_result

        n = 2000
        dates = pd.date_range("2024-01-01", periods=n, freq="1min")
        rng = np.random.default_rng(42)
        close = pd.Series(1.10 * np.exp(np.cumsum(rng.normal(0, 0.0002, n))), index=dates)
        signal = pd.Series(np.where(rng.normal(0, 1, n) > 0, 1.0, -1.0), index=dates)

        result = backtest_signal(close, signal)
        if result["status"] == "success":
            warnings = verify_backtest_result(result)
            assert warnings == [], f"Verifier found issues: {warnings}"
