"""Unit tests for services.signal_engine.formatter — Output Formatter.

Tests trade plan generation for dual_confirmed, probabilistic_only,
heuristic-only, and no-BUY cases.  Also tests the
``signal_output_to_recommendation`` mapping to the existing
``Recommendation`` schema.

Requirements: 10.2, 10.3, 10.4, 12.3, 12.4
"""
from __future__ import annotations

from services.shared.schemas import ActionType, RecommendationMode
from services.signal_engine.config import SignalEngineConfig
from services.signal_engine.formatter import (
    format_output,
    signal_output_to_recommendation,
)
from services.signal_engine.models import (
    DeltaResult,
    ExitSignal,
    ExitType,
    HeuristicResult,
    ProbabilisticResult,
    Verdict,
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _default_config() -> SignalEngineConfig:
    return SignalEngineConfig()


def _heuristic(
    verdict: Verdict = Verdict.BUY,
    confidence: float = 0.85,
    s_total: float = 1.5,
) -> HeuristicResult:
    return HeuristicResult(
        verdict=verdict,
        confidence=confidence,
        s_total=s_total,
        s_company=1.0,
        s_macro=0.3,
        s_competitive=0.2,
        signal_weights=[],
        reasoning=[f"{verdict.value} verdict"],
    )


def _probabilistic(
    verdict: Verdict = Verdict.BUY,
    p_up: float = 0.75,
    entropy: float = 0.6,
    ev_r: float = 2.0,
) -> ProbabilisticResult:
    return ProbabilisticResult(
        verdict=verdict,
        p_up=p_up,
        entropy=entropy,
        ev_r=ev_r,
        prior=0.58,
        posterior=0.75,
        likelihood_ratios=[],
        regime="bull",
        reasoning=[f"{verdict.value} verdict"],
    )


def _delta(
    agreement: bool = True,
    confidence_delta: float = 0.1,
    reasons: list[str] | None = None,
) -> DeltaResult:
    return DeltaResult(
        agreement=agreement,
        confidence_delta=confidence_delta,
        heuristic_verdict="BUY",
        probabilistic_verdict="BUY",
        disagreement_reasons=reasons or [],
        rolling_agreement_rate=0.85,
    )


# ===========================================================================
# 1. Dual confirmed trade plan (Requirement 10.4)
# ===========================================================================


class TestDualConfirmed:
    """Both pipelines BUY → dual_confirmed, full position sizing."""

    def test_dual_confirmed_trade_plan(self) -> None:
        """Both BUY → trade_plan with dual_confirmed=True."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.dual_confirmed is True
        assert output.trade_plan.probabilistic_only is False

    def test_dual_confirmed_full_position_sizing(self) -> None:
        """Dual confirmed → position_size_pct=0.02, max_loss_pct=0.005."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.position_size_pct == 0.02
        assert output.trade_plan.max_loss_pct == 0.005

    def test_dual_confirmed_price_levels(self) -> None:
        """Trade plan price levels: stop=95%, target_1=105%, target_2=110%."""
        price = 200.0
        output = format_output(
            ticker="AAPL",
            price=price,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        tp = output.trade_plan
        assert tp is not None
        assert tp.entry_price == price
        assert abs(tp.stop_loss - price * 0.95) < 1e-6
        assert abs(tp.target_1 - price * 1.05) < 1e-6
        assert abs(tp.target_2 - price * 1.10) < 1e-6


# ===========================================================================
# 2. Probabilistic-only trade plan (Requirement 10.3)
# ===========================================================================


class TestProbabilisticOnly:
    """Probabilistic BUY, heuristic not BUY → probabilistic_only, 50% sizing."""

    def test_probabilistic_only_trade_plan(self) -> None:
        """Probabilistic BUY + heuristic WATCH → probabilistic_only flag."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.probabilistic_only is True
        assert output.trade_plan.dual_confirmed is False

    def test_probabilistic_only_reduced_sizing(self) -> None:
        """Probabilistic-only → position_size_pct=0.01 (50% of standard)."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.SKIP, confidence=0.40),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.position_size_pct == 0.01
        assert output.trade_plan.max_loss_pct == 0.005

    def test_probabilistic_only_price_levels(self) -> None:
        """Price levels are the same regardless of confirmation mode."""
        price = 100.0
        output = format_output(
            ticker="MSFT",
            price=price,
            heuristic=_heuristic(Verdict.WATCH),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        tp = output.trade_plan
        assert tp is not None
        assert tp.entry_price == price
        assert abs(tp.stop_loss - price * 0.95) < 1e-6
        assert abs(tp.target_1 - price * 1.05) < 1e-6
        assert abs(tp.target_2 - price * 1.10) < 1e-6


# ===========================================================================
# 3. Heuristic-only trade plan (Requirement 10.2)
# ===========================================================================


class TestHeuristicOnly:
    """Heuristic BUY, probabilistic not BUY → standard position sizing."""

    def test_heuristic_only_trade_plan(self) -> None:
        """Heuristic BUY + probabilistic WATCH → standard trade plan."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.dual_confirmed is False
        assert output.trade_plan.probabilistic_only is False

    def test_heuristic_only_full_sizing(self) -> None:
        """Heuristic-only → position_size_pct=0.02 (full standard)."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is not None
        assert output.trade_plan.position_size_pct == 0.02
        assert output.trade_plan.max_loss_pct == 0.005


# ===========================================================================
# 4. No BUY — no trade plan (Requirement 10.4 inverse)
# ===========================================================================


class TestNoBuy:
    """Neither pipeline BUY → no trade_plan."""

    def test_both_watch_no_trade_plan(self) -> None:
        """Both WATCH → no trade_plan."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is None

    def test_both_skip_no_trade_plan(self) -> None:
        """Both SKIP → no trade_plan."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.SKIP, confidence=0.30),
            probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is None

    def test_watch_and_skip_no_trade_plan(self) -> None:
        """WATCH + SKIP → no trade_plan."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.trade_plan is None

    def test_no_buy_still_has_pipeline_data(self) -> None:
        """Even without trade_plan, pipeline data is populated for analysis."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.heuristic_verdict == "WATCH"
        assert output.probabilistic_verdict == "WATCH"
        assert output.heuristic_confidence == 0.60
        assert output.probabilistic_p_up == 0.57
        assert output.delta_agreement is True


# ===========================================================================
# 5. signal_output_to_recommendation mapping (Requirements 12.3, 12.4)
# ===========================================================================


class TestSignalOutputToRecommendation:
    """Map SignalOutput to existing Recommendation schema."""

    def test_dual_confirmed_confidence(self) -> None:
        """Dual confirmed → confidence = max(heuristic, probabilistic)."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY, confidence=0.85),
            probabilistic=_probabilistic(Verdict.BUY, p_up=0.75),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.confidence == max(0.85, 0.75)
        assert rec.confidence == 0.85

    def test_probabilistic_only_confidence_haircut(self) -> None:
        """Probabilistic only → confidence = P_up * 0.8 (20% haircut)."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.BUY, p_up=0.75),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert abs(rec.confidence - 0.75 * 0.8) < 1e-9
        assert abs(rec.confidence - 0.6) < 1e-9

    def test_heuristic_only_confidence(self) -> None:
        """Heuristic only → confidence = heuristic_confidence."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY, confidence=0.80),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.confidence == 0.80

    def test_buy_action_mapping(self) -> None:
        """Any BUY verdict → ActionType.BUY."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=False),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.action == ActionType.BUY

    def test_watch_action_mapping(self) -> None:
        """Both WATCH → ActionType.WATCH."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.WATCH, confidence=0.60),
            probabilistic=_probabilistic(Verdict.WATCH, p_up=0.57),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.action == ActionType.WATCH

    def test_skip_action_mapping(self) -> None:
        """Both SKIP → ActionType.HOLD."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.SKIP, confidence=0.30),
            probabilistic=_probabilistic(Verdict.SKIP, p_up=0.40, entropy=0.9, ev_r=0.5),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.action == ActionType.HOLD

    def test_recommendation_mode_paper_eligible(self) -> None:
        """All recommendations use PAPER_ELIGIBLE mode."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.mode == RecommendationMode.PAPER_ELIGIBLE

    def test_recommendation_position_sizing_from_trade_plan(self) -> None:
        """Position sizing in recommendation matches trade plan."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.position_sizing.portfolio_pct == 0.02
        assert rec.position_sizing.max_loss_pct == 0.005

    def test_recommendation_probabilistic_fields(self) -> None:
        """Recommendation includes probabilistic pipeline fields."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY, p_up=0.75, ev_r=2.0),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.p_bull == 0.75
        assert rec.expected_value == 2.0
        assert rec.pipeline_mode == "dual_pipeline"

    def test_recommendation_ticker_and_id(self) -> None:
        """Recommendation inherits ticker and output_id."""
        output = format_output(
            ticker="MSFT",
            price=300.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )
        rec = signal_output_to_recommendation(output)

        assert rec.ticker == "MSFT"
        assert rec.recommendation_id == output.output_id


# ===========================================================================
# 6. SignalOutput structure and metadata
# ===========================================================================


class TestSignalOutputStructure:
    """Verify SignalOutput has all required fields populated."""

    def test_output_has_all_pipeline_data(self) -> None:
        """SignalOutput contains heuristic, probabilistic, and delta sections."""
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY, confidence=0.85, s_total=1.5),
            probabilistic=_probabilistic(Verdict.BUY, p_up=0.75, entropy=0.6, ev_r=2.0),
            delta=_delta(agreement=True, confidence_delta=0.1),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.ticker == "AAPL"
        assert output.price == 150.0
        assert output.heuristic_verdict == "BUY"
        assert output.heuristic_confidence == 0.85
        assert output.heuristic_s_total == 1.5
        assert output.probabilistic_verdict == "BUY"
        assert output.probabilistic_p_up == 0.75
        assert output.probabilistic_entropy == 0.6
        assert output.probabilistic_ev_r == 2.0
        assert output.delta_agreement is True
        assert output.delta_confidence_delta == 0.1
        assert output.pipeline_mode == "dual_pipeline"

    def test_output_includes_exit_signals(self) -> None:
        """Exit signals are passed through to the output."""
        exits = [
            ExitSignal(
                position_id="pos-1",
                ticker="AAPL",
                exit_type=ExitType.EXIT_HALF,
                reason="target_1_hit",
                price=157.5,
            ),
        ]
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=exits,
            config=_default_config(),
        )

        assert len(output.exit_signals) == 1
        assert output.exit_signals[0].position_id == "pos-1"
        assert output.exit_signals[0].reason == "target_1_hit"

    def test_output_shadow_mode(self) -> None:
        """Shadow mode flag is propagated from config."""
        config = SignalEngineConfig(shadow_mode=True)
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=_heuristic(Verdict.BUY),
            probabilistic=_probabilistic(Verdict.BUY),
            delta=_delta(agreement=True),
            exit_signals=[],
            config=config,
        )

        assert output.shadow_mode is True

    def test_output_detail_payloads(self) -> None:
        """Heuristic and probabilistic detail payloads are populated for audit."""
        h = _heuristic(Verdict.BUY, confidence=0.85)
        p = _probabilistic(Verdict.BUY, p_up=0.75)
        output = format_output(
            ticker="AAPL",
            price=150.0,
            heuristic=h,
            probabilistic=p,
            delta=_delta(agreement=True),
            exit_signals=[],
            config=_default_config(),
        )

        assert output.heuristic_detail["verdict"] == "BUY"
        assert output.heuristic_detail["confidence"] == 0.85
        assert output.probabilistic_detail["verdict"] == "BUY"
        assert output.probabilistic_detail["p_up"] == 0.75
