"""Tests for the paper trading adapter - local order simulation and state sync.

Validates position tracking, order fills, idempotency, cash management,
and the PaperAccount/PaperPosition data structures.
"""
import pytest

from services.adapters.broker_adapter import (
    OrderRequest,
    OrderSide,
    OrderStatus,
    OrderType,
    PositionInfo,
    TradingMode,
)
from services.adapters.paper_trading import (
    PaperAccount,
    PaperPosition,
    PaperTradingAdapter,
)

# ---------------------------------------------------------------------------
# PaperPosition tests
# ---------------------------------------------------------------------------


class TestPaperPosition:
    def test_new_position_is_not_open(self):
        pos = PaperPosition(ticker="AAPL")
        assert not pos.is_open
        assert pos.quantity == 0.0

    def test_buy_fill_opens_position(self):
        pos = PaperPosition(ticker="AAPL")
        pnl = pos.apply_fill(OrderSide.BUY, 10, 150.0)
        assert pos.is_open
        assert pos.quantity == 10
        assert pos.avg_entry_price == 150.0
        assert pnl == 0.0

    def test_sell_fill_realizes_pnl(self):
        pos = PaperPosition(ticker="AAPL", quantity=10, avg_entry_price=150.0)
        pnl = pos.apply_fill(OrderSide.SELL, 5, 160.0)
        assert pos.quantity == 5
        assert pnl == 50.0  # 5 shares * $10 gain
        assert pos.realized_pnl == 50.0

    def test_sell_all_closes_position(self):
        pos = PaperPosition(ticker="AAPL", quantity=10, avg_entry_price=150.0)
        pos.apply_fill(OrderSide.SELL, 10, 140.0)
        assert not pos.is_open
        assert pos.quantity == 0
        assert pos.avg_entry_price == 0.0
        assert pos.realized_pnl == -100.0  # 10 * -$10

    def test_buy_averages_up(self):
        pos = PaperPosition(ticker="AAPL", quantity=10, avg_entry_price=100.0)
        pos.apply_fill(OrderSide.BUY, 10, 200.0)
        assert pos.quantity == 20
        assert pos.avg_entry_price == 150.0  # (1000 + 2000) / 20

    def test_to_position_info(self):
        pos = PaperPosition(ticker="AAPL", quantity=10, avg_entry_price=150.0)
        info = pos.to_position_info(current_price=160.0)
        assert isinstance(info, PositionInfo)
        assert info.ticker == "AAPL"
        assert info.quantity == 10
        assert info.unrealized_pnl == 100.0  # 10 * $10
        assert info.market_value == 1600.0

    def test_to_position_info_no_current_price(self):
        pos = PaperPosition(ticker="AAPL", quantity=10, avg_entry_price=150.0)
        info = pos.to_position_info()
        assert info.current_price == 150.0
        assert info.unrealized_pnl == 0.0


# ---------------------------------------------------------------------------
# PaperAccount tests
# ---------------------------------------------------------------------------


class TestPaperAccount:
    def test_default_account(self):
        acct = PaperAccount()
        assert acct.cash == 100_000.0
        assert acct.portfolio_value == 100_000.0
        assert acct.buying_power == 100_000.0

    def test_custom_initial_cash(self):
        acct = PaperAccount(initial_cash=50_000.0)
        assert acct.cash == 50_000.0

    def test_get_position_creates_new(self):
        acct = PaperAccount()
        pos = acct.get_position("AAPL")
        assert pos.ticker == "AAPL"
        assert pos.quantity == 0

    def test_get_position_returns_existing(self):
        acct = PaperAccount()
        pos1 = acct.get_position("AAPL")
        pos1.quantity = 10
        pos2 = acct.get_position("AAPL")
        assert pos2.quantity == 10

    def test_portfolio_value_includes_positions(self):
        acct = PaperAccount(initial_cash=10_000.0)
        acct.cash = 5_000.0
        pos = acct.get_position("AAPL")
        pos.quantity = 10
        pos.avg_entry_price = 100.0
        # portfolio = cash + position value = 5000 + 1000 = 6000
        assert acct.portfolio_value == 6_000.0

    def test_to_account_info(self):
        acct = PaperAccount(account_id="test-acct")
        info = acct.to_account_info()
        assert info.account_id == "test-acct"
        assert info.mode == TradingMode.PAPER
        assert info.cash == 100_000.0


# ---------------------------------------------------------------------------
# PaperTradingAdapter tests
# ---------------------------------------------------------------------------


class TestPaperTradingAdapterBasics:
    def test_mode_is_paper(self):
        adapter = PaperTradingAdapter()
        assert adapter.mode == TradingMode.PAPER

    def test_source_type(self):
        adapter = PaperTradingAdapter()
        assert adapter.source_type() == "broker"

    def test_custom_initial_cash(self):
        adapter = PaperTradingAdapter(initial_cash=50_000.0)
        assert adapter.account.cash == 50_000.0


