"""Integration tests for services.signal_engine.worker — Top-level orchestrator.

Tests the full evaluation tick flow with mocked DB/Redis, pipeline failure
isolation, hard filter short-circuit, and shadow mode behavior.

Requirements: 11.1, 11.2, 11.3, 11.6, 13.1, 13.6, 13.7, 15.1, 15.4, 16.1, 16.6
"""

from __future__ import annotations

from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from services.signal_engine.config import SignalEngineConfig
from services.signal_engine.models import (
    DeltaResult,
    HeuristicResult,
    NormalizedInput,
    ProbabilisticResult,
    Verdict,
)
from services.signal_engine.worker import evaluate_tick

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


def _default_config(**overrides: object) -> SignalEngineConfig:
    """Build a SignalEngineConfig with sensible test defaults."""
    defaults = {
        "dual_pipeline_enabled": True,
        "shadow_mode": False,
    }
    defaults.update(overrides)
    return SignalEngineConfig(**defaults)


def _normalized_input(
    *,
    ticker: str = "AAPL",
    macro_bias: float = 0.5,
    valuation_score: float = 0.8,
    earnings_proximity_days: int = 30,
    current_price: float = 150.0,
) -> NormalizedInput:
    """Build a NormalizedInput with test defaults that pass hard filters."""
    return NormalizedInput(
        ticker=ticker,
        evaluated_at=datetime.now(tz=timezone.utc),
        bars={},
        valuation_score=valuation_score,
        earnings_proximity_days=earnings_proximity_days,
        macro_bias=macro_bias,
        open_positions=[],
        closing_prices=[100.0 + i for i in range(120)],
        returns=[0.01] * 119,
        current_price=current_price,
    )


def _heuristic_buy() -> HeuristicResult:
    return HeuristicResult(
        verdict=Verdict.BUY,
        confidence=0.85,
        s_total=1.5,
        s_company=1.0,
        s_macro=0.3,
        s_competitive=0.2,
        signal_weights=[],
        reasoning=["BUY: all conditions met"],
    )


def _heuristic_skip() -> HeuristicResult:
    return HeuristicResult(
        verdict=Verdict.SKIP,
        confidence=0.3,
        s_total=0.5,
        s_company=0.3,
        s_macro=0.1,
        s_competitive=0.1,
        signal_weights=[],
        reasoning=["SKIP: low confidence"],
    )


def _probabilistic_buy() -> ProbabilisticResult:
    return ProbabilisticResult(
        verdict=Verdict.BUY,
        p_up=0.72,
        entropy=0.6,
        ev_r=2.0,
        prior=0.58,
        posterior=0.72,
        likelihood_ratios=[],
        regime="trend_following",
        reasoning=["BUY: all conditions met"],
    )


def _probabilistic_skip() -> ProbabilisticResult:
    return ProbabilisticResult(
        verdict=Verdict.SKIP,
        p_up=0.4,
        entropy=0.95,
        ev_r=0.5,
        prior=0.5,
        posterior=0.4,
        likelihood_ratios=[],
        regime="uncertainty",
        reasoning=["SKIP: low P_up"],
    )


def _delta_result(agreement: bool = True) -> DeltaResult:
    return DeltaResult(
        agreement=agreement,
        confidence_delta=0.1,
        heuristic_verdict="BUY",
        probabilistic_verdict="BUY",
        disagreement_reasons=[],
        rolling_agreement_rate=0.9,
    )


# ===========================================================================
# 1. Full tick evaluation with mocked data (Req 11.1, 11.2, 11.5, 11.6)
# ===========================================================================


