"""Property-based tests for Trading Engine HTTP API.

Feature: autonomous-trading-engine

Tests properties 35 and 29 from the design specification, covering
configuration change audit trail and persistence round-trip for
trading engine state via the FastAPI endpoints.
"""

from __future__ import annotations

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

from services.trading.app import app

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

_RISK_TIERS = st.sampled_from(["conservative", "moderate", "aggressive"])

_CONFIG_UPDATES = st.fixed_dictionaries(
    {},
    optional={
        "enabled": st.booleans(),
        "risk_tier": _RISK_TIERS,
        "reserve_siphon_pct": st.floats(
            min_value=0.0, max_value=1.0,
            allow_nan=False, allow_infinity=False,
        ),
        "polling_interval_seconds": st.integers(min_value=1, max_value=3600),
        "absolute_position_cap": st.floats(
            min_value=1.0, max_value=10_000.0,
            allow_nan=False, allow_infinity=False,
        ),
        "active_pool_minimum": st.floats(
            min_value=0.0, max_value=10_000.0,
            allow_nan=False, allow_infinity=False,
        ),
        "micro_trading_enabled": st.booleans(),
    },
).filter(lambda d: len(d) > 0)  # at least one field must be set


# ---------------------------------------------------------------------------
# Module-level client with lifespan
# ---------------------------------------------------------------------------

# Use a stack to manage the TestClient context manager at module scope.
_client: TestClient | None = None


def _get_client() -> TestClient:
    global _client
    if _client is None:
        _client = TestClient(app, raise_server_exceptions=True)
        _client.__enter__()
    return _client


def teardown_module() -> None:
    global _client
    if _client is not None:
        _client.__exit__(None, None, None)
        _client = None


# ---------------------------------------------------------------------------
# Property 35: Configuration change audit trail
# **Validates: Requirements 16.6**
# ---------------------------------------------------------------------------


class TestProperty35ConfigurationChangeAuditTrail:
    """Property 35: Configuration change audit trail.

    **Validates: Requirements 16.6**
    """

    @settings(max_examples=100)
    @given(update=_CONFIG_UPDATES)
    def test_config_update_returns_previous_and_new(
        self, update: dict,
    ) -> None:
        """PUT /api/trading/config returns previous and new values for every changed field."""
        client = _get_client()
        resp = client.put("/api/trading/config", json=update)
        assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"

        data = resp.json()
        assert "previous" in data, "Response must include 'previous'"
        assert "updated" in data, "Response must include 'updated'"
        assert "change_source" in data, "Response must include 'change_source'"
        assert "changed_at" in data, "Response must include 'changed_at'"

        # Every field sent in the request must appear in both previous and updated
        for field_name, new_value in update.items():
            assert field_name in data["previous"], (
                f"Field '{field_name}' missing from previous config"
            )
            assert field_name in data["updated"], (
                f"Field '{field_name}' missing from updated config"
            )
            # The updated value must match what was sent
            assert data["updated"][field_name] == new_value, (
                f"Updated value for '{field_name}' should be {new_value}, "
                f"got {data['updated'][field_name]}"
            )

    @settings(max_examples=100)
    @given(update=_CONFIG_UPDATES)
    def test_config_update_change_source_is_api(
        self, update: dict,
    ) -> None:
        """PUT /api/trading/config always records change_source as 'api'."""
        client = _get_client()
        resp = client.put("/api/trading/config", json=update)
        assert resp.status_code == 200
        assert resp.json()["change_source"] == "api"


# ---------------------------------------------------------------------------
# Property 29: Persistence round-trip for trading engine state
# **Validates: Requirements 3.2, 4.7, 5.5, 6.4, 14.3, 15.4, 16.1**
# ---------------------------------------------------------------------------


