"""Property-based tests for the Override Trade Tab.

Feature: override-trade-tab

Property 1: Ticker validation and normalization
Property 2: Override job payload completeness
Property 3: Invalid override order rejection
"""
from __future__ import annotations

import json
import re
from unittest.mock import AsyncMock, MagicMock, patch

from hypothesis import given, settings
from hypothesis import strategies as st
from pydantic import ValidationError
from starlette.testclient import TestClient

import services.trading.app as _app_module
from services.trading.app import OverrideOrderRequest, app

TICKER_PATTERN = re.compile(r"^[A-Z]{1,10}$")


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


def _make_fake_engine(redis_rpush_side_effect=None):
    """Create a fake engine object with a mock Redis client."""
    fake_engine = MagicMock()
    fake_engine.running = True
    fake_engine.redis = AsyncMock()
    fake_engine.redis.rpush = AsyncMock(side_effect=redis_rpush_side_effect)
    return fake_engine


def _override_client(fake_engine):
    """Return a TestClient with the module-level engine replaced."""
    original = _app_module.engine
    _app_module.engine = fake_engine
    client = TestClient(app, raise_server_exceptions=False)
    return client, original


def _restore_engine(original):
    _app_module.engine = original


# ---------------------------------------------------------------------------
# Strategies
# ---------------------------------------------------------------------------

# Strategy for valid tickers: 1-10 uppercase alpha characters
valid_ticker_st = st.from_regex(r"[A-Za-z]{1,10}", fullmatch=True)

# Strategy for valid sides
valid_side_st = st.sampled_from(["buy", "sell"])

# Strategy for valid positive quantities
valid_quantity_st = st.floats(min_value=0.01, max_value=1_000_000.0, allow_nan=False, allow_infinity=False)

# Strategy for valid order types
valid_order_type_st = st.sampled_from(["market", "limit", "stop", "stop_limit"])

# Strategy for valid positive prices
valid_price_st = st.floats(min_value=0.01, max_value=1_000_000.0, allow_nan=False, allow_infinity=False)


@st.composite
def valid_override_order_st(draw):
    """Generate a valid override order request dict."""
    ticker = draw(valid_ticker_st)
    side = draw(valid_side_st)
    quantity = draw(valid_quantity_st)
    order_type = draw(valid_order_type_st)

    order = {
        "ticker": ticker,
        "side": side,
        "quantity": quantity,
        "order_type": order_type,
    }

    if order_type in ("limit", "stop_limit"):
        order["limit_price"] = draw(valid_price_st)
    if order_type in ("stop", "stop_limit"):
        order["stop_price"] = draw(valid_price_st)

    return order


@st.composite
def invalid_override_order_st(draw):
    """Generate an override order request that violates at least one validation rule.

    Possible violations:
    - Invalid ticker format (digits, spaces, special chars, empty, too long)
    - Non-positive quantity (zero or negative)
    - Missing limit_price for limit/stop_limit orders
    - Missing stop_price for stop/stop_limit orders
    """
    violation = draw(st.sampled_from([
        "bad_ticker",
        "non_positive_quantity",
        "missing_limit_price",
        "missing_stop_price",
    ]))

    if violation == "bad_ticker":
        # Generate a ticker that does NOT match ^[A-Z]{1,10}$ after uppercasing
        bad_ticker = draw(st.sampled_from([
            "",                    # empty
            "ABCDEFGHIJK",         # 11 chars — too long
            draw(st.from_regex(r"[0-9]{1,5}", fullmatch=True)),  # digits
            draw(st.from_regex(r"[A-Z]{1,5} [A-Z]{1,5}", fullmatch=True)),  # spaces
            draw(st.from_regex(r"[A-Z]{1,5}[^A-Za-z0-9]", fullmatch=True)),  # special char
        ]))
        return {
            "ticker": bad_ticker,
            "side": "buy",
            "quantity": 10.0,
            "order_type": "market",
        }

    if violation == "non_positive_quantity":
        qty = draw(st.floats(min_value=-1_000_000.0, max_value=0.0, allow_nan=False, allow_infinity=False))
        return {
            "ticker": "AAPL",
            "side": "buy",
            "quantity": qty,
            "order_type": "market",
        }

    if violation == "missing_limit_price":
        order_type = draw(st.sampled_from(["limit", "stop_limit"]))
        order = {
            "ticker": "AAPL",
            "side": "buy",
            "quantity": 10.0,
            "order_type": order_type,
        }
        # For stop_limit, provide stop_price but NOT limit_price
        if order_type == "stop_limit":
            order["stop_price"] = 100.0
        return order

    # violation == "missing_stop_price"
    order_type = draw(st.sampled_from(["stop", "stop_limit"]))
    order = {
        "ticker": "AAPL",
        "side": "buy",
        "quantity": 10.0,
        "order_type": order_type,
    }
    # For stop_limit, provide limit_price but NOT stop_price
    if order_type == "stop_limit":
        order["limit_price"] = 150.0
    return order


# ===========================================================================
# Property 1: Ticker validation and normalization
# **Validates: Requirements 2.2, 8.1**
# ===========================================================================


