"""Property-based tests for the Micro-Trading Module.

Feature: autonomous-trading-engine

Property 31: Micro-trade parameter constraints.
Property 32: Micro-trade auto-close after max hold duration.
Property 34: Micro-trades respect all existing constraints.
"""
from __future__ import annotations

from hypothesis import given, settings
from hypothesis import strategies as st

from services.trading.micro_trading import MicroTradeConfig, MicroTradingModule

# ---------------------------------------------------------------------------
# Hypothesis strategies
# ---------------------------------------------------------------------------


def _micro_config_strategy() -> st.SearchStrategy[MicroTradeConfig]:
    """Generate random MicroTradeConfig objects with valid ranges."""
    return st.builds(
        MicroTradeConfig,
        enabled=st.just(True),
        allocation_cap_pct=st.floats(
            min_value=0.01, max_value=0.10, allow_nan=False, allow_infinity=False,
        ),
        max_daily=st.integers(min_value=1, max_value=50),
        max_hold_minutes=st.integers(min_value=10, max_value=480),
        stop_loss_atr_multiplier=st.just(1.0),
        reward_risk_ratio=st.just(1.5),
    )


# ---------------------------------------------------------------------------
# Property 31: Micro-trade parameter constraints
# **Validates: Requirements 20.3, 20.4, 20.5**
# ---------------------------------------------------------------------------


class TestProperty31MicroTradeParameterConstraints:
    """Property 31: Micro-trade parameter constraints.

    Verify allocation does not exceed micro_trading_allocation_cap_pct.
    Verify daily count does not exceed configured maximum.

    **Validates: Requirements 20.3, 20.4, 20.5**
    """

    module = MicroTradingModule()

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        active_pool=st.floats(
            min_value=100.0, max_value=100_000.0,
            allow_nan=False, allow_infinity=False,
        ),
    )
    def test_allocation_does_not_exceed_cap(
        self,
        config: MicroTradeConfig,
        active_pool: float,
    ) -> None:
        """Micro-trade allocation never exceeds allocation_cap_pct * active_pool."""
        cap = self.module.compute_allocation_cap(config, active_pool)
        expected = config.allocation_cap_pct * active_pool

        assert abs(cap - expected) < 1e-6
        assert cap <= config.allocation_cap_pct * active_pool + 1e-6

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        daily_count=st.integers(min_value=0, max_value=100),
    )
    def test_daily_count_enforced(
        self,
        config: MicroTradeConfig,
        daily_count: int,
    ) -> None:
        """should_evaluate returns False when daily count >= max_daily."""
        result = self.module.should_evaluate(config, daily_count)

        if daily_count >= config.max_daily:
            assert result is False
        else:
            assert result is True

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
    )
    def test_stop_loss_at_1x_atr(
        self,
        config: MicroTradeConfig,
    ) -> None:
        """Micro-trade stop-loss uses 1.0x ATR multiplier."""
        assert config.stop_loss_atr_multiplier == 1.0

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
    )
    def test_take_profit_at_1_5x_stop_distance(
        self,
        config: MicroTradeConfig,
    ) -> None:
        """Micro-trade take-profit uses 1.5x reward-risk ratio."""
        assert config.reward_risk_ratio == 1.5

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        active_pool=st.floats(
            min_value=100.0, max_value=100_000.0,
            allow_nan=False, allow_infinity=False,
        ),
    )
    def test_allocation_cap_scales_with_active_pool(
        self,
        config: MicroTradeConfig,
        active_pool: float,
    ) -> None:
        """Allocation cap is proportional to active pool."""
        cap = self.module.compute_allocation_cap(config, active_pool)
        # Doubling active pool should double the cap
        cap_double = self.module.compute_allocation_cap(config, active_pool * 2)
        assert abs(cap_double - cap * 2) < 1e-4


# ---------------------------------------------------------------------------
# Property 32: Micro-trade auto-close after max hold duration
# **Validates: Requirements 20.6**
# ---------------------------------------------------------------------------


class TestProperty32MicroTradeAutoClose:
    """Property 32: Micro-trade auto-close after max hold duration.

    Verify positions closed when hold exceeds max duration.
    Verify positions not closed when hold is within duration.

    **Validates: Requirements 20.6**
    """

    module = MicroTradingModule()

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        hold_minutes=st.floats(
            min_value=0.0, max_value=1000.0,
            allow_nan=False, allow_infinity=False,
        ),
    )
    def test_auto_close_when_exceeds_max_hold(
        self,
        config: MicroTradeConfig,
        hold_minutes: float,
    ) -> None:
        """Position auto-closed when hold_minutes > max_hold_minutes."""
        result = self.module.should_auto_close(config, hold_minutes)

        if hold_minutes > config.max_hold_minutes:
            assert result is True
        else:
            assert result is False

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
    )
    def test_not_closed_at_exact_max(
        self,
        config: MicroTradeConfig,
    ) -> None:
        """Position is NOT auto-closed at exactly max_hold_minutes."""
        result = self.module.should_auto_close(config, float(config.max_hold_minutes))
        assert result is False

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
    )
    def test_closed_just_past_max(
        self,
        config: MicroTradeConfig,
    ) -> None:
        """Position IS auto-closed just past max_hold_minutes."""
        result = self.module.should_auto_close(
            config, float(config.max_hold_minutes) + 0.01,
        )
        assert result is True


