"""
Unit tests for TerminalService and UserService.

Mocks database, Redis, and Celery to test business logic in isolation.
"""

import json
import pytest
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
from datetime import datetime

from app.modules.terminal.auto.service import TerminalService
from app.modules.terminal.auto.models import TradingInstance, InstanceStatus, ScriptType
from app.core.exceptions import BadRequestError, NotFoundError


# ============================================================================
# TerminalService — Credential Management
# ============================================================================


class TestCredentialManagement:
    async def _seed_user_cache(self, redis, user_id=1):
        """Seed Redis user cache so _get_user_session_ttl doesn't hit DB."""
        await redis.set(f"user:{user_id}", json.dumps({"session_ttl_hours": 4}))

    @pytest.mark.asyncio
    async def test_store_kalshi_credentials(self, mock_db, fake_redis_async):
        await self._seed_user_cache(fake_redis_async)
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.store_session_credentials(
            platform="kalshi",
            api_key="test-api-key",
            rsa_key="test-rsa-key",
        )
        stored = await fake_redis_async.get("user:1:credentials")
        assert stored is not None
        data = json.loads(stored)
        assert data["platform"] == "kalshi"
        assert "api_key" in data
        assert data["api_key"] != "test-api-key"  # encrypted

    @pytest.mark.asyncio
    async def test_store_turbine_credentials(self, mock_db, fake_redis_async):
        await self._seed_user_cache(fake_redis_async)
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.store_session_credentials(
            platform="turbine",
            turbine_private_key="0x" + "ab" * 32,
        )
        stored = await fake_redis_async.get("user:1:credentials")
        data = json.loads(stored)
        assert data["platform"] == "turbine"

    @pytest.mark.asyncio
    async def test_store_polymarket_credentials(self, mock_db, fake_redis_async):
        await self._seed_user_cache(fake_redis_async)
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.store_session_credentials(
            platform="polymarket",
            poly_private_key="0x" + "cd" * 32,
        )
        stored = await fake_redis_async.get("user:1:credentials")
        data = json.loads(stored)
        assert data["platform"] == "polymarket"

    @pytest.mark.asyncio
    async def test_get_credentials_expired(self, mock_db, fake_redis_async):
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        with pytest.raises(BadRequestError, match="Session expired"):
            await service.get_session_credentials()

    @pytest.mark.asyncio
    async def test_clear_credentials(self, mock_db, fake_redis_async):
        await self._seed_user_cache(fake_redis_async)
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.store_session_credentials(
            platform="kalshi", api_key="k", rsa_key="r"
        )
        await service.clear_session_credentials()
        stored = await fake_redis_async.get("user:1:credentials")
        assert stored is None


# ============================================================================
# TerminalService — Instance Management
# ============================================================================


