"""Tests for wallet/flow analytics module."""

from unittest.mock import MagicMock, patch

import pytest
import requests

from horizon.flow import (
    Holder,
    MarketFlow,
    Trade,
    WalletPosition,
    WalletProfile,
    analyze_market_flow,
    get_market_trades,
    get_top_holders,
    get_wallet_positions,
    get_wallet_profile,
    get_wallet_trades,
    get_wallet_value,
)

# ---------------------------------------------------------------------------
# Fixtures: mock API responses
# ---------------------------------------------------------------------------

MOCK_TRADES = [
    {
        "proxyWallet": "0xaaa",
        "side": "BUY",
        "outcome": "Yes",
        "size": 100.0,
        "price": 0.60,
        "timestamp": 1700000000,
        "slug": "will-btc-hit-100k",
        "title": "Will BTC hit 100k?",
        "conditionId": "0xcond1",
        "asset": "token_yes_1",
        "transactionHash": "0xtx1",
        "pseudonym": "trader_a",
    },
    {
        "proxyWallet": "0xbbb",
        "side": "SELL",
        "outcome": "Yes",
        "size": 50.0,
        "price": 0.62,
        "timestamp": 1700000100,
        "slug": "will-btc-hit-100k",
        "title": "Will BTC hit 100k?",
        "conditionId": "0xcond1",
        "asset": "token_yes_1",
        "transactionHash": "0xtx2",
        "pseudonym": "trader_b",
    },
    {
        "proxyWallet": "0xaaa",
        "side": "BUY",
        "outcome": "Yes",
        "size": 200.0,
        "price": 0.58,
        "timestamp": 1700000200,
        "slug": "will-btc-hit-100k",
        "title": "Will BTC hit 100k?",
        "conditionId": "0xcond1",
        "asset": "token_yes_1",
    },
]

MOCK_POSITIONS = [
    {
        "proxyWallet": "0xaaa",
        "slug": "will-btc-hit-100k",
        "title": "Will BTC hit 100k?",
        "conditionId": "0xcond1",
        "asset": "token_yes_1",
        "outcome": "Yes",
        "size": 300.0,
        "avgPrice": 0.55,
        "curPrice": 0.62,
        "currentValue": 186.0,
        "cashPnl": 21.0,
        "percentPnl": 12.7,
    },
]

MOCK_HOLDERS = [
    {
        "token": "token_yes_1",
        "holders": [
            {
                "proxyWallet": "0xwhale1",
                "amount": 10000.0,
                "outcomeIndex": 0,
                "pseudonym": "whale_1",
                "name": "Whale One",
            },
            {
                "proxyWallet": "0xwhale2",
                "amount": 5000.0,
                "outcomeIndex": 0,
                "pseudonym": "whale_2",
            },
        ],
    },
    {
        "token": "token_no_1",
        "holders": [
            {
                "proxyWallet": "0xwhale3",
                "amount": 8000.0,
                "outcomeIndex": 1,
                "pseudonym": "whale_3",
            },
        ],
    },
]

MOCK_PROFILE = {
    "proxyWallet": "0xaaa",
    "pseudonym": "trader_a",
    "name": "Trader Alpha",
    "bio": "DeFi trader",
    "profileImage": "https://example.com/pic.jpg",
    "xUsername": "trader_alpha",
    "createdAt": "2023-01-15T00:00:00Z",
}

MOCK_VALUE = [{"user": "0xaaa", "value": 12345.67}]


def _mock_response(json_data, status_code=200):
    """Create a mock requests.Response."""
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = json_data
    resp.raise_for_status.return_value = None
    return resp


# ---------------------------------------------------------------------------
# get_market_trades
# ---------------------------------------------------------------------------


