"""
Unit tests for BaseMarketMaker (mm_core.py).

Tests command processing, fair value tracking, jump mode,
single fire mode, and state transitions.
"""

import json
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from collections import deque


# ============================================================================
# BaseMarketMaker — State Management
# ============================================================================


class TestBaseMarketMakerState:
    def _make_mm(self):
        """Create BaseMarketMaker with mocked parent (KalshiAPITrader)."""
        with patch("app.modules.terminal.auto.mm_core.KalshiAPITrader.__init__", return_value=None):
            from app.modules.terminal.auto.mm_core import BaseMarketMaker
            mm = BaseMarketMaker.__new__(BaseMarketMaker)
            # Initialize state that __init__ sets
            mm.running = False
            mm.stopping = False
            mm.active = False
            mm.paused = False
            mm.waiting_for_manual_resume = False
            mm.is_rebalancing = False
            mm.contract_increment = 3
            mm.single_fire_mode = False
            mm.single_fire_cycles_completed = 0
            mm.fair_value_enabled = False
            mm.fair_value_history = deque(maxlen=10)
            mm.current_fair_value = None
            mm.jump_active = {}
            mm.jump_target = {}
            mm.instance_id = 1
            mm.redis_client = None
            mm.session_start_time = None
            mm.cycles_completed = 0
            mm.order_ids = {}
            mm.last_prices = {}
            return mm

    def test_initial_state(self):
        mm = self._make_mm()
        assert not mm.running
        assert not mm.paused
        assert not mm.stopping
        assert not mm.fair_value_enabled
        assert mm.cycles_completed == 0

    def test_toggle_jump(self):
        mm = self._make_mm()
        market_id = "KXBTC-25MAR05-BTC1"

        # First toggle — ON
        mm.toggle_jump(market_id)
        assert mm.jump_active[market_id] is True

        # Second toggle — OFF, target cleared
        mm.jump_target[market_id] = 50
        mm.toggle_jump(market_id)
        assert mm.jump_active[market_id] is False
        assert mm.jump_target[market_id] is None

    def test_update_fair_value(self):
        mm = self._make_mm()
        mm.update_fair_value(45.0, 55.0)
        assert mm.current_fair_value == 50.0  # midpoint of 45 and 55

        mm.update_fair_value(40.0, 60.0)
        # Rolling avg of (50, 50) = 50
        assert mm.current_fair_value == 50.0

    def test_update_fair_value_with_none(self):
        mm = self._make_mm()
        mm.update_fair_value(None, 55.0)
        assert mm.current_fair_value is None

        mm.update_fair_value(45.0, None)
        assert mm.current_fair_value is None

    def test_fair_value_rolling_window(self):
        mm = self._make_mm()
        # Fill 10 entries
        for i in range(10):
            mm.update_fair_value(float(i), float(100 - i))
        assert len(mm.fair_value_history) == 10

        # Add one more — oldest drops off (maxlen=10)
        mm.update_fair_value(50.0, 50.0)
        assert len(mm.fair_value_history) == 10


# ============================================================================
# BaseMarketMaker — Sync Redis Commands
# ============================================================================