class TestProperty29PersistenceRoundTrip:
    """Property 29: Persistence round-trip for trading engine state.

    **Validates: Requirements 3.2, 4.7, 5.5, 6.4, 14.3, 15.4, 16.1**
    """

    def test_status_returns_valid_dict_with_expected_keys(self) -> None:
        """GET /api/trading/status returns a dict with all expected keys."""
        client = _get_client()
        resp = client.get("/api/trading/status")
        assert resp.status_code == 200

        data = resp.json()
        expected_keys = {
            "enabled",
            "paused",
            "risk_tier",
            "circuit_breaker_status",
            "active_pool",
            "reserve_pool",
            "portfolio_heat",
            "open_positions",
            "last_decision_at",
        }
        assert expected_keys.issubset(data.keys()), (
            f"Missing keys: {expected_keys - data.keys()}"
        )

    def test_pause_then_status_shows_paused(self) -> None:
        """POST /api/trading/pause followed by GET /api/trading/status shows paused=true."""
        client = _get_client()
        pause_resp = client.post("/api/trading/pause")
        assert pause_resp.status_code == 200
        assert pause_resp.json()["paused"] is True

        status_resp = client.get("/api/trading/status")
        assert status_resp.status_code == 200
        assert status_resp.json()["paused"] is True

    def test_resume_then_status_shows_not_paused(self) -> None:
        """POST /api/trading/resume followed by GET /api/trading/status shows paused=false."""
        client = _get_client()
        resume_resp = client.post("/api/trading/resume")
        assert resume_resp.status_code == 200
        assert resume_resp.json()["paused"] is False

        status_resp = client.get("/api/trading/status")
        assert status_resp.status_code == 200
        assert status_resp.json()["paused"] is False

    @settings(max_examples=100)
    @given(risk_tier=_RISK_TIERS)
    def test_config_round_trip_risk_tier(self, risk_tier: str) -> None:
        """Setting risk_tier via config update is reflected in status."""
        client = _get_client()
        resp = client.put(
            "/api/trading/config",
            json={"risk_tier": risk_tier},
        )
        assert resp.status_code == 200
        assert resp.json()["updated"]["risk_tier"] == risk_tier

        status = client.get("/api/trading/status").json()
        assert status["risk_tier"] == risk_tier

    @settings(max_examples=100)
    @given(enabled=st.booleans())
    def test_config_round_trip_enabled(self, enabled: bool) -> None:
        """Setting enabled via config update is reflected in status."""
        client = _get_client()
        resp = client.put(
            "/api/trading/config",
            json={"enabled": enabled},
        )
        assert resp.status_code == 200
        assert resp.json()["updated"]["enabled"] == enabled

        status = client.get("/api/trading/status").json()
        assert status["enabled"] == enabled

    def test_health_returns_ok(self) -> None:
        """GET /health always returns status ok."""
        client = _get_client()
        resp = client.get("/health")
        assert resp.status_code == 200
        assert resp.json() == {"status": "ok"}

    def test_ready_returns_boolean(self) -> None:
        """GET /ready returns a dict with a boolean 'ready' field."""
        client = _get_client()
        resp = client.get("/ready")
        assert resp.status_code == 200
        assert isinstance(resp.json()["ready"], bool)

    def test_backtest_returns_id(self) -> None:
        """POST /api/trading/backtest returns an id string."""
        client = _get_client()
        resp = client.post(
            "/api/trading/backtest",
            json={
                "start_date": "2024-01-01",
                "end_date": "2024-06-30",
                "initial_capital": 500.0,
                "risk_tier": "moderate",
            },
        )
        assert resp.status_code == 200
        data = resp.json()
        assert "id" in data
        assert isinstance(data["id"], str)
        assert len(data["id"]) > 0

    def test_backtest_get_returns_placeholder(self) -> None:
        """GET /api/trading/backtest/{id} returns a result dict."""
        client = _get_client()
        test_id = "test-123"
        resp = client.get(f"/api/trading/backtest/{test_id}")
        assert resp.status_code == 200
        data = resp.json()
        assert data["id"] == test_id

    def test_decisions_returns_list(self) -> None:
        """GET /api/trading/decisions returns a list."""
        client = _get_client()
        resp = client.get("/api/trading/decisions")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)

    def test_metrics_returns_expected_keys(self) -> None:
        """GET /api/trading/metrics returns a dict with expected metric keys."""
        client = _get_client()
        resp = client.get("/api/trading/metrics")
        assert resp.status_code == 200
        data = resp.json()
        expected_keys = {
            "total_portfolio_value",
            "active_pool",
            "reserve_pool",
            "unrealized_pnl",
            "realized_pnl",
            "daily_pnl",
            "win_rate",
            "profit_factor",
            "sharpe_ratio",
            "max_drawdown",
            "portfolio_heat",
        }
        assert expected_keys.issubset(data.keys()), (
            f"Missing keys: {expected_keys - data.keys()}"
        )

    def test_metrics_history_returns_list(self) -> None:
        """GET /api/trading/metrics/history returns a list."""
        client = _get_client()
        resp = client.get("/api/trading/metrics/history")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)

    def test_notification_config_returns_dict(self) -> None:
        """GET /api/trading/notifications/config returns a dict."""
        client = _get_client()
        resp = client.get("/api/trading/notifications/config")
        assert resp.status_code == 200
        data = resp.json()
        assert "sms_enabled" in data
        assert "email_enabled" in data

    def test_notification_history_returns_list(self) -> None:
        """GET /api/trading/notifications/history returns a list."""
        client = _get_client()
        resp = client.get("/api/trading/notifications/history")
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)
