"""Property-based tests for the Stop-Loss Manager.

Feature: autonomous-trading-engine

Tests properties 9, 10, 11, 15, and 25 from the design specification,
covering initial stop/take-profit computation, price crossing triggers,
trailing stop activation, high-severity event tightening, and proactive
heat-based stop tightening.
"""
from __future__ import annotations

from datetime import datetime, timezone

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

from services.trading.models import (
    OpenPosition,
    RiskTierConfig,
    StopLevels,
)
from services.trading.stop_loss_manager import StopLossManager

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


def _risk_tier_config_strategy() -> st.SearchStrategy[RiskTierConfig]:
    """Generate random RiskTierConfig objects with valid parameter ranges."""
    return st.builds(
        RiskTierConfig,
        name=st.sampled_from(["conservative", "moderate", "aggressive"]),
        min_confidence=st.floats(min_value=0.10, max_value=0.95, allow_nan=False, allow_infinity=False),
        max_position_pct=st.floats(min_value=0.02, max_value=0.50, allow_nan=False, allow_infinity=False),
        stop_loss_atr_multiplier=st.floats(min_value=0.5, max_value=5.0, allow_nan=False, allow_infinity=False),
        reward_risk_ratio=st.floats(min_value=0.5, max_value=5.0, allow_nan=False, allow_infinity=False),
        max_sector_pct=st.floats(min_value=0.05, max_value=0.60, allow_nan=False, allow_infinity=False),
        max_portfolio_heat=st.floats(min_value=0.05, max_value=0.50, allow_nan=False, allow_infinity=False),
    )


def _open_position_strategy(
    ticker: st.SearchStrategy[str] | None = None,
    entry_price: st.SearchStrategy[float] | None = None,
    signal_confidence: st.SearchStrategy[float] | None = None,
) -> st.SearchStrategy[OpenPosition]:
    """Generate random OpenPosition objects."""
    return st.builds(
        OpenPosition,
        ticker=ticker if ticker is not None else st.from_regex(r"[A-Z]{3,5}", fullmatch=True),
        quantity=st.integers(min_value=1, max_value=100),
        entry_price=entry_price if entry_price is not None else st.floats(
            min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False,
        ),
        current_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        unrealized_pnl=st.floats(min_value=-1000.0, max_value=1000.0, allow_nan=False, allow_infinity=False),
        market_value=st.floats(min_value=10.0, max_value=5000.0, allow_nan=False, allow_infinity=False),
        sector=st.sampled_from(["Technology", "Healthcare", "Energy", "Financials", "Consumer"]),
        stop_loss_price=st.floats(min_value=1.0, max_value=400.0, allow_nan=False, allow_infinity=False),
        take_profit_price=st.floats(min_value=10.0, max_value=1000.0, allow_nan=False, allow_infinity=False),
        signal_confidence=signal_confidence if signal_confidence is not None else st.floats(
            min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False,
        ),
        is_micro_trade=st.just(False),
    )


# ---------------------------------------------------------------------------
# Property 9: Stop-loss and take-profit initial computation
# **Validates: Requirements 4.1, 4.2**
# ---------------------------------------------------------------------------