class TestSyncRedisCommands:
    def _make_mm_with_redis(self):
        with patch("app.modules.terminal.auto.mm_core.KalshiAPITrader.__init__", return_value=None):
            from app.modules.terminal.auto.mm_core import BaseMarketMaker
            mm = BaseMarketMaker.__new__(BaseMarketMaker)
            mm.running = False
            mm.stopping = False
            mm.active = True
            mm.paused = False
            mm.waiting_for_manual_resume = False
            mm.is_rebalancing = False
            mm.contract_increment = 3
            mm.single_fire_mode = False
            mm.single_fire_cycles_completed = 0
            mm.fair_value_enabled = False
            mm.fair_value_history = deque(maxlen=10)
            mm.current_fair_value = None
            mm.jump_active = {}
            mm.jump_target = {}
            mm.instance_id = 42
            mm.order_ids = {}
            mm.last_prices = {}
            mm.session_start_time = None
            mm.cycles_completed = 0

            # Mock Redis
            mm.redis_client = MagicMock()
            return mm

    def test_toggle_pause_command(self):
        mm = self._make_mm_with_redis()
        cmd = json.dumps({"action": "toggle_pause"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.paused is True

    def test_resume_from_pause(self):
        mm = self._make_mm_with_redis()
        mm.paused = True
        cmd = json.dumps({"action": "toggle_pause"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.paused is False

    def test_single_fire_when_paused(self):
        mm = self._make_mm_with_redis()
        mm.paused = True
        cmd = json.dumps({"action": "single_fire"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.single_fire_mode is True
        assert mm.paused is False

    def test_single_fire_when_running_ignored(self):
        mm = self._make_mm_with_redis()
        mm.paused = False
        cmd = json.dumps({"action": "single_fire"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.single_fire_mode is False  # ignored

    def test_toggle_fair_value_on(self):
        mm = self._make_mm_with_redis()
        cmd = json.dumps({"action": "toggle_fair_value"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.fair_value_enabled is True

    def test_toggle_fair_value_off_clears_history(self):
        mm = self._make_mm_with_redis()
        mm.fair_value_enabled = True
        mm.fair_value_history.append(50.0)
        mm.current_fair_value = 50.0
        cmd = json.dumps({"action": "toggle_fair_value"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.fair_value_enabled is False
        assert len(mm.fair_value_history) == 0
        assert mm.current_fair_value is None

    def test_stop_command(self):
        mm = self._make_mm_with_redis()
        cmd = json.dumps({"action": "stop"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.stopping is True

    def test_force_stop_command(self):
        mm = self._make_mm_with_redis()
        mm.cancel_all_orders = MagicMock()
        cmd = json.dumps({"action": "force_stop"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        mm.process_redis_commands()
        assert mm.active is False
        assert mm.stopping is False
        mm.cancel_all_orders.assert_called_once()

    def test_no_commands_noop(self):
        mm = self._make_mm_with_redis()
        mm.redis_client.lpop = MagicMock(return_value=None)
        mm.process_redis_commands()
        assert mm.paused is False  # unchanged

    def test_no_redis_client_noop(self):
        mm = self._make_mm_with_redis()
        mm.redis_client = None
        mm.process_redis_commands()  # should not raise

    def test_no_instance_id_noop(self):
        mm = self._make_mm_with_redis()
        mm.instance_id = None
        mm.process_redis_commands()  # should not raise

    def test_multiple_commands_processed(self):
        mm = self._make_mm_with_redis()
        cmds = [
            json.dumps({"action": "toggle_pause"}),
            json.dumps({"action": "toggle_fair_value"}),
            None,
        ]
        mm.redis_client.lpop = MagicMock(side_effect=cmds)

        mm.process_redis_commands()
        assert mm.paused is True
        assert mm.fair_value_enabled is True

    def test_malformed_command_handled(self):
        mm = self._make_mm_with_redis()
        mm.redis_client.lpop = MagicMock(side_effect=["not-json", None])
        mm.process_redis_commands()  # should not raise


# ============================================================================
# BaseMarketMaker — Async Redis Commands
# ============================================================================


class TestAsyncRedisCommands:
    def _make_mm_async(self):
        with patch("app.modules.terminal.auto.mm_core.KalshiAPITrader.__init__", return_value=None):
            from app.modules.terminal.auto.mm_core import BaseMarketMaker
            mm = BaseMarketMaker.__new__(BaseMarketMaker)
            mm.running = False
            mm.stopping = False
            mm.active = True
            mm.paused = False
            mm.waiting_for_manual_resume = False
            mm.is_rebalancing = False
            mm.contract_increment = 3
            mm.single_fire_mode = False
            mm.single_fire_cycles_completed = 0
            mm.fair_value_enabled = False
            mm.fair_value_history = deque(maxlen=10)
            mm.current_fair_value = None
            mm.jump_active = {}
            mm.jump_target = {}
            mm.instance_id = 99
            mm.order_ids = {}
            mm.last_prices = {}
            mm.session_start_time = None
            mm.cycles_completed = 0

            # Mock Redis (sync-style lpop used even in async method)
            mm.redis_client = MagicMock()
            return mm

    @pytest.mark.asyncio
    async def test_async_toggle_pause(self):
        mm = self._make_mm_async()
        cmd = json.dumps({"action": "toggle_pause"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        await mm.process_redis_commands_async()
        assert mm.paused is True

    @pytest.mark.asyncio
    async def test_async_stop(self):
        mm = self._make_mm_async()
        cmd = json.dumps({"action": "stop"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        await mm.process_redis_commands_async()
        assert mm.stopping is True

    @pytest.mark.asyncio
    async def test_async_cancel_orders(self):
        mm = self._make_mm_async()
        mm.cancel_order = AsyncMock()
        mm.order_ids = {"yes": "order-1", "no": "order-2"}
        cmd = json.dumps({"action": "cancel_orders"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        await mm.process_redis_commands_async()
        assert mm.cancel_order.call_count == 2

    @pytest.mark.asyncio
    async def test_async_force_stop_cancels_and_stops(self):
        mm = self._make_mm_async()
        mm.cancel_order = AsyncMock()
        mm.order_ids = {"yes": "order-1"}
        cmd = json.dumps({"action": "force_stop"})
        mm.redis_client.lpop = MagicMock(side_effect=[cmd, None])

        await mm.process_redis_commands_async()
        assert mm.active is False
        mm.cancel_order.assert_called_once()
