"""Tests for AltSportsData SDK v2"""

import pytest
from unittest.mock import MagicMock, patch

from altsportsdata import (
    AltSportsData, SportType, OddType, EventStatus, FutureType,
    ConfigurationError, AltSportsDataError,
)
from altsportsdata.exceptions import (
    AuthenticationError, NotFoundError, NetworkError, RateLimitError,
    RetryExhaustedError,
)


def _ok(data):
    r = MagicMock(); r.ok = True; r.json.return_value = data; return r

def _err(code, msg="error"):
    r = MagicMock(); r.ok = False; r.status_code = code
    r.json.return_value = {"message": msg}; return r

P = "altsportsdata.client.requests.Session.request"


def _params(mock):
    """Extract params from mock call as a dict (handles list-of-tuples format)."""
    raw = mock.call_args.kwargs.get("params", {})
    if isinstance(raw, list):
        d = {}
        for k, v in raw:
            clean_k = k.rstrip("[]")
            if clean_k in d:
                if not isinstance(d[clean_k], list):
                    d[clean_k] = [d[clean_k]]
                d[clean_k].append(v)
            else:
                d[clean_k] = v
        return d
    return raw


class TestInit:
    def test_requires_key(self):
        with pytest.raises(ConfigurationError):
            AltSportsData(api_key=None)

    def test_empty_key(self):
        with pytest.raises(ConfigurationError):
            AltSportsData(api_key="")

    def test_league_stored(self):
        c = AltSportsData(api_key="k", league="wsl")
        assert c.league == "wsl"

    def test_league_enum(self):
        c = AltSportsData(api_key="k", league=SportType.F1)
        assert c.league == "f1"

    def test_header(self):
        c = AltSportsData(api_key="my-key")
        assert c._session.headers["X-API-KEY"] == "my-key"

    def test_repr(self):
        c = AltSportsData(api_key="k", league="wsl")
        assert "wsl" in repr(c)

    def test_get_league(self):
        c = AltSportsData(api_key="k")
        wsl = c.get_league("wsl")
        assert wsl.league == "wsl"
        assert wsl.api_key == "k"
        assert wsl._session is c._session  # shares session

    def test_get_league_enum(self):
        c = AltSportsData(api_key="k")
        f1 = c.get_league(SportType.F1)
        assert f1.league == "f1"


class TestLeagues:
    @patch(P)
    def test_list_leagues(self, m):
        m.return_value = _ok([{"id": "1", "name": "WSL", "sportType": "wsl"}])
        assert len(AltSportsData(api_key="k").list_leagues()) == 1

    @patch(P)
    def test_list_sports_alias(self, m):
        m.return_value = _ok([])
        AltSportsData(api_key="k").list_sports()  # should not raise


class TestEvents:
    @patch(P)
    def test_list_events(self, m):
        m.return_value = _ok([{"id": "e1", "sportType": "f1"}])
        events = AltSportsData(api_key="k").list_events(league="f1")
        assert events[0].sportType == "f1"

    @patch(P)
    def test_status_string(self, m):
        m.return_value = _ok([])
        AltSportsData(api_key="k", league="wsl").list_events(status="upcoming")
        p = _params(m)
        assert p["eventStatus"] == "UPCOMING"

    @patch(P)
    def test_status_list(self, m):
        m.return_value = _ok([])
        AltSportsData(api_key="k").list_events(status=["live", "upcoming"])
        p = _params(m)
        assert p["eventStatus"] == ["LIVE", "UPCOMING"]

    @patch(P)
    def test_league_auto_filter(self, m):
        m.return_value = _ok([])
        AltSportsData(api_key="k", league="wsl").list_events()
        assert _params(m)["sportType"] == "wsl"

    @patch(P)
    def test_get_event(self, m):
        m.return_value = _ok({"id": "e1", "name": "Event"})
        e = AltSportsData(api_key="k").get_event("e1")
        assert e.id == "e1"

    @patch(P)
    def test_participants(self, m):
        m.return_value = _ok([{"athlete": {"id": "a1", "firstName": "J", "lastName": "D"}}])
        p = AltSportsData(api_key="k").get_participants("e1")
        assert p[0].athlete.firstName == "J"

    @patch(P)
    def test_heat_scores(self, m):
        m.return_value = _ok([{"id": "s1", "athleteHeatScore": 8.5}])
        s = AltSportsData(api_key="k").get_heat_scores("e1", "h1")
        assert s[0].athleteHeatScore == 8.5