class TestProperty9InitialComputation:
    """Property 9: Stop-loss and take-profit initial computation.

    **Validates: Requirements 4.1, 4.2**
    """

    manager = StopLossManager()

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_stop_loss_equals_entry_minus_atr_times_multiplier(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Stop-loss = entry_price - (ATR * stop_loss_atr_multiplier)."""
        levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        expected_stop = entry_price - (atr * risk_tier.stop_loss_atr_multiplier)
        assert abs(levels.stop_loss_price - expected_stop) < 1e-9, (
            f"stop_loss {levels.stop_loss_price} != expected {expected_stop}"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_stop_loss_always_below_entry(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Stop-loss is always below entry price (for positive ATR and multiplier)."""
        assume(atr * risk_tier.stop_loss_atr_multiplier > 0)
        levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        assert levels.stop_loss_price < entry_price, (
            f"stop_loss {levels.stop_loss_price} >= entry {entry_price}"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_take_profit_equals_entry_plus_stop_distance_times_ratio(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Take-profit = entry_price + (stop_distance * reward_risk_ratio)."""
        levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        stop_distance = atr * risk_tier.stop_loss_atr_multiplier
        expected_tp = entry_price + (stop_distance * risk_tier.reward_risk_ratio)
        assert abs(levels.take_profit_price - expected_tp) < 1e-9, (
            f"take_profit {levels.take_profit_price} != expected {expected_tp}"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_take_profit_always_above_entry(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Take-profit is always above entry price."""
        assume(atr * risk_tier.stop_loss_atr_multiplier * risk_tier.reward_risk_ratio > 0)
        levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        assert levels.take_profit_price > entry_price, (
            f"take_profit {levels.take_profit_price} <= entry {entry_price}"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_trailing_stop_initially_inactive(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Trailing stop is not active on initial computation."""
        levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        assert levels.trailing_stop_active is False



# ---------------------------------------------------------------------------
# Property 10: Price crossing triggers immediate sell
# **Validates: Requirements 4.4, 4.5**
# ---------------------------------------------------------------------------


class TestProperty10PriceCrossingTriggers:
    """Property 10: Price crossing triggers immediate sell.

    **Validates: Requirements 4.4, 4.5**
    """

    manager = StopLossManager()

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False),
        tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False),
    )
    def test_sell_triggered_when_price_at_or_below_stop_loss(
        self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float,
    ) -> None:
        """Sell triggered when current price <= stop_loss."""
        stop_loss = entry_price * (1 - stop_distance_pct)
        take_profit = entry_price * (1 + tp_distance_pct)
        assume(stop_loss > 0)
        assume(take_profit > stop_loss)

        ticker = "TEST"
        position = OpenPosition(
            ticker=ticker, quantity=10, entry_price=entry_price,
            current_price=stop_loss, unrealized_pnl=0.0,
            market_value=entry_price * 10, sector="Technology",
            stop_loss_price=stop_loss, take_profit_price=take_profit,
            signal_confidence=0.7,
        )
        levels = StopLevels(
            stop_loss_price=stop_loss,
            take_profit_price=take_profit,
            trailing_stop_active=False,
            atr_value=1.0,
            atr_multiplier=2.0,
            reward_risk_ratio=1.5,
            last_updated=datetime.now(tz=timezone.utc),
        )

        # Price at stop_loss
        triggers = self.manager.check_price_crossings(
            positions=[position],
            prices={ticker: stop_loss},
            stop_levels={ticker: levels},
        )
        assert len(triggers) == 1
        assert triggers[0].trigger_type == "stop_loss"
        assert triggers[0].ticker == ticker

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False),
        tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False),
        below_pct=st.floats(min_value=0.001, max_value=0.10, allow_nan=False, allow_infinity=False),
    )
    def test_sell_triggered_when_price_below_stop_loss(
        self, entry_price: float, stop_distance_pct: float,
        tp_distance_pct: float, below_pct: float,
    ) -> None:
        """Sell triggered when current price is below stop_loss."""
        stop_loss = entry_price * (1 - stop_distance_pct)
        take_profit = entry_price * (1 + tp_distance_pct)
        price_below = stop_loss * (1 - below_pct)
        assume(stop_loss > 0)
        assume(price_below > 0)
        assume(take_profit > stop_loss)

        ticker = "TEST"
        position = OpenPosition(
            ticker=ticker, quantity=10, entry_price=entry_price,
            current_price=price_below, unrealized_pnl=0.0,
            market_value=entry_price * 10, sector="Technology",
            stop_loss_price=stop_loss, take_profit_price=take_profit,
            signal_confidence=0.7,
        )
        levels = StopLevels(
            stop_loss_price=stop_loss,
            take_profit_price=take_profit,
            trailing_stop_active=False,
            atr_value=1.0,
            atr_multiplier=2.0,
            reward_risk_ratio=1.5,
            last_updated=datetime.now(tz=timezone.utc),
        )

        triggers = self.manager.check_price_crossings(
            positions=[position],
            prices={ticker: price_below},
            stop_levels={ticker: levels},
        )
        assert len(triggers) == 1
        assert triggers[0].trigger_type == "stop_loss"

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False),
        tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False),
    )
    def test_sell_triggered_when_price_at_or_above_take_profit(
        self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float,
    ) -> None:
        """Sell triggered when current price >= take_profit."""
        stop_loss = entry_price * (1 - stop_distance_pct)
        take_profit = entry_price * (1 + tp_distance_pct)
        assume(stop_loss > 0)
        assume(take_profit > stop_loss)

        ticker = "TEST"
        position = OpenPosition(
            ticker=ticker, quantity=10, entry_price=entry_price,
            current_price=take_profit, unrealized_pnl=0.0,
            market_value=entry_price * 10, sector="Technology",
            stop_loss_price=stop_loss, take_profit_price=take_profit,
            signal_confidence=0.7,
        )
        levels = StopLevels(
            stop_loss_price=stop_loss,
            take_profit_price=take_profit,
            trailing_stop_active=False,
            atr_value=1.0,
            atr_multiplier=2.0,
            reward_risk_ratio=1.5,
            last_updated=datetime.now(tz=timezone.utc),
        )

        triggers = self.manager.check_price_crossings(
            positions=[position],
            prices={ticker: take_profit},
            stop_levels={ticker: levels},
        )
        assert len(triggers) == 1
        assert triggers[0].trigger_type == "take_profit"

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        stop_distance_pct=st.floats(min_value=0.05, max_value=0.20, allow_nan=False, allow_infinity=False),
        tp_distance_pct=st.floats(min_value=0.05, max_value=0.40, allow_nan=False, allow_infinity=False),
        between_pct=st.floats(min_value=0.01, max_value=0.99, allow_nan=False, allow_infinity=False),
    )
    def test_no_trigger_when_price_between_levels(
        self, entry_price: float, stop_distance_pct: float,
        tp_distance_pct: float, between_pct: float,
    ) -> None:
        """No trigger when price is strictly between stop_loss and take_profit."""
        stop_loss = entry_price * (1 - stop_distance_pct)
        take_profit = entry_price * (1 + tp_distance_pct)
        assume(stop_loss > 0)
        assume(take_profit > stop_loss + 0.02)

        # Price strictly between stop_loss and take_profit
        price_between = stop_loss + (take_profit - stop_loss) * between_pct
        # Ensure strictly between (not at boundaries)
        assume(price_between > stop_loss)
        assume(price_between < take_profit)

        ticker = "TEST"
        position = OpenPosition(
            ticker=ticker, quantity=10, entry_price=entry_price,
            current_price=price_between, unrealized_pnl=0.0,
            market_value=entry_price * 10, sector="Technology",
            stop_loss_price=stop_loss, take_profit_price=take_profit,
            signal_confidence=0.7,
        )
        levels = StopLevels(
            stop_loss_price=stop_loss,
            take_profit_price=take_profit,
            trailing_stop_active=False,
            atr_value=1.0,
            atr_multiplier=2.0,
            reward_risk_ratio=1.5,
            last_updated=datetime.now(tz=timezone.utc),
        )

        triggers = self.manager.check_price_crossings(
            positions=[position],
            prices={ticker: price_between},
            stop_levels={ticker: levels},
        )
        assert len(triggers) == 0



# ---------------------------------------------------------------------------
# Property 11: Trailing stop activation at 50% of take-profit distance
# **Validates: Requirements 4.6**
# ---------------------------------------------------------------------------


class TestProperty11TrailingStopActivation:
    """Property 11: Trailing stop activation at 50% of take-profit distance.

    **Validates: Requirements 4.6**
    """

    manager = StopLossManager()

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
        move_fraction=st.floats(min_value=0.51, max_value=0.99, allow_nan=False, allow_infinity=False),
    )
    def test_trailing_stop_activates_when_move_exceeds_50_pct(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
        move_fraction: float,
    ) -> None:
        """Trailing stop activates when favorable move > 50% of TP distance."""
        # Compute initial levels to get the TP distance
        initial_levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        tp_distance = initial_levels.take_profit_price - entry_price
        assume(tp_distance > 0.01)

        # Current price moved favorably by more than 50% of TP distance
        current_price = entry_price + (tp_distance * move_fraction)

        position = OpenPosition(
            ticker="TEST", quantity=10, entry_price=entry_price,
            current_price=current_price, unrealized_pnl=0.0,
            market_value=current_price * 10, sector="Technology",
            stop_loss_price=initial_levels.stop_loss_price,
            take_profit_price=initial_levels.take_profit_price,
            signal_confidence=0.7,
        )

        result = self.manager.re_evaluate_levels(
            position=position,
            current_price=current_price,
            atr=atr,
            risk_tier=risk_tier,
            last_levels=initial_levels,
        )

        # re_evaluate_levels returns None when no material change, or StopLevels
        # Since trailing stop activation IS a material change, we expect a result
        assert result is not None, "Expected re_evaluate to return updated levels"
        assert result.trailing_stop_active is True
        # When trailing stop is active, stop should be at least at entry (breakeven)
        assert result.stop_loss_price >= entry_price - 1e-9, (
            f"Trailing stop {result.stop_loss_price} should be >= entry {entry_price}"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
        move_fraction=st.floats(min_value=0.0, max_value=0.49, allow_nan=False, allow_infinity=False),
    )
    def test_trailing_stop_does_not_activate_when_move_at_or_below_50_pct(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
        move_fraction: float,
    ) -> None:
        """Trailing stop does NOT activate when favorable move <= 50% of TP distance."""
        initial_levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )
        tp_distance = initial_levels.take_profit_price - entry_price
        assume(tp_distance > 0.01)

        # Current price moved favorably by <= 50% of TP distance
        current_price = entry_price + (tp_distance * move_fraction)

        position = OpenPosition(
            ticker="TEST", quantity=10, entry_price=entry_price,
            current_price=current_price, unrealized_pnl=0.0,
            market_value=current_price * 10, sector="Technology",
            stop_loss_price=initial_levels.stop_loss_price,
            take_profit_price=initial_levels.take_profit_price,
            signal_confidence=0.7,
        )

        result = self.manager.re_evaluate_levels(
            position=position,
            current_price=current_price,
            atr=atr,
            risk_tier=risk_tier,
            last_levels=initial_levels,
        )

        # Either None (no change) or result with trailing_stop_active=False
        if result is not None:
            assert result.trailing_stop_active is False, (
                f"Trailing stop should not activate at {move_fraction*100:.1f}% move"
            )



# ---------------------------------------------------------------------------
# Property 15: Stop tightening during high-severity events
# **Validates: Requirements 7.2**
# ---------------------------------------------------------------------------


class TestProperty15HighSeverityEventTightening:
    """Property 15: Stop tightening during high-severity events.

    **Validates: Requirements 7.2**
    """

    manager = StopLossManager()

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_tightened_stop_uses_half_normal_multiplier(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """During high-severity events, stop uses 0.5x normal ATR multiplier."""
        initial_levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )

        # Use a current price near entry so trailing stop doesn't activate
        current_price = entry_price + 0.01

        position = OpenPosition(
            ticker="TEST", quantity=10, entry_price=entry_price,
            current_price=current_price, unrealized_pnl=0.0,
            market_value=current_price * 10, sector="Technology",
            stop_loss_price=initial_levels.stop_loss_price,
            take_profit_price=initial_levels.take_profit_price,
            signal_confidence=0.7,
        )

        result = self.manager.re_evaluate_levels(
            position=position,
            current_price=current_price,
            atr=atr,
            risk_tier=risk_tier,
            last_levels=initial_levels,
            high_severity_event=True,
        )

        # High-severity event changes the multiplier, so we expect a result
        assert result is not None, "Expected updated levels during high-severity event"

        # The tightened stop should use 0.5x the normal multiplier
        expected_tightened_multiplier = risk_tier.stop_loss_atr_multiplier * 0.5
        expected_stop = entry_price - (atr * expected_tightened_multiplier)
        assert abs(result.stop_loss_price - expected_stop) < 1e-9, (
            f"Tightened stop {result.stop_loss_price} != expected {expected_stop} "
            f"(0.5x multiplier = {expected_tightened_multiplier})"
        )

    @settings(max_examples=100)
    @given(
        entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False),
        atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False),
        risk_tier=_risk_tier_config_strategy(),
    )
    def test_tightened_stop_closer_to_entry_than_normal(
        self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
    ) -> None:
        """Tightened stop is closer to entry price (higher) than normal stop."""
        initial_levels = self.manager.compute_initial_levels(
            entry_price=entry_price, atr=atr, risk_tier=risk_tier,
        )

        current_price = entry_price + 0.01

        position = OpenPosition(
            ticker="TEST", quantity=10, entry_price=entry_price,
            current_price=current_price, unrealized_pnl=0.0,
            market_value=current_price * 10, sector="Technology",
            stop_loss_price=initial_levels.stop_loss_price,
            take_profit_price=initial_levels.take_profit_price,
            signal_confidence=0.7,
        )

        result = self.manager.re_evaluate_levels(
            position=position,
            current_price=current_price,
            atr=atr,
            risk_tier=risk_tier,
            last_levels=initial_levels,
            high_severity_event=True,
        )

        assert result is not None
        # Tightened stop should be closer to entry (higher value) than normal stop
        normal_stop = entry_price - (atr * risk_tier.stop_loss_atr_multiplier)
        assert result.stop_loss_price >= normal_stop - 1e-9, (
            f"Tightened stop {result.stop_loss_price} should be >= normal stop {normal_stop}"
        )



# ---------------------------------------------------------------------------
# Property 25: Proactive stop tightening at 80% heat threshold
# **Validates: Requirements 13.3**
# ---------------------------------------------------------------------------


class TestProperty25ProactiveHeatTightening:
    """Property 25: Proactive stop tightening at 80% heat threshold.

    **Validates: Requirements 13.3**
    """

    manager = StopLossManager()

    @settings(max_examples=100)
    @given(
        num_positions=st.integers(min_value=2, max_value=5),
        max_heat=st.floats(min_value=0.10, max_value=0.30, allow_nan=False, allow_infinity=False),
    )
    def test_lowest_confidence_positions_tightened_first(
        self, num_positions: int, max_heat: float,
    ) -> None:
        """Lowest-confidence positions get stops tightened first when heat > 80% of max."""
        # Create positions with distinct confidence levels
        positions: list[OpenPosition] = []
        stop_levels_dict: dict[str, StopLevels] = {}

        for i in range(num_positions):
            ticker = f"T{i}"
            entry_price = 100.0
            confidence = 0.3 + (i * 0.15)  # ascending confidence: 0.3, 0.45, 0.6, ...
            stop_loss = 90.0  # 10% below entry
            take_profit = 115.0

            positions.append(OpenPosition(
                ticker=ticker, quantity=10, entry_price=entry_price,
                current_price=entry_price, unrealized_pnl=0.0,
                market_value=entry_price * 10, sector="Technology",
                stop_loss_price=stop_loss, take_profit_price=take_profit,
                signal_confidence=confidence,
            ))
            stop_levels_dict[ticker] = StopLevels(
                stop_loss_price=stop_loss,
                take_profit_price=take_profit,
                trailing_stop_active=False,
                atr_value=5.0,
                atr_multiplier=2.0,
                reward_risk_ratio=1.5,
                last_updated=datetime.now(tz=timezone.utc),
            )

        # Set heat above 80% of max to trigger tightening
        portfolio_heat = max_heat * 0.85
        active_pool = 10000.0

        updated = self.manager.tighten_for_heat(
            positions=positions,
            stop_levels=stop_levels_dict,
            portfolio_heat=portfolio_heat,
            max_heat=max_heat,
            active_pool=active_pool,
        )

        if updated:
            # Verify that tightened positions are ordered by confidence (lowest first)
            tightened_tickers = list(updated.keys())
            tightened_confidences = [
                next(p.signal_confidence for p in positions if p.ticker == t)
                for t in tightened_tickers
            ]

            # The first tightened position should have the lowest confidence
            min_confidence_in_portfolio = min(p.signal_confidence for p in positions)
            if tightened_tickers:
                first_tightened_confidence = next(
                    p.signal_confidence for p in positions if p.ticker == tightened_tickers[0]
                )
                assert first_tightened_confidence == min_confidence_in_portfolio, (
                    f"First tightened position confidence {first_tightened_confidence} "
                    f"!= min confidence {min_confidence_in_portfolio}"
                )

            # All tightened stops should be >= original stops (moved closer to entry)
            for ticker, new_levels in updated.items():
                original_stop = stop_levels_dict[ticker].stop_loss_price
                assert new_levels.stop_loss_price >= original_stop - 1e-9, (
                    f"{ticker}: tightened stop {new_levels.stop_loss_price} "
                    f"< original {original_stop}"
                )

    @settings(max_examples=100)
    @given(
        max_heat=st.floats(min_value=0.10, max_value=0.30, allow_nan=False, allow_infinity=False),
        heat_fraction=st.floats(min_value=0.0, max_value=0.79, allow_nan=False, allow_infinity=False),
    )
    def test_no_tightening_when_heat_below_80_pct_threshold(
        self, max_heat: float, heat_fraction: float,
    ) -> None:
        """No tightening when portfolio heat <= 80% of max."""
        portfolio_heat = max_heat * heat_fraction

        positions = [
            OpenPosition(
                ticker="T0", quantity=10, entry_price=100.0,
                current_price=100.0, unrealized_pnl=0.0,
                market_value=1000.0, sector="Technology",
                stop_loss_price=90.0, take_profit_price=115.0,
                signal_confidence=0.5,
            ),
        ]
        stop_levels_dict = {
            "T0": StopLevels(
                stop_loss_price=90.0,
                take_profit_price=115.0,
                trailing_stop_active=False,
                atr_value=5.0,
                atr_multiplier=2.0,
                reward_risk_ratio=1.5,
                last_updated=datetime.now(tz=timezone.utc),
            ),
        }

        updated = self.manager.tighten_for_heat(
            positions=positions,
            stop_levels=stop_levels_dict,
            portfolio_heat=portfolio_heat,
            max_heat=max_heat,
            active_pool=10000.0,
        )

        assert updated == {}, (
            f"Expected no tightening at heat {portfolio_heat} "
            f"(80% threshold = {max_heat * 0.8}), got {len(updated)} updates"
        )