# ---------------------------------------------------------------------------
# Property 34: Micro-trades respect all existing constraints
# **Validates: Requirements 20.10**
# ---------------------------------------------------------------------------


class TestProperty34MicroTradesRespectConstraints:
    """Property 34: Micro-trades respect all existing constraints.

    Verify trading window, circuit breakers, portfolio heat constraints
    all enforced for micro-trades.

    **Validates: Requirements 20.10**
    """

    module = MicroTradingModule()

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        daily_count=st.integers(min_value=0, max_value=5),
    )
    def test_circuit_breaker_blocks_micro_trade(
        self,
        config: MicroTradeConfig,
        daily_count: int,
    ) -> None:
        """Micro-trades blocked when circuit breaker is active."""
        allowed, reason = self.module.check_constraints(
            config=config,
            daily_count=daily_count,
            is_within_window=True,
            circuit_breaker_active=True,
            portfolio_heat_pct=0.05,
            max_heat=0.20,
        )
        assert allowed is False
        assert reason == "circuit_breaker_active"

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        daily_count=st.integers(min_value=0, max_value=5),
    )
    def test_outside_trading_window_blocks_micro_trade(
        self,
        config: MicroTradeConfig,
        daily_count: int,
    ) -> None:
        """Micro-trades blocked outside trading window."""
        allowed, reason = self.module.check_constraints(
            config=config,
            daily_count=daily_count,
            is_within_window=False,
            circuit_breaker_active=False,
            portfolio_heat_pct=0.05,
            max_heat=0.20,
        )
        assert allowed is False
        assert reason == "outside_trading_window"

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        heat_pct=st.floats(
            min_value=0.20, max_value=1.0,
            allow_nan=False, allow_infinity=False,
        ),
    )
    def test_portfolio_heat_blocks_micro_trade(
        self,
        config: MicroTradeConfig,
        heat_pct: float,
    ) -> None:
        """Micro-trades blocked when portfolio heat exceeds max."""
        max_heat = 0.20
        allowed, reason = self.module.check_constraints(
            config=config,
            daily_count=0,
            is_within_window=True,
            circuit_breaker_active=False,
            portfolio_heat_pct=heat_pct,
            max_heat=max_heat,
        )
        assert allowed is False
        assert reason == "portfolio_heat_exceeded"

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
    )
    def test_daily_limit_blocks_micro_trade(
        self,
        config: MicroTradeConfig,
    ) -> None:
        """Micro-trades blocked when daily limit reached."""
        allowed, reason = self.module.check_constraints(
            config=config,
            daily_count=config.max_daily,
            is_within_window=True,
            circuit_breaker_active=False,
            portfolio_heat_pct=0.05,
            max_heat=0.20,
        )
        assert allowed is False
        assert reason == "daily_limit_reached"

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        daily_count=st.integers(min_value=0, max_value=5),
    )
    def test_disabled_blocks_micro_trade(
        self,
        config: MicroTradeConfig,
        daily_count: int,
    ) -> None:
        """Micro-trades blocked when module is disabled."""
        disabled_config = MicroTradeConfig(
            enabled=False,
            allocation_cap_pct=config.allocation_cap_pct,
            max_daily=config.max_daily,
            max_hold_minutes=config.max_hold_minutes,
        )
        allowed, reason = self.module.check_constraints(
            config=disabled_config,
            daily_count=daily_count,
            is_within_window=True,
            circuit_breaker_active=False,
            portfolio_heat_pct=0.05,
            max_heat=0.20,
        )
        assert allowed is False
        assert reason == "micro_trading_disabled"

    @settings(max_examples=100)
    @given(
        config=_micro_config_strategy(),
        daily_count=st.integers(min_value=0, max_value=5),
        heat_pct=st.floats(
            min_value=0.0, max_value=0.15,
            allow_nan=False, allow_infinity=False,
        ),
    )
    def test_all_constraints_pass_allows_trade(
        self,
        config: MicroTradeConfig,
        daily_count: int,
        heat_pct: float,
    ) -> None:
        """Micro-trade allowed when all constraints pass."""
        # Ensure daily_count is under the limit
        safe_count = min(daily_count, config.max_daily - 1)
        allowed, reason = self.module.check_constraints(
            config=config,
            daily_count=safe_count,
            is_within_window=True,
            circuit_breaker_active=False,
            portfolio_heat_pct=heat_pct,
            max_heat=0.20,
        )
        assert allowed is True
        assert reason == "ok"