class TestOdds:
    @patch(P)
    def test_get_odds_string(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "eventWinner")
        assert "/eventWinner" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_odds_alias(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "moneyline")
        assert "/eventWinner" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_moneylines(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_moneylines("e1")
        assert "/eventWinner" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_matchups(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_matchups("e1")
        assert "/headToHead" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_player_props(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_player_props("e1")
        assert "/propBets" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_totals(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_totals("e1", sub_market="points")
        p = _params(m)
        assert "/overUnder" in m.call_args.kwargs["url"]
        assert p["overUnderSubMarketType"] == "points"

    @patch(P)
    def test_get_heat_winners(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_heat_winners("e1")
        assert "/heatWinner" in m.call_args.kwargs["url"]

    @patch(P)
    def test_get_exactas(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_exactas("e1", n=3)
        p = _params(m)
        assert p["exactasType"] == 3

    @patch(P)
    def test_enum_market(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", OddType.DREAM_TEAM)
        assert "/dreamTeam" in m.call_args.kwargs["url"]


class TestParlays:
    @patch(P)
    def test_calculate_parlay(self, m):
        m.return_value = _ok({"combinedOdds": 5.2})
        r = AltSportsData(api_key="k").calculate_parlay("e1", picks=["o1", "o2"])
        assert r["combinedOdds"] == 5.2

    @patch(P)
    def test_sgp_alias(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").calculate_sgp("e1", picks=["o1"])


class TestJaiAlai:
    @patch(P)
    def test_matches(self, m):
        m.return_value = _ok([{"id": "j1", "name": "M1"}])
        assert len(AltSportsData(api_key="k").get_jaialai_matches("j1")) == 1

    @patch(P)
    def test_odds(self, m):
        m.return_value = _ok({"id": "j1", "matchName": "M1"})
        assert AltSportsData(api_key="k").get_jaialai_odds("j1").matchName == "M1"


class TestFutures:
    @patch(P)
    def test_list(self, m):
        m.return_value = _ok([{"id": "f1"}])
        assert len(AltSportsData(api_key="k").list_futures()) == 1

    @patch(P)
    def test_get_futures(self, m):
        m.return_value = _ok([{"id": "fo1"}])
        AltSportsData(api_key="k", league="f1").get_futures(tour="t1", type="winner")
        assert "/futures/f1/tour/t1/odds/winner" in m.call_args.kwargs["url"]

    def test_futures_requires_league(self):
        with pytest.raises(ConfigurationError):
            AltSportsData(api_key="k").get_futures(tour="t1")


class TestErrors:
    @patch(P)
    def test_401(self, m):
        m.return_value = _err(401)
        with pytest.raises(AuthenticationError):
            AltSportsData(api_key="k").list_leagues()

    @patch(P)
    def test_404(self, m):
        m.return_value = _err(404)
        with pytest.raises(NotFoundError):
            AltSportsData(api_key="k").get_event("x")

    @patch(P)
    def test_429(self, m):
        m.return_value = _err(429)
        with pytest.raises((RateLimitError, RetryExhaustedError)):
            AltSportsData(api_key="k", max_retries=0).list_leagues()

    @patch(P)
    def test_429_retries_then_exhausts(self, m):
        """429 with retries enabled exhausts all attempts."""
        m.return_value = _err(429)
        with pytest.raises(RetryExhaustedError):
            AltSportsData(api_key="k", max_retries=1, retry_backoff=0.01).list_leagues()
        assert m.call_count == 2  # initial + 1 retry

    @patch(P)
    def test_timeout(self, m):
        import requests as req
        m.side_effect = req.exceptions.Timeout()
        with pytest.raises(NetworkError):
            AltSportsData(api_key="k", max_retries=0).list_leagues()

    @patch(P)
    def test_connection(self, m):
        import requests as req
        m.side_effect = req.exceptions.ConnectionError()
        with pytest.raises(NetworkError):
            AltSportsData(api_key="k", max_retries=0).list_leagues()


class TestResponseUnwrapping:
    @patch(P)
    def test_unwrap_data_envelope(self, m):
        """API wraps responses in {"status": 200, "data": [...]}."""
        m.return_value = _ok({"status": 200, "data": [{"id": "e1", "sportType": "f1"}]})
        events = AltSportsData(api_key="k").list_events()
        assert events[0].sportType == "f1"

    @patch(P)
    def test_unwrap_statusCode_envelope(self, m):
        """Dev API uses statusCode instead of status."""
        m.return_value = _ok({"statusCode": 200, "data": [{"id": "e1"}]})
        events = AltSportsData(api_key="k").list_events()
        assert len(events) == 1

    @patch(P)
    def test_no_unwrap_plain_dict(self, m):
        """Plain dicts without data/status envelope pass through."""
        m.return_value = _ok({"eventWinner": [{"id": "o1"}]})
        result = AltSportsData(api_key="k").get_moneylines("e1")
        assert "eventWinner" in result


class TestMarketAliases:
    @patch(P)
    def test_alias_h2h(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "h2h")
        assert "/headToHead" in m.call_args.kwargs["url"]

    @patch(P)
    def test_alias_winner(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "winner")
        assert "/eventWinner" in m.call_args.kwargs["url"]

    @patch(P)
    def test_alias_totals(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "totals")
        assert "/overUnder" in m.call_args.kwargs["url"]

    @patch(P)
    def test_unknown_market_passthrough(self, m):
        m.return_value = _ok({})
        AltSportsData(api_key="k").get_odds("e1", "someNewMarket")
        assert "/someNewMarket" in m.call_args.kwargs["url"]


class TestGetMarkets:
    @patch(P)
    def test_returns_events_with_odds(self, m):
        # Use league-scoped client to skip get_leagues() call
        # Calls: get_events, eventWinner, headToHead, podiums, heatWinner
        h2h_entry = {"id": "h1", "eventParticipant1": {"id": "p1", "odds": 1.5, "athlete": {"firstName": "A", "lastName": "B"}}, "eventParticipant2": {"id": "p2", "odds": 2.5, "athlete": {"firstName": "C", "lastName": "D"}}}
        m.side_effect = [
            _ok([{"id": "e1", "name": "Race 1", "sportType": "spr", "startDate": "2026-03-10", "eventStatus": "UPCOMING"}]),
            _ok({"eventWinner": [{"id": "o1", "athlete": {"firstName": "J", "lastName": "D"}, "odds": 2.0, "probability": 50}]}),
            _ok({"headToHead": [h2h_entry]}),
            _ok({"podiums": []}),
            _ok({"heatWinner": []}),
        ]
        results = AltSportsData(api_key="k", league="spr").get_markets()
        assert len(results) == 1
        assert results[0]["event_id"] == "e1"
        assert "eventWinner" in results[0]["markets_available"]
        assert "headToHead" in results[0]["markets_available"]
        assert len(results[0]["matchups"]) == 1
        assert results[0]["matchups"][0]["player1"] == "A B"

    @patch(P)
    def test_skips_events_without_odds(self, m):
        m.side_effect = [
            _ok([{"id": "e1", "name": "Race 1", "sportType": "spr", "startDate": "2026-03-10", "eventStatus": "UPCOMING"}]),
            _ok({"eventWinner": []}),  # no outcomes
        ]
        results = AltSportsData(api_key="k", league="spr").get_markets()
        assert len(results) == 0

    @patch(P)
    def test_skips_jaialai(self, m):
        m.side_effect = [
            _ok([{"id": "jaialai-e1", "name": "Match", "sportType": "jaialai"}]),
        ]
        results = AltSportsData(api_key="k", league="jaialai").get_markets()
        assert len(results) == 0

    @patch(P)
    def test_propagates_network_error(self, m):
        import requests as req
        m.side_effect = [
            _ok([{"id": "e1", "name": "Race", "sportType": "spr"}]),
            req.exceptions.ConnectionError(),
        ]
        with pytest.raises(NetworkError):
            AltSportsData(api_key="k", league="spr", max_retries=0).get_markets()

    @patch(P)
    def test_market_filter_single_type(self, m):
        """get_markets(market='matchup') only fetches headToHead."""
        h2h_entry = {"id": "h1", "eventParticipant1": {"id": "p1", "odds": 1.5, "athlete": {"firstName": "A", "lastName": "B"}}, "eventParticipant2": {"id": "p2", "odds": 2.5, "athlete": {"firstName": "C", "lastName": "D"}}}
        m.side_effect = [
            _ok([{"id": "e1", "name": "Race 1", "sportType": "spr", "startDate": "2026-03-10", "eventStatus": "UPCOMING"}]),
            _ok({"headToHead": [h2h_entry]}),
        ]
        results = AltSportsData(api_key="k", league="spr").get_markets(market="matchup")
        assert len(results) == 1
        assert results[0]["market_type"] == "headToHead"
        assert len(results[0]["matchups"]) == 1

    @patch(P)
    def test_market_filter_moneyline(self, m):
        """get_markets(market='moneyline') fetches eventWinner."""
        m.side_effect = [
            _ok([{"id": "e1", "name": "Race", "sportType": "spr", "startDate": "2026-03-10", "eventStatus": "UPCOMING"}]),
            _ok({"eventWinner": [{"id": "o1", "athlete": {"firstName": "J", "lastName": "D"}, "odds": 2.0, "probability": 50}]}),
        ]
        results = AltSportsData(api_key="k", league="spr").get_markets(market="moneyline")
        assert len(results) == 1
        assert results[0]["market_type"] == "eventWinner"
        assert len(results[0]["outcomes"]) == 1


# ══════════════════════════════════════════════════════════════════════
#  CONVENIENCE METHODS
# ══════════════════════════════════════════════════════════════════════

_SAMPLE_ODDS = {
    "eventWinner": [
        {"id": "o1", "athlete": {"firstName": "Eli", "lastName": "Tomac"}, "odds": 2.02, "probability": 49.5, "status": "OPEN", "outcomeId": "oid_1"},
        {"id": "o2", "athlete": {"firstName": "Hunter", "lastName": "Lawrence"}, "odds": 2.66, "probability": 37.6, "status": "OPEN", "outcomeId": "oid_2"},
        {"id": "o3", "athlete": {"firstName": "Chase", "lastName": "Sexton"}, "odds": 5.50, "probability": 18.2, "status": "OPEN", "outcomeId": "oid_3"},
    ]
}


class TestGetFavorites:
    @patch(P)
    def test_returns_sorted(self, m):
        m.return_value = _ok(_SAMPLE_ODDS)
        favs = AltSportsData(api_key="k").get_favorites("e1", n=2)
        assert len(favs) == 2
        assert favs[0]["athlete"] == "Eli Tomac"
        assert favs[0]["odds"] == 2.02
        assert favs[1]["athlete"] == "Hunter Lawrence"

    @patch(P)
    def test_includes_all_formats(self, m):
        m.return_value = _ok(_SAMPLE_ODDS)
        favs = AltSportsData(api_key="k").get_favorites("e1", n=1)
        fav = favs[0]
        assert "odds" in fav
        assert "probability" in fav
        assert "american" in fav
        assert "outcome_id" in fav
        assert fav["probability"] > 0
        assert isinstance(fav["american"], (int, float))

    @patch(P)
    def test_default_n(self, m):
        m.return_value = _ok(_SAMPLE_ODDS)
        favs = AltSportsData(api_key="k").get_favorites("e1")
        assert len(favs) == 3  # only 3 in sample, default n=5

    @patch(P)
    def test_empty_odds(self, m):
        m.return_value = _ok({"eventWinner": []})
        favs = AltSportsData(api_key="k").get_favorites("e1")
        assert favs == []


class TestGetSettledResults:
    @patch(P)
    def test_returns_winners(self, m):
        settled = {"eventWinner": [
            {"id": "o1", "athlete": {"firstName": "Gabby", "lastName": "Bryan"}, "odds": 6.46, "settlement": "WIN", "outcomeId": "oid_1"},
            {"id": "o2", "athlete": {"firstName": "Carissa", "lastName": "Moore"}, "odds": 2.10, "settlement": "LOSE"},
        ]}
        m.return_value = _ok(settled)
        winners = AltSportsData(api_key="k").get_settled_results("e1")
        assert len(winners) == 1
        assert winners[0]["athlete"] == "Gabby Bryan"
        assert winners[0]["settlement"] == "WIN"

    @patch(P)
    def test_no_winners(self, m):
        m.return_value = _ok({"eventWinner": [
            {"id": "o1", "athlete": {"firstName": "A", "lastName": "B"}, "odds": 2.0, "settlement": "LOSE"},
        ]})
        assert AltSportsData(api_key="k").get_settled_results("e1") == []


class TestGetEventSummary:
    @patch(P)
    def test_basic_summary(self, m):
        # Calls: get_event, get_moneylines, then probes 8 markets
        m.side_effect = [
            _ok({"id": "e1", "name": "Indianapolis", "sportType": "spr", "eventStatus": "UPCOMING", "startDate": "2026-03-15"}),
            _ok(_SAMPLE_ODDS),
            # Market probes (headToHead, podiums, propBets, overUnder, shows, fastestLap, heatWinner, dreamTeam)
            _ok({"headToHead": [{"id": "h1"}]}),
            _ok({"podiums": []}),
            _ok({"propBets": []}),
            _ok({"overUnder": []}),
            _ok({"shows": []}),
            _ok({"fastestLap": []}),
            _ok({"heatWinner": []}),
            _ok({"dreamTeam": []}),
        ]
        summary = AltSportsData(api_key="k").get_event_summary("e1")
        assert summary["event_name"] == "Indianapolis"
        assert summary["favorite"]["athlete"] == "Eli Tomac"
        assert summary["field_size"] == 3
        assert "eventWinner" in summary["markets_available"]
        assert "headToHead" in summary["markets_available"]
        assert summary["market_count"] >= 2

    @patch(P)
    def test_no_odds(self, m):
        m.side_effect = [
            _ok({"id": "e1", "name": "Event", "sportType": "wsl"}),
            _ok({"eventWinner": []}),
            # Market probes all empty
            _ok({"headToHead": []}),
            _ok({"podiums": []}),
            _ok({"propBets": []}),
            _ok({"overUnder": []}),
            _ok({"shows": []}),
            _ok({"fastestLap": []}),
            _ok({"heatWinner": []}),
            _ok({"dreamTeam": []}),
        ]
        summary = AltSportsData(api_key="k").get_event_summary("e1")
        assert summary["favorite"] is None
        assert summary["field_size"] == 0


class TestToDataFrame:
    def test_with_dicts(self):
        try:
            import pandas as pd
        except ImportError:
            pytest.skip("pandas not installed")
        client = AltSportsData(api_key="k")
        df = client.to_dataframe([{"a": 1, "b": 2}, {"a": 3, "b": 4}])
        assert len(df) == 2
        assert list(df.columns) == ["a", "b"]

    def test_empty(self):
        try:
            import pandas as pd
        except ImportError:
            pytest.skip("pandas not installed")
        client = AltSportsData(api_key="k")
        df = client.to_dataframe([])
        assert len(df) == 0

    def test_no_pandas(self):
        import sys
        # Only test if we can temporarily hide pandas
        if "pandas" not in sys.modules:
            client = AltSportsData(api_key="k")
            with pytest.raises(ImportError, match="pandas"):
                client.to_dataframe([{"a": 1}])


class TestOddsToDataFrame:
    @patch(P)
    def test_basic(self, m):
        try:
            import pandas as pd
        except ImportError:
            pytest.skip("pandas not installed")
        client = AltSportsData(api_key="k")
        df = client.odds_to_dataframe(_SAMPLE_ODDS)
        assert len(df) == 3
        assert "athlete" in df.columns
        assert "american" in df.columns
        assert "probability" in df.columns
        assert df.iloc[0]["athlete"] == "Eli Tomac"