class TestProperty1TickerValidationAndNormalization:
    """Property 1: Ticker validation and normalization.

    For any string input, the ticker validation function SHALL accept it
    if and only if, after uppercasing, it matches ^[A-Z]{1,10}$.
    The normalized output SHALL always be the uppercased version of the input.

    **Validates: Requirements 2.2, 8.1**
    """

    @settings(max_examples=100)
    @given(ticker=st.text(min_size=0, max_size=20))
    def test_ticker_accepted_iff_matches_pattern(self, ticker: str) -> None:
        """After uppercasing, accepted iff matches ^[A-Z]{1,10}$;
        normalized output is always uppercased input."""
        uppercased = ticker.upper()
        should_accept = bool(TICKER_PATTERN.match(uppercased))

        try:
            req = OverrideOrderRequest(
                ticker=ticker,
                side="buy",
                quantity=1.0,
                order_type="market",
            )
            # Accepted — verify it should have been accepted
            assert should_accept, (
                f"Ticker {ticker!r} was accepted but uppercased form "
                f"{uppercased!r} does not match ^[A-Z]{{1,10}}$"
            )
            # Normalized output is always uppercased
            assert req.ticker == uppercased, (
                f"Expected normalized ticker {uppercased!r}, got {req.ticker!r}"
            )
        except ValidationError:
            # Rejected — verify it should have been rejected
            assert not should_accept, (
                f"Ticker {ticker!r} was rejected but uppercased form "
                f"{uppercased!r} matches ^[A-Z]{{1,10}}$"
            )


# ===========================================================================
# Property 2: Override job payload completeness
# **Validates: Requirements 3.2, 9.1**
# ===========================================================================


class TestProperty2OverrideJobPayloadCompleteness:
    """Property 2: Override job payload completeness.

    For any valid override order request, the job payload enqueued to the
    broker queue SHALL contain all required fields, source == "manual_override",
    and idempotency_key starts with "override-".

    **Validates: Requirements 3.2, 9.1**
    """

    @settings(max_examples=100)
    @given(order=valid_override_order_st())
    def test_enqueued_payload_has_all_required_fields(self, order: dict) -> None:
        """Enqueued payload contains all required fields with correct values."""
        captured_payloads: list[str] = []

        async def capture_rpush(key, value):
            captured_payloads.append(value)

        fake_engine = _make_fake_engine()
        fake_engine.redis.rpush = AsyncMock(side_effect=capture_rpush)
        client, original = _override_client(fake_engine)

        try:
            with patch.object(
                _app_module,
                "auto_register_symbol",
                new_callable=AsyncMock,
                return_value=(False, "comp-1"),
            ):
                import httpx

                def _mock_handler(request: httpx.Request) -> httpx.Response:
                    return httpx.Response(
                        200,
                        content=json.dumps([{"id": "comp-1", "ticker": order["ticker"].upper()}]).encode(),
                        headers={"content-type": "application/json"},
                    )

                original_init = httpx.AsyncClient.__init__

                def patched_init(self, *args, **kwargs):
                    kwargs.pop("timeout", None)
                    kwargs["transport"] = httpx.MockTransport(_mock_handler)
                    original_init(self, *args, **kwargs)

                with patch.object(httpx.AsyncClient, "__init__", patched_init):
                    resp = client.post("/api/trading/override/order", json=order)

            assert resp.status_code == 202, (
                f"Expected 202 for valid order {order!r}, got {resp.status_code}: {resp.text}"
            )
            assert len(captured_payloads) == 1, "Expected exactly one RPUSH call"

            payload = json.loads(captured_payloads[0])

            # All required fields present
            expected_ticker = order["ticker"].upper()
            assert payload["ticker"] == expected_ticker
            assert payload["side"] == order["side"]
            assert payload["quantity"] == order["quantity"]
            assert payload["order_type"] == order["order_type"]
            assert payload["source"] == "manual_override"
            assert isinstance(payload["idempotency_key"], str)
            assert payload["idempotency_key"].startswith("override-")

            # Conditional price fields
            if "limit_price" in order:
                assert payload["limit_price"] == order["limit_price"]
            if "stop_price" in order:
                assert payload["stop_price"] == order["stop_price"]
        finally:
            _restore_engine(original)


# ===========================================================================
# Property 3: Invalid override order rejection
# **Validates: Requirements 3.5, 2.6**
# ===========================================================================


class TestProperty3InvalidOverrideOrderRejection:
    """Property 3: Invalid override order rejection.

    For any override order request that violates at least one validation rule,
    the endpoint SHALL return a 422 status code and the response body SHALL
    contain at least one descriptive error message.

    **Validates: Requirements 3.5, 2.6**
    """

    @settings(max_examples=100)
    @given(order=invalid_override_order_st())
    def test_invalid_order_returns_422_with_error_message(self, order: dict) -> None:
        """Invalid orders return 422 with at least one descriptive error."""
        fake_engine = _make_fake_engine()
        client, original = _override_client(fake_engine)

        try:
            resp = client.post("/api/trading/override/order", json=order)

            assert resp.status_code == 422, (
                f"Expected 422 for invalid order {order!r}, got {resp.status_code}: {resp.text}"
            )

            body = resp.json()
            # FastAPI returns validation errors in a "detail" field
            assert "detail" in body, (
                f"Expected 'detail' in 422 response body, got: {body}"
            )
            # At least one error message
            detail = body["detail"]
            assert isinstance(detail, list) and len(detail) >= 1, (
                f"Expected at least one validation error, got: {detail}"
            )
        finally:
            _restore_engine(original)