class TestFullTickEvaluation:
    """Test the full evaluation tick with both pipelines producing BUY."""

    @pytest.mark.asyncio
    async def test_full_tick_both_buy_publishes_to_queue(self) -> None:
        """Both pipelines BUY → output persisted and published to trading queue."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input()
        heuristic = _heuristic_buy()
        probabilistic = _probabilistic_buy()
        delta = _delta_result(agreement=True)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                return_value=heuristic,
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                return_value=probabilistic,
            ),
            patch(
                "services.signal_engine.worker.analyze_delta",
                new_callable=AsyncMock,
                return_value=delta,
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is not None
        assert output.ticker == "AAPL"
        assert output.heuristic_verdict == "BUY"
        assert output.probabilistic_verdict == "BUY"

        # Persistence was called
        mock_persist.assert_awaited_once()

        # Trading queue was published to (at least one BUY, not shadow mode)
        redis_client.rpush.assert_awaited_once()
        call_args = redis_client.rpush.call_args
        assert call_args[0][0] == "stonks:queue:trading_decisions"

    @pytest.mark.asyncio
    async def test_full_tick_no_buy_does_not_publish(self) -> None:
        """Both pipelines SKIP → output persisted but NOT published to queue."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input()
        heuristic = _heuristic_skip()
        probabilistic = _probabilistic_skip()
        delta = _delta_result(agreement=True)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                return_value=heuristic,
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                return_value=probabilistic,
            ),
            patch(
                "services.signal_engine.worker.analyze_delta",
                new_callable=AsyncMock,
                return_value=delta,
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is not None
        # Persisted
        mock_persist.assert_awaited_once()
        # NOT published to trading queue
        redis_client.rpush.assert_not_awaited()


# ===========================================================================
# 2. Pipeline failure isolation (Req 11.3)
# ===========================================================================


class TestPipelineFailureIsolation:
    """One pipeline fails → SKIP verdict for that pipeline, other completes."""

    @pytest.mark.asyncio
    async def test_heuristic_fails_probabilistic_completes(self) -> None:
        """Heuristic raises exception → SKIP, probabilistic completes normally."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input()
        probabilistic = _probabilistic_buy()
        delta = _delta_result(agreement=False)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                side_effect=RuntimeError("heuristic boom"),
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                return_value=probabilistic,
            ),
            patch(
                "services.signal_engine.worker.analyze_delta",
                new_callable=AsyncMock,
                return_value=delta,
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is not None
        # Heuristic fell back to SKIP
        assert output.heuristic_verdict == "SKIP"
        # Probabilistic completed normally
        assert output.probabilistic_verdict == "BUY"
        # Still persisted
        mock_persist.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_probabilistic_fails_heuristic_completes(self) -> None:
        """Probabilistic raises exception → SKIP, heuristic completes normally."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input()
        heuristic = _heuristic_buy()
        delta = _delta_result(agreement=False)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                return_value=heuristic,
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                side_effect=RuntimeError("probabilistic boom"),
            ),
            patch(
                "services.signal_engine.worker.analyze_delta",
                new_callable=AsyncMock,
                return_value=delta,
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is not None
        assert output.heuristic_verdict == "BUY"
        assert output.probabilistic_verdict == "SKIP"
        mock_persist.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_both_pipelines_fail_returns_none(self) -> None:
        """Both pipelines raise exceptions → returns None."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input()

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                side_effect=RuntimeError("heuristic boom"),
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                side_effect=RuntimeError("probabilistic boom"),
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is None
        # Nothing persisted when both fail
        mock_persist.assert_not_awaited()


# ===========================================================================
# 3. Hard filter short-circuit (Req 4.1, 4.2, 4.3)
# ===========================================================================


class TestHardFilterShortCircuit:
    """Hard filter triggers → returns None without running pipelines."""

    @pytest.mark.asyncio
    async def test_hard_filter_returns_none(self) -> None:
        """When hard filter triggers, evaluate_tick returns None."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config()

        normalized = _normalized_input(macro_bias=-1.0)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
            ) as mock_heuristic,
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
            ) as mock_probabilistic,
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(
                filtered=True, reasons=["macro_bias_negative"]
            )

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is None
        # Pipelines were NOT called
        mock_heuristic.assert_not_called()
        mock_probabilistic.assert_not_called()
        # Nothing persisted
        mock_persist.assert_not_awaited()


# ===========================================================================
# 4. Shadow mode behavior (Req 16.6)
# ===========================================================================


class TestShadowMode:
    """Shadow mode: persists output but does NOT publish to trading queue."""

    @pytest.mark.asyncio
    async def test_shadow_mode_persists_but_does_not_publish(self) -> None:
        """In shadow mode, BUY signals are persisted but not forwarded."""
        pool = AsyncMock()
        redis_client = AsyncMock()
        config = _default_config(shadow_mode=True)

        normalized = _normalized_input()
        heuristic = _heuristic_buy()
        probabilistic = _probabilistic_buy()
        delta = _delta_result(agreement=True)

        with (
            patch(
                "services.signal_engine.worker.normalize_input",
                new_callable=AsyncMock,
                return_value=normalized,
            ),
            patch(
                "services.signal_engine.worker.evaluate_exits",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.evaluate_hard_filters",
            ) as mock_hf,
            patch(
                "services.signal_engine.worker._evaluate_signals",
                return_value={},
            ),
            patch(
                "services.signal_engine.worker.compute_confluence",
                return_value=[],
            ),
            patch(
                "services.signal_engine.worker.classify_regime",
            ),
            patch(
                "services.signal_engine.worker.run_heuristic_pipeline",
                return_value=heuristic,
            ),
            patch(
                "services.signal_engine.worker.run_probabilistic_pipeline",
                return_value=probabilistic,
            ),
            patch(
                "services.signal_engine.worker.analyze_delta",
                new_callable=AsyncMock,
                return_value=delta,
            ),
            patch(
                "services.signal_engine.worker.persist_signal_output",
                new_callable=AsyncMock,
            ) as mock_persist,
        ):
            mock_hf.return_value = MagicMock(filtered=False, reasons=[])

            output = await evaluate_tick(pool, redis_client, "AAPL", config)

        assert output is not None
        assert output.heuristic_verdict == "BUY"
        assert output.probabilistic_verdict == "BUY"

        # Persisted (shadow mode still persists)
        mock_persist.assert_awaited_once()

        # NOT published to trading queue (shadow mode blocks publishing)
        redis_client.rpush.assert_not_awaited()
