"""Property-based tests for operator approval workflow.

Feature: operator-approval-workflow (bugfix)

Bug Condition Exploration:
  The `requires_approval()` function is correct at the function level — when
  `auto_approve_paper=False`, it returns `True` for PAPER mode.  However, the
  config JSON stored in `risk_configs` is always `{}`, so
  `PortfolioRiskConfig.from_db_json({})` produces `auto_approve_paper=True`,
  and no UI/endpoint ever sets it to `False`.  The approval gate is therefore
  never reached for paper orders.

Tests:
  - Property 1 (Bug Condition): Paper Mode Approval Bypass
"""
from __future__ import annotations

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

from services.risk.approval import requires_approval
from services.risk.engine import (
    OperatorApproval,
    PortfolioRiskConfig,
    TradingMode,
)

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

def _risk_config_with_approval_disabled() -> st.SearchStrategy[PortfolioRiskConfig]:
    """Generate random PortfolioRiskConfig instances with auto_approve_paper=False."""
    return st.builds(
        PortfolioRiskConfig,
        trading_mode=st.just(TradingMode.PAPER),
        operator_approval=st.builds(
            OperatorApproval,
            auto_approve_paper=st.just(False),
            require_approval_for_live=st.booleans(),
            approval_timeout_minutes=st.integers(min_value=1, max_value=1440),
        ),
    )


# ---------------------------------------------------------------------------
# Property 1: Bug Condition — Paper Mode Approval Bypass
# ---------------------------------------------------------------------------


class TestBugConditionExploration:
    """Exploration tests to surface the bug condition.

    **Validates: Requirements 1.1, 1.2, 1.5, 2.1**

    The bug is that paper orders always bypass approval because the config
    JSON in the database never contains operator_approval settings, so
    `auto_approve_paper` always defaults to `True`.
    """

    @given(config=_risk_config_with_approval_disabled())
    @settings(max_examples=100)
    def test_requires_approval_returns_true_when_auto_approve_paper_is_false(
        self,
        config: PortfolioRiskConfig,
    ) -> None:
        """Function-level correctness: requires_approval() returns True
        when auto_approve_paper is explicitly False and mode is PAPER.

        This property is expected to PASS — the function itself is correct.
        The bug is at the config/persistence layer, not the function.
        """
        result = requires_approval(config, TradingMode.PAPER)
        assert result is True, (
            f"requires_approval() returned False for PAPER mode with "
            f"auto_approve_paper=False. Config: {config.operator_approval}"
        )

    def test_from_db_json_empty_config_defaults_to_auto_approve(self) -> None:
        """Root cause documentation: PortfolioRiskConfig.from_db_json({})
        produces auto_approve_paper=True by default.

        The bug fix was implemented at the API layer — dedicated endpoints
        now allow operators to set auto_approve_paper=False. The default
        behavior (True) is intentional and correct: empty config JSON means
        paper orders are auto-approved until an operator explicitly opts in
        to the approval workflow.
        """
        config = PortfolioRiskConfig.from_db_json({})
        # The default: empty JSON defaults auto_approve_paper to True.
        # This is the expected behavior — the API endpoints now allow
        # operators to change this setting when needed.
        assert config.operator_approval.auto_approve_paper is True, (
            f"PortfolioRiskConfig.from_db_json({{}}) produced "
            f"auto_approve_paper={config.operator_approval.auto_approve_paper}. "
            f"Expected True as the default — the fix is at the API layer."
        )

    def test_no_dedicated_approval_config_endpoint(self) -> None:
        """Verify there is no dedicated endpoint to update operator_approval
        settings in the risk config.

        This test is EXPECTED TO FAIL because the endpoint does not exist yet.
        It imports the FastAPI app and checks that no route matches
        PUT /api/admin/trading/approval-config.
        """
        from services.api.app import app

        approval_config_routes = [
            route
            for route in app.routes
            if hasattr(route, "path")
            and route.path == "/api/admin/trading/approval-config"
        ]
        assert len(approval_config_routes) > 0, (
            "No dedicated endpoint exists for PUT /api/admin/trading/approval-config. "
            "The existing PUT /api/admin/trading/config replaces the entire config JSON, "
            "and no UI calls it with operator_approval fields. "
            "This means there is no way for the operator to toggle auto_approve_paper."
        )


# ---------------------------------------------------------------------------
# Strategies for Preservation Properties
# ---------------------------------------------------------------------------