class TestGetMarketTrades:
    @patch("horizon.flow.requests.get")
    def test_basic_fetch(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_TRADES)

        trades = get_market_trades("0xcond1", limit=10)

        assert len(trades) == 3
        assert all(isinstance(t, Trade) for t in trades)
        mock_get.assert_called_once()
        call_params = mock_get.call_args[1]["params"]
        assert call_params["market"] == "0xcond1"
        assert call_params["limit"] == 10

    @patch("horizon.flow.requests.get")
    def test_trade_fields(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_TRADES[:1])

        trades = get_market_trades("0xcond1")

        t = trades[0]
        assert t.wallet == "0xaaa"
        assert t.side == "BUY"
        assert t.outcome == "Yes"
        assert t.size == 100.0
        assert t.price == 0.60
        assert t.usdc_size == pytest.approx(60.0)
        assert t.timestamp == 1700000000
        assert t.market_slug == "will-btc-hit-100k"
        assert t.tx_hash == "0xtx1"
        assert t.pseudonym == "trader_a"

    @patch("horizon.flow.requests.get")
    def test_side_filter(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_market_trades("0xcond1", side="BUY")

        call_params = mock_get.call_args[1]["params"]
        assert call_params["side"] == "BUY"

    @patch("horizon.flow.requests.get")
    def test_min_size_filter(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_market_trades("0xcond1", min_size=100.0)

        call_params = mock_get.call_args[1]["params"]
        assert call_params["filterType"] == "CASH"
        assert call_params["filterAmount"] == 100.0

    @patch("horizon.flow.requests.get")
    def test_network_error_returns_empty(self, mock_get):
        mock_get.side_effect = requests.RequestException("network error")

        trades = get_market_trades("0xcond1")

        assert trades == []

    @patch("horizon.flow.requests.get")
    def test_invalid_json_returns_empty(self, mock_get):
        resp = MagicMock()
        resp.raise_for_status.return_value = None
        resp.json.side_effect = ValueError("bad json")
        mock_get.return_value = resp

        trades = get_market_trades("0xcond1")

        assert trades == []

    @patch("horizon.flow.requests.get")
    def test_limit_capped_at_10000(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_market_trades("0xcond1", limit=99999)

        call_params = mock_get.call_args[1]["params"]
        assert call_params["limit"] == 10000

    @patch("horizon.flow.requests.get")
    def test_non_list_response_returns_empty(self, mock_get):
        mock_get.return_value = _mock_response({"error": "bad"})

        trades = get_market_trades("0xcond1")

        assert trades == []


# ---------------------------------------------------------------------------
# get_wallet_trades
# ---------------------------------------------------------------------------


class TestGetWalletTrades:
    @patch("horizon.flow.requests.get")
    def test_basic_fetch(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_TRADES[:1])

        trades = get_wallet_trades("0xaaa")

        assert len(trades) == 1
        call_params = mock_get.call_args[1]["params"]
        assert call_params["user"] == "0xaaa"

    @patch("horizon.flow.requests.get")
    def test_with_condition_filter(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_wallet_trades("0xaaa", condition_id="0xcond1")

        call_params = mock_get.call_args[1]["params"]
        assert call_params["market"] == "0xcond1"

    @patch("horizon.flow.requests.get")
    def test_error_returns_empty(self, mock_get):
        mock_get.side_effect = requests.RequestException("timeout")

        trades = get_wallet_trades("0xaaa")

        assert trades == []


# ---------------------------------------------------------------------------
# get_wallet_positions
# ---------------------------------------------------------------------------


class TestGetWalletPositions:
    @patch("horizon.flow.requests.get")
    def test_basic_fetch(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_POSITIONS)

        positions = get_wallet_positions("0xaaa")

        assert len(positions) == 1
        assert isinstance(positions[0], WalletPosition)

    @patch("horizon.flow.requests.get")
    def test_position_fields(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_POSITIONS)

        p = get_wallet_positions("0xaaa")[0]

        assert p.wallet == "0xaaa"
        assert p.market_slug == "will-btc-hit-100k"
        assert p.outcome == "Yes"
        assert p.size == 300.0
        assert p.avg_price == 0.55
        assert p.current_price == 0.62
        assert p.current_value == 186.0
        assert p.pnl == 21.0
        assert p.pnl_percent == 12.7

    @patch("horizon.flow.requests.get")
    def test_limit_capped_at_500(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_wallet_positions("0xaaa", limit=9999)

        call_params = mock_get.call_args[1]["params"]
        assert call_params["limit"] == 500

    @patch("horizon.flow.requests.get")
    def test_sort_by_param(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_wallet_positions("0xaaa", sort_by="CASHPNL")

        call_params = mock_get.call_args[1]["params"]
        assert call_params["sortBy"] == "CASHPNL"

    @patch("horizon.flow.requests.get")
    def test_error_returns_empty(self, mock_get):
        mock_get.side_effect = requests.RequestException("error")

        assert get_wallet_positions("0xaaa") == []


# ---------------------------------------------------------------------------
# get_wallet_value
# ---------------------------------------------------------------------------


class TestGetWalletValue:
    @patch("horizon.flow.requests.get")
    def test_returns_value(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_VALUE)

        value = get_wallet_value("0xaaa")

        assert value == pytest.approx(12345.67)

    @patch("horizon.flow.requests.get")
    def test_empty_response_returns_zero(self, mock_get):
        mock_get.return_value = _mock_response([])

        assert get_wallet_value("0xaaa") == 0.0

    @patch("horizon.flow.requests.get")
    def test_error_returns_zero(self, mock_get):
        mock_get.side_effect = requests.RequestException("error")

        assert get_wallet_value("0xaaa") == 0.0


# ---------------------------------------------------------------------------
# get_top_holders
# ---------------------------------------------------------------------------


class TestGetTopHolders:
    @patch("horizon.flow.requests.get")
    def test_basic_fetch(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_HOLDERS)

        holders = get_top_holders("0xcond1")

        assert len(holders) == 3
        assert all(isinstance(h, Holder) for h in holders)

    @patch("horizon.flow.requests.get")
    def test_sorted_by_amount(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_HOLDERS)

        holders = get_top_holders("0xcond1")

        assert holders[0].amount == 10000.0
        assert holders[1].amount == 8000.0
        assert holders[2].amount == 5000.0

    @patch("horizon.flow.requests.get")
    def test_holder_fields(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_HOLDERS)

        h = get_top_holders("0xcond1")[0]

        assert h.wallet == "0xwhale1"
        assert h.amount == 10000.0
        assert h.token_id == "token_yes_1"
        assert h.outcome_index == 0
        assert h.pseudonym == "whale_1"
        assert h.name == "Whale One"

    @patch("horizon.flow.requests.get")
    def test_limit_capped_at_20(self, mock_get):
        mock_get.return_value = _mock_response([])

        get_top_holders("0xcond1", limit=999)

        call_params = mock_get.call_args[1]["params"]
        assert call_params["limit"] == 20

    @patch("horizon.flow.requests.get")
    def test_error_returns_empty(self, mock_get):
        mock_get.side_effect = requests.RequestException("error")

        assert get_top_holders("0xcond1") == []


# ---------------------------------------------------------------------------
# get_wallet_profile
# ---------------------------------------------------------------------------


class TestGetWalletProfile:
    @patch("horizon.flow.requests.get")
    def test_basic_fetch(self, mock_get):
        mock_get.return_value = _mock_response(MOCK_PROFILE)

        profile = get_wallet_profile("0xaaa")

        assert isinstance(profile, WalletProfile)
        assert profile.wallet == "0xaaa"
        assert profile.pseudonym == "trader_a"
        assert profile.name == "Trader Alpha"
        assert profile.bio == "DeFi trader"
        assert profile.x_username == "trader_alpha"

    @patch("horizon.flow.requests.get")
    def test_not_found_returns_none(self, mock_get):
        mock_get.return_value = _mock_response({})

        assert get_wallet_profile("0xnotfound") is None

    @patch("horizon.flow.requests.get")
    def test_error_returns_none(self, mock_get):
        mock_get.side_effect = requests.RequestException("error")

        assert get_wallet_profile("0xaaa") is None


# ---------------------------------------------------------------------------
# analyze_market_flow
# ---------------------------------------------------------------------------


class TestAnalyzeMarketFlow:
    @patch("horizon.flow.get_market_trades")
    def test_aggregation(self, mock_trades):
        mock_trades.return_value = [
            Trade(
                wallet="0xaaa", side="BUY", outcome="Yes",
                size=100.0, price=0.60, usdc_size=60.0,
                timestamp=1, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
            Trade(
                wallet="0xbbb", side="SELL", outcome="Yes",
                size=50.0, price=0.62, usdc_size=31.0,
                timestamp=2, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
            Trade(
                wallet="0xaaa", side="BUY", outcome="Yes",
                size=200.0, price=0.58, usdc_size=116.0,
                timestamp=3, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
        ]

        flow = analyze_market_flow("0xcond1")

        assert isinstance(flow, MarketFlow)
        assert flow.total_trades == 3
        assert flow.buy_volume == pytest.approx(176.0)  # 60 + 116
        assert flow.sell_volume == pytest.approx(31.0)
        assert flow.net_flow == pytest.approx(145.0)
        assert flow.unique_wallets == 2

    @patch("horizon.flow.get_market_trades")
    def test_top_buyers_sellers(self, mock_trades):
        mock_trades.return_value = [
            Trade(
                wallet="0xaaa", side="BUY", outcome="Yes",
                size=100.0, price=0.60, usdc_size=60.0,
                timestamp=1, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
            Trade(
                wallet="0xbbb", side="BUY", outcome="Yes",
                size=200.0, price=0.58, usdc_size=116.0,
                timestamp=2, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
            Trade(
                wallet="0xccc", side="SELL", outcome="Yes",
                size=50.0, price=0.62, usdc_size=31.0,
                timestamp=3, market_slug="m", market_title="M",
                condition_id="c", token_id="t",
            ),
        ]

        flow = analyze_market_flow("0xcond1", top_n=2)

        assert len(flow.top_buyers) == 2
        # 0xbbb has higher buy volume
        assert flow.top_buyers[0][0] == "0xbbb"
        assert flow.top_buyers[0][1] == pytest.approx(116.0)
        assert flow.top_buyers[1][0] == "0xaaa"

        assert len(flow.top_sellers) == 1
        assert flow.top_sellers[0][0] == "0xccc"

    @patch("horizon.flow.get_market_trades")
    def test_empty_trades(self, mock_trades):
        mock_trades.return_value = []

        flow = analyze_market_flow("0xcond1")

        assert flow.total_trades == 0
        assert flow.buy_volume == 0.0
        assert flow.sell_volume == 0.0
        assert flow.net_flow == 0.0
        assert flow.unique_wallets == 0
        assert flow.top_buyers == []
        assert flow.top_sellers == []


# ---------------------------------------------------------------------------
# Import from horizon namespace
# ---------------------------------------------------------------------------


class TestFlowExports:
    def test_imports_from_horizon(self):
        from horizon import (
            Holder,
            MarketFlow,
            PolymarketPosition,
            Trade,
            WalletProfile,
            analyze_market_flow,
            get_market_trades,
            get_top_holders,
            get_wallet_positions,
            get_wallet_profile,
            get_wallet_trades,
            get_wallet_value,
        )

        assert Trade is not None
        assert MarketFlow is not None
        assert PolymarketPosition is not None
        assert Holder is not None
        assert WalletProfile is not None
        assert get_market_trades is not None
        assert get_wallet_trades is not None
        assert get_wallet_positions is not None
        assert get_wallet_value is not None
        assert get_top_holders is not None
        assert get_wallet_profile is not None
        assert analyze_market_flow is not None