@pytest.mark.asyncio
class TestPaperTradingSubmitOrder:
    async def test_buy_market_order_fills(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL",
            side=OrderSide.BUY,
            quantity=10,
            order_type=OrderType.LIMIT,
            limit_price=150.0,
        )
        resp = await adapter.submit_order(order)
        assert resp.status == OrderStatus.FILLED
        assert resp.filled_quantity == 10
        assert resp.filled_avg_price == 150.0
        assert resp.ok
        # Cash should decrease
        assert adapter.account.cash < 100_000.0

    async def test_sell_order_realizes_pnl(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        # Buy first
        buy = OrderRequest(ticker="AAPL", side=OrderSide.BUY, quantity=10,
                           order_type=OrderType.LIMIT, limit_price=150.0)
        await adapter.submit_order(buy)

        # Sell at higher price
        sell = OrderRequest(ticker="AAPL", side=OrderSide.SELL, quantity=10,
                            order_type=OrderType.LIMIT, limit_price=160.0)
        resp = await adapter.submit_order(sell)
        assert resp.status == OrderStatus.FILLED
        assert resp.raw_response["realized_pnl"] == 100.0  # 10 * $10

    async def test_insufficient_cash_rejects(self):
        adapter = PaperTradingAdapter(initial_cash=1_000.0)
        order = OrderRequest(
            ticker="AAPL",
            side=OrderSide.BUY,
            quantity=100,
            order_type=OrderType.LIMIT,
            limit_price=150.0,
        )
        resp = await adapter.submit_order(order)
        assert resp.status == OrderStatus.REJECTED
        assert "Insufficient cash" in resp.error

    async def test_insufficient_shares_rejects(self):
        adapter = PaperTradingAdapter()
        order = OrderRequest(
            ticker="AAPL",
            side=OrderSide.SELL,
            quantity=10,
            order_type=OrderType.LIMIT,
            limit_price=150.0,
        )
        resp = await adapter.submit_order(order)
        assert resp.status == OrderStatus.REJECTED
        assert "Insufficient shares" in resp.error

    async def test_idempotency_returns_same_response(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL",
            side=OrderSide.BUY,
            quantity=10,
            order_type=OrderType.LIMIT,
            limit_price=150.0,
            idempotency_key="test-key-1",
        )
        resp1 = await adapter.submit_order(order)
        resp2 = await adapter.submit_order(order)
        assert resp1.broker_order_id == resp2.broker_order_id
        assert resp1.status == resp2.status
        # Cash should only be deducted once
        assert adapter.account.cash == pytest.approx(100_000.0 - 1500.0)

    async def test_order_events_recorded(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=5,
            order_type=OrderType.LIMIT, limit_price=100.0,
        )
        await adapter.submit_order(order)
        events = adapter.account.order_events
        event_types = [e["event_type"] for e in events]
        assert "submitted" in event_types
        assert "accepted" in event_types
        assert "fill" in event_types

    async def test_stop_order_fills_at_stop_price(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=10,
            order_type=OrderType.STOP, stop_price=145.0,
        )
        resp = await adapter.submit_order(order)
        assert resp.filled_avg_price == 145.0


@pytest.mark.asyncio
class TestPaperTradingCancelAndStatus:
    async def test_cancel_filled_order_rejected(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=5,
            order_type=OrderType.LIMIT, limit_price=100.0,
        )
        resp = await adapter.submit_order(order)
        cancel_resp = await adapter.cancel_order(resp.broker_order_id)
        assert cancel_resp.status == OrderStatus.REJECTED
        assert "filled" in cancel_resp.error.lower()

    async def test_cancel_unknown_order(self):
        adapter = PaperTradingAdapter()
        resp = await adapter.cancel_order("nonexistent-id")
        assert resp.status == OrderStatus.REJECTED
        assert "not found" in resp.error

    async def test_get_order_status(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=5,
            order_type=OrderType.LIMIT, limit_price=100.0,
        )
        resp = await adapter.submit_order(order)
        status = await adapter.get_order_status(resp.broker_order_id)
        assert status.status == OrderStatus.FILLED

    async def test_get_unknown_order_status(self):
        adapter = PaperTradingAdapter()
        resp = await adapter.get_order_status("nonexistent")
        assert resp.status == OrderStatus.REJECTED


@pytest.mark.asyncio
class TestPaperTradingPositionsAndAccount:
    async def test_get_positions_empty(self):
        adapter = PaperTradingAdapter()
        positions = await adapter.get_positions()
        assert positions == []

    async def test_get_positions_after_buy(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        order = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=10,
            order_type=OrderType.LIMIT, limit_price=150.0,
        )
        await adapter.submit_order(order)
        positions = await adapter.get_positions()
        assert len(positions) == 1
        assert positions[0].ticker == "AAPL"
        assert positions[0].quantity == 10

    async def test_get_account(self):
        adapter = PaperTradingAdapter(initial_cash=50_000.0, account_id="test")
        info = await adapter.get_account()
        assert info.account_id == "test"
        assert info.cash == 50_000.0
        assert info.mode == TradingMode.PAPER


@pytest.mark.asyncio
class TestPaperTradingFetch:
    async def test_fetch_positions(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        buy = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=5,
            order_type=OrderType.LIMIT, limit_price=100.0,
        )
        await adapter.submit_order(buy)
        result = await adapter.fetch("AAPL", {"endpoint": "positions"})
        assert result.ok
        assert len(result.items) == 1
        assert result.metadata["provider"] == "paper"

    async def test_fetch_account(self):
        adapter = PaperTradingAdapter()
        result = await adapter.fetch("*", {"endpoint": "account"})
        assert result.ok
        assert result.items[0]["mode"] == "paper"

    async def test_fetch_orders(self):
        adapter = PaperTradingAdapter(initial_cash=100_000.0)
        buy = OrderRequest(
            ticker="AAPL", side=OrderSide.BUY, quantity=5,
            order_type=OrderType.LIMIT, limit_price=100.0,
        )
        await adapter.submit_order(buy)
        result = await adapter.fetch("AAPL", {"endpoint": "orders"})
        assert len(result.items) == 1

    async def test_fetch_empty_position(self):
        adapter = PaperTradingAdapter()
        result = await adapter.fetch("AAPL", {"endpoint": "positions"})
        assert len(result.items) == 0