def _live_mode_config() -> st.SearchStrategy[PortfolioRiskConfig]:
    """Generate random PortfolioRiskConfig instances in LIVE mode."""
    return st.builds(
        PortfolioRiskConfig,
        trading_mode=st.just(TradingMode.LIVE),
        operator_approval=st.builds(
            OperatorApproval,
            auto_approve_paper=st.booleans(),
            require_approval_for_live=st.booleans(),
            approval_timeout_minutes=st.integers(min_value=1, max_value=1440),
        ),
    )


def _disabled_mode_config() -> st.SearchStrategy[PortfolioRiskConfig]:
    """Generate random PortfolioRiskConfig instances in DISABLED mode."""
    return st.builds(
        PortfolioRiskConfig,
        trading_mode=st.just(TradingMode.DISABLED),
        operator_approval=st.builds(
            OperatorApproval,
            auto_approve_paper=st.booleans(),
            require_approval_for_live=st.booleans(),
            approval_timeout_minutes=st.integers(min_value=1, max_value=1440),
        ),
    )


def _paper_mode_auto_approve_config() -> st.SearchStrategy[PortfolioRiskConfig]:
    """Generate random PortfolioRiskConfig instances in PAPER mode with auto_approve_paper=True."""
    return st.builds(
        PortfolioRiskConfig,
        trading_mode=st.just(TradingMode.PAPER),
        operator_approval=st.builds(
            OperatorApproval,
            auto_approve_paper=st.just(True),
            require_approval_for_live=st.booleans(),
            approval_timeout_minutes=st.integers(min_value=1, max_value=1440),
        ),
    )


# ---------------------------------------------------------------------------
# Property 2: Preservation — Non-Paper-Toggle Approval Behavior
# ---------------------------------------------------------------------------


class TestPreservationProperties:
    """Preservation tests for non-buggy approval behavior.

    **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6**

    These tests capture the existing correct behavior of requires_approval()
    for inputs that are NOT affected by the bug (i.e., non-paper-toggle
    scenarios). They must PASS on unfixed code and continue to PASS after
    the fix is applied, ensuring no regressions.
    """

    @given(config=_live_mode_config())
    @settings(max_examples=100)
    def test_live_mode_result_equals_require_approval_for_live(
        self,
        config: PortfolioRiskConfig,
    ) -> None:
        """For all LIVE mode configs, requires_approval() returns the value
        of require_approval_for_live.

        Observed behavior on unfixed code:
          - requires_approval(config, LIVE) returns True when require_approval_for_live=True
          - requires_approval(config, LIVE) returns False when require_approval_for_live=False
        """
        result = requires_approval(config, TradingMode.LIVE)
        expected = config.operator_approval.require_approval_for_live
        assert result is expected, (
            f"LIVE mode: requires_approval() returned {result}, "
            f"expected {expected} (require_approval_for_live={expected}). "
            f"Config: {config.operator_approval}"
        )

    @given(config=_disabled_mode_config())
    @settings(max_examples=100)
    def test_disabled_mode_always_returns_false(
        self,
        config: PortfolioRiskConfig,
    ) -> None:
        """For all DISABLED mode configs, requires_approval() always returns False.

        Observed behavior on unfixed code:
          - requires_approval(config, DISABLED) returns False for all configs
        Orders are blocked upstream by the risk engine in disabled mode,
        so the approval gate is irrelevant.
        """
        result = requires_approval(config, TradingMode.DISABLED)
        assert result is False, (
            f"DISABLED mode: requires_approval() returned {result}, "
            f"expected False. Config: {config.operator_approval}"
        )

    @given(config=_paper_mode_auto_approve_config())
    @settings(max_examples=100)
    def test_paper_mode_auto_approve_true_returns_false(
        self,
        config: PortfolioRiskConfig,
    ) -> None:
        """For all PAPER mode configs with auto_approve_paper=True,
        requires_approval() always returns False.

        Observed behavior on unfixed code:
          - requires_approval(config, PAPER) returns False when auto_approve_paper=True
        This is the default (non-buggy) paper mode behavior — paper orders
        are auto-approved when the setting is True.
        """
        result = requires_approval(config, TradingMode.PAPER)
        assert result is False, (
            f"PAPER mode with auto_approve_paper=True: requires_approval() "
            f"returned {result}, expected False. Config: {config.operator_approval}"
        )
