# Feature: dual-pipeline-signal-engine, Property: SignalOutput round-trip serialization
"""Property-based tests for SignalOutput round-trip serialization.

Feature: dual-pipeline-signal-engine

Tests the SignalOutput round-trip serialization property from the design
specification: for any valid SignalOutput instance, serializing to JSON via
model_dump_json() and deserializing back via model_validate_json() SHALL
produce a SignalOutput object equivalent to the original.
"""
from __future__ import annotations

from datetime import datetime, timezone

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

from services.signal_engine.models import (
    ExitSignal,
    ExitType,
    SignalOutput,
    TradePlan,
)

# ---------------------------------------------------------------------------
# Property: SignalOutput Round-Trip Serialization
# Validates: Requirements 10.5, 17.6
# ---------------------------------------------------------------------------

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

_finite_float = st.floats(allow_nan=False, allow_infinity=False)
_non_negative_finite_float = st.floats(
    min_value=0.0, allow_nan=False, allow_infinity=False,
)
_unit_float = st.floats(
    min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False,
)

_aware_datetime_strategy = st.datetimes(
    min_value=datetime(2020, 1, 1),
    max_value=datetime(2030, 12, 31),
    timezones=st.just(timezone.utc),
)

_ticker_strategy = st.text(
    alphabet=st.characters(whitelist_categories=("Lu",)),
    min_size=1,
    max_size=5,
)

_verdict_strategy = st.sampled_from(["BUY", "WATCH", "SKIP"])

_pipeline_mode_strategy = st.sampled_from(["dual_pipeline", "heuristic_only", "probabilistic_only"])

# --- TradePlan strategy ---

_trade_plan_strategy = st.builds(
    TradePlan,
    entry_price=_finite_float,
    stop_loss=_finite_float,
    target_1=_finite_float,
    target_2=_finite_float,
    position_size_pct=_unit_float,
    max_loss_pct=_unit_float,
    dual_confirmed=st.booleans(),
    probabilistic_only=st.booleans(),
)

# --- ExitSignal strategy ---

_exit_signal_strategy = st.builds(
    ExitSignal,
    position_id=st.uuids().map(str),
    ticker=_ticker_strategy,
    exit_type=st.sampled_from(list(ExitType)),
    reason=st.sampled_from(["stop_hit", "target_1_hit", "target_2_hit", "trailing_stop_hit"]),
    price=_finite_float,
)

# --- Simple dict strategies for detail payloads ---

_simple_detail_strategy = st.fixed_dictionaries(
    {},
    optional={
        "score": _finite_float,
        "label": st.text(max_size=20),
        "count": st.integers(min_value=0, max_value=1000),
    },
)

# --- SignalOutput strategy ---

_signal_output_strategy = st.builds(
    SignalOutput,
    output_id=st.uuids().map(str),
    ticker=_ticker_strategy,
    timestamp=_aware_datetime_strategy,
    price=_finite_float,
    heuristic_verdict=_verdict_strategy,
    heuristic_confidence=_unit_float,
    heuristic_s_total=_finite_float,
    probabilistic_verdict=_verdict_strategy,
    probabilistic_p_up=_unit_float,
    probabilistic_entropy=_unit_float,
    probabilistic_ev_r=_finite_float,
    delta_agreement=st.booleans(),
    delta_confidence_delta=_non_negative_finite_float,
    delta_reasons=st.lists(st.text(min_size=1, max_size=50), min_size=0, max_size=5),
    trade_plan=st.one_of(st.none(), _trade_plan_strategy),
    exit_signals=st.lists(_exit_signal_strategy, min_size=0, max_size=3),
    heuristic_detail=_simple_detail_strategy,
    probabilistic_detail=_simple_detail_strategy,
    pipeline_mode=_pipeline_mode_strategy,
    shadow_mode=st.booleans(),
)


# ---------------------------------------------------------------------------
# Property test
# ---------------------------------------------------------------------------


@given(output=_signal_output_strategy)
@settings(max_examples=100)
def test_signal_output_round_trip_serialization(output: SignalOutput) -> None:
    """**Validates: Requirements 10.5, 17.6**

    For any valid SignalOutput instance, serializing to JSON and then
    deserializing back SHALL produce a SignalOutput object equivalent
    to the original.
    """
    json_str = output.model_dump_json()
    restored = SignalOutput.model_validate_json(json_str)
    assert restored == output, (
        f"Round-trip failed: deserialized SignalOutput differs from original.\n"
        f"  ticker: {output.ticker}\n"
        f"  heuristic_verdict: {output.heuristic_verdict}\n"
        f"  probabilistic_verdict: {output.probabilistic_verdict}\n"
        f"  trade_plan present: {output.trade_plan is not None}\n"
        f"  exit_signals count: {len(output.exit_signals)}"
    )