class TestInstanceManagement:
    def _make_instance(self, status=InstanceStatus.RUNNING, **kwargs):
        inst = MagicMock(spec=TradingInstance)
        inst.id = kwargs.get("id", 1)
        inst.user_id = kwargs.get("user_id", 1)
        inst.status = status
        inst.script = ScriptType.AUTO1
        inst.markets = {"markets": ["KXBTC-25MAR05-BTC1-YES"]}
        inst.config = {"min_spread": 2, "max_spread": 7}
        inst.position = kwargs.get("position", 0)
        inst.pnl = kwargs.get("pnl", 0.0)
        inst.start_time = "2025-03-05 10:00:00"
        inst.celery_task_id = "celery-task-123"
        inst.orderbook_data = None
        inst.current_increment = None
        inst.created_at = datetime.utcnow()
        return inst

    @pytest.mark.asyncio
    async def test_get_instance_not_found(self, mock_db, fake_redis_async):
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        with pytest.raises(NotFoundError, match="not found"):
            await service.get_instance(999)

    @pytest.mark.asyncio
    async def test_get_instance_found(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        result = await service.get_instance(1)
        assert result == inst

    @pytest.mark.asyncio
    async def test_stop_already_stopped_raises(self, mock_db, fake_redis_async):
        inst = self._make_instance(status=InstanceStatus.STOPPED)
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        with pytest.raises(BadRequestError, match="already stopped"):
            await service.stop_instance(1)

    @pytest.mark.asyncio
    async def test_stop_sends_redis_command(self, mock_db, fake_redis_async):
        inst = self._make_instance(status=InstanceStatus.RUNNING)
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.stop_instance(1)

        # Verify Redis command was pushed
        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "stop"

    @pytest.mark.asyncio
    async def test_force_stop_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance(status=InstanceStatus.RUNNING)
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.force_stop_instance(1)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "force_stop"

    @pytest.mark.asyncio
    async def test_toggle_pause_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.toggle_pause_instance(1)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "toggle_pause"

    @pytest.mark.asyncio
    async def test_single_fire_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.single_fire_instance(1)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "single_fire"

    @pytest.mark.asyncio
    async def test_cancel_orders_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.cancel_orders_instance(1)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "cancel_orders"

    @pytest.mark.asyncio
    async def test_toggle_fair_value_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        await service.toggle_fair_value_instance(1)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        assert json.loads(cmd_data)["action"] == "toggle_fair_value"

    @pytest.mark.asyncio
    async def test_toggle_jump_sends_command(self, mock_db, fake_redis_async):
        inst = self._make_instance()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        result = await service.toggle_jump(1, market_index=0)

        cmd_data = await fake_redis_async.lpop("trading:instance:1:command")
        cmd = json.loads(cmd_data)
        assert cmd["action"] == "toggle_jump"
        assert cmd["market_index"] == 0

    @pytest.mark.asyncio
    async def test_end_instance_sets_dead(self, mock_db, fake_redis_async):
        inst = self._make_instance(status=InstanceStatus.STOPPED)
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        result = await service.end_instance(1)
        assert inst.status == InstanceStatus.DEAD


# ============================================================================
# TerminalService — format_instance_response
# ============================================================================


class TestFormatResponse:
    def test_format_positive_pnl(self, mock_db, fake_redis_async):
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        inst = MagicMock()
        inst.id = 1
        inst.script = ScriptType.AUTO1
        inst.markets = {"markets": ["KXBTC-25MAR05-BTC1-YES"]}
        inst.status = InstanceStatus.RUNNING
        inst.start_time = "2025-03-05 10:00:00"
        inst.position = 50
        inst.pnl = 15.50
        inst.config = {"grid_mode": False}
        inst.orderbook_data = None
        inst.celery_task_id = "abc"
        inst.current_increment = None

        resp = service.format_instance_response(inst)
        assert resp.pnl == "+$15.50"
        assert resp.trade_mode == "Join"

    def test_format_negative_pnl(self, mock_db, fake_redis_async):
        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        inst = MagicMock()
        inst.id = 1
        inst.script = ScriptType.AUTO2
        inst.markets = {"markets": ["M1", "M2"]}
        inst.status = InstanceStatus.RUNNING
        inst.start_time = "2025-03-05 10:00:00"
        inst.position = -20
        inst.pnl = -8.75
        inst.config = {"grid_mode": True}
        inst.orderbook_data = None
        inst.celery_task_id = "def"
        inst.current_increment = None

        resp = service.format_instance_response(inst)
        assert resp.pnl == "-$8.75"
        assert resp.trade_mode == "Grid"

    @pytest.mark.asyncio
    async def test_get_instance_status_from_redis(self, mock_db, fake_redis_async):
        inst = MagicMock()
        inst.id = 1
        inst.status = InstanceStatus.RUNNING
        inst.position = 0
        inst.pnl = 0.0
        inst.orderbook_data = {}
        inst.current_increment = {}
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = inst
        mock_db.execute = AsyncMock(return_value=mock_result)

        # Put status data in Redis
        status_data = {"position": 25, "pnl": 5.0, "orderbook": {}, "current_increment": {}}
        await fake_redis_async.set(
            "trading:instance:1:status",
            json.dumps(status_data),
        )

        service = TerminalService(mock_db, user_id=1, redis=fake_redis_async)
        result = await service.get_instance_status(1)
        assert result["position"] == 25
        assert result["status"] == "running"
