from __future__ import annotations

import json

import httpx
import pytest
import respx
from conftest import (
    INITIATE_SEARCH_RESPONSE,
    SAMPLE_ROUTE_JSON,
    _wrap_routes,
    async_sse_stream,
    sse_bytes,
)

from point_me import AuthError, PointMeClient, StreamError
from point_me.client import Site
from point_me.models import RoundtripResults, Route

BASE_URL = "https://api.point.me/api"

_INIT_JSON = json.loads(INITIATE_SEARCH_RESPONSE)


def mock_search_api(
    respx_mock, *, post_status=200, post_json=None, sse_events=None
):
    """Register respx mocks for the two-step search flow."""
    if post_json is None:
        post_json = _INIT_JSON
    respx_mock.post(f"{BASE_URL}/initiateSearch").mock(
        return_value=httpx.Response(post_status, json=post_json)
    )
    if sse_events is not None:
        body = sse_bytes(*sse_events)
        respx_mock.get(f"{BASE_URL}/subscribeToSearch").mock(
            return_value=httpx.Response(
                200,
                headers={"content-type": "text/event-stream"},
                stream=async_sse_stream(body),
            )
        )


def test_client_explicit_token():
    client = PointMeClient(token="abc123")
    assert client._token == "abc123"


def test_client_env_var_token(monkeypatch):
    monkeypatch.setenv("POINTME_TOKEN", "env_token_value")
    client = PointMeClient()
    assert client._token == "env_token_value"


def test_client_explicit_overrides_env(monkeypatch):
    monkeypatch.setenv("POINTME_TOKEN", "env_value")
    client = PointMeClient(token="explicit_value")
    assert client._token == "explicit_value"


def test_client_no_token_raises(monkeypatch):
    monkeypatch.delenv("POINTME_TOKEN", raising=False)
    with pytest.raises(AuthError):
        PointMeClient()


def test_client_error_message(monkeypatch):
    monkeypatch.delenv("POINTME_TOKEN", raising=False)
    with pytest.raises(AuthError, match="POINTME_TOKEN"):
        PointMeClient()


@respx.mock(assert_all_called=False)
async def test_search_returns_routes(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[("SEARCH", SAMPLE_ROUTE_JSON), ("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    results = await client.search("JFK", "LHR", "2026-05-01", "business")
    assert isinstance(results, list)
    assert len(results) == 1
    route = results[0]
    assert isinstance(route, Route)
    assert route.stops == 1
    assert len(route.fares) == 1
    assert len(route.connections) == 1


@respx.mock(assert_all_called=False)
async def test_search_sends_departures(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search("JFK", "LHR", "2026-05-01", "business")
    post_request = respx_mock.calls[0].request
    body = json.loads(post_request.content)
    assert body["departures"] == ["JFK"]


@respx.mock(assert_all_called=False)
async def test_search_sends_arrivals(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search("JFK", "LHR", "2026-05-01", "business")
    post_request = respx_mock.calls[0].request
    body = json.loads(post_request.content)
    assert body["arrivals"] == ["LHR"]


@respx.mock(assert_all_called=False)
async def test_search_sends_date(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search("JFK", "LHR", "2026-05-01", "business")
    post_request = respx_mock.calls[0].request
    body = json.loads(post_request.content)
    assert body["departure_date"]["when"] == "2026-05-01"


@respx.mock(assert_all_called=False)
async def test_search_sends_cabin(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search("JFK", "LHR", "2026-05-01", "business")
    post_request = respx_mock.calls[0].request
    body = json.loads(post_request.content)
    assert body["cabins"] == ["BUSINESS"]


@respx.mock(assert_all_called=False)
async def test_search_sse_event_handling(respx_mock):
    route2_data = {
        "ulid": "route-2",
        "num_stops": 0,
        "times": {"flight": "7:00", "layover": "0:0"},
        "connections": [
            {
                "departure": {"airport": "JFK"},
                "arrival": {"airport": "LHR"},
                "airline": "AA",
                "flight": ["AA100"],
                "cabin": "business",
            }
        ],
        "fares": [
            {
                "miles": 57500,
                "program": "AAdvantage",
                "program_key": "AA",
                "payment": {"taxes": 5.60},
                "transfer": [],
            }
        ],
    }
    route2 = _wrap_routes(route2_data)
    mock_search_api(
        respx_mock,
        sse_events=[
            ("SEARCH", SAMPLE_ROUTE_JSON),
            ("HEARTBEAT", ""),
            ("SEARCH", route2),
            ("COMPLETE", ""),
        ],
    )
    client = PointMeClient(token="test-token")
    results = await client.search("JFK", "LHR", "2026-05-01", "business")
    assert len(results) == 2


@respx.mock(assert_all_called=False)
async def test_search_401_raises_auth_error(respx_mock):
    mock_search_api(respx_mock, post_status=401)
    client = PointMeClient(token="test-token")
    with pytest.raises(AuthError):
        await client.search("JFK", "LHR", "2026-05-01", "business")


@respx.mock(assert_all_called=False)
async def test_search_auth_error_message(respx_mock):
    mock_search_api(respx_mock, post_status=401)
    client = PointMeClient(token="test-token")
    with pytest.raises(
        AuthError, match="Log in to point.me"
    ):
        await client.search("JFK", "LHR", "2026-05-01", "business")


@respx.mock(assert_all_called=False)
async def test_search_timeout(respx_mock):
    respx_mock.post(f"{BASE_URL}/initiateSearch").mock(
        return_value=httpx.Response(200, json=_INIT_JSON)
    )
    respx_mock.get(f"{BASE_URL}/subscribeToSearch").mock(
        side_effect=httpx.ReadTimeout("test")
    )
    client = PointMeClient(token="test-token")
    with pytest.raises(StreamError):
        await client.search("JFK", "LHR", "2026-05-01", "business")


@respx.mock(assert_all_called=False)
async def test_search_unknown_event_skipped(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[
            ("SEARCH", SAMPLE_ROUTE_JSON),
            ("UNKNOWN_TYPE", "blah"),
            ("COMPLETE", ""),
        ],
    )
    client = PointMeClient(token="test-token")
    results = await client.search("JFK", "LHR", "2026-05-01", "business")
    assert len(results) == 1


@respx.mock(assert_all_called=False)
async def test_sends_cobrand_header(respx_mock):
    mock_search_api(respx_mock, sse_events=[("COMPLETE", "")])
    client = PointMeClient(token="test-token")
    await client.search("JFK", "LHR", "2026-05-01", "business")
    req = respx_mock.calls[0].request
    assert req.headers["x-cobrand"] == "amex"


# --- Roundtrip tests ---


def _mock_roundtrip_api(respx_mock, *, sse_events=None):
    """Mock for roundtrip: initiate + SSE that returns fresh streams."""
    respx_mock.post(f"{BASE_URL}/initiateSearch").mock(
        return_value=httpx.Response(200, json=_INIT_JSON)
    )
    if sse_events is not None:
        def _fresh_sse(request):
            body = sse_bytes(*sse_events)
            return httpx.Response(
                200,
                headers={"content-type": "text/event-stream"},
                stream=async_sse_stream(body),
            )
        respx_mock.get(f"{BASE_URL}/subscribeToSearch").mock(
            side_effect=_fresh_sse
        )


@respx.mock(assert_all_called=False)
async def test_roundtrip_returns_both_directions(respx_mock):
    _mock_roundtrip_api(
        respx_mock,
        sse_events=[("SEARCH", SAMPLE_ROUTE_JSON), ("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    results = await client.search_roundtrip(
        "JFK", "LHR", "2026-05-01", "2026-05-15", "business"
    )
    assert isinstance(results, RoundtripResults)
    assert len(results.outbound) >= 1
    assert len(results.inbound) >= 1


@respx.mock(assert_all_called=False)
async def test_roundtrip_payload_has_return_date(respx_mock):
    _mock_roundtrip_api(
        respx_mock,
        sse_events=[("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search_roundtrip(
        "JFK", "LHR", "2026-05-01", "2026-05-15", "business"
    )
    req = respx_mock.calls[0].request
    body = json.loads(req.content)
    assert body["return_date"] == {
        "when": "2026-05-15",
        "flexibility": "0",
    }


@respx.mock(assert_all_called=False)
async def test_roundtrip_subscribes_twice(respx_mock):
    _mock_roundtrip_api(
        respx_mock,
        sse_events=[("SEARCH", SAMPLE_ROUTE_JSON), ("COMPLETE", "")],
    )
    client = PointMeClient(token="test-token")
    await client.search_roundtrip(
        "JFK", "LHR", "2026-05-01", "2026-05-15", "business"
    )
    sse_calls = [
        c for c in respx_mock.calls
        if "subscribeToSearch" in str(c.request.url)
    ]
    assert len(sse_calls) == 2
    dirs = [
        dict(c.request.url.params)["directionality"] for c in sse_calls
    ]
    assert "outbound" in dirs
    assert "inbound" in dirs


# --- exchange_session_cookie tests ---


@respx.mock(assert_all_called=False)
async def test_exchange_session_cookie_returns_token(respx_mock):
    respx_mock.get("https://amex.point.me/api/auth/session").mock(
        return_value=httpx.Response(
            200, json={"accessToken": "jwt-abc", "user": {"name": "Test"}}
        )
    )
    token = await PointMeClient.exchange_session_cookie("sess-cookie")
    assert token == "jwt-abc"


@respx.mock(assert_all_called=False)
async def test_exchange_session_cookie_www_site(respx_mock):
    respx_mock.get("https://www.point.me/api/auth/session").mock(
        return_value=httpx.Response(
            200, json={"accessToken": "jwt-www"}
        )
    )
    token = await PointMeClient.exchange_session_cookie(
        "sess-cookie", Site.WWW
    )
    assert token == "jwt-www"


@respx.mock(assert_all_called=False)
async def test_exchange_session_cookie_sends_cookie(respx_mock):
    respx_mock.get("https://amex.point.me/api/auth/session").mock(
        return_value=httpx.Response(
            200, json={"accessToken": "jwt-abc"}
        )
    )
    await PointMeClient.exchange_session_cookie("my-session-value")
    req = respx_mock.calls[0].request
    assert "__Secure-next-auth.session-token=my-session-value" in (
        req.headers.get("cookie", "")
    )


@respx.mock(assert_all_called=False)
async def test_exchange_session_cookie_no_access_token(respx_mock):
    respx_mock.get("https://amex.point.me/api/auth/session").mock(
        return_value=httpx.Response(200, json={"user": {"name": "Test"}})
    )
    with pytest.raises(AuthError, match="no access token"):
        await PointMeClient.exchange_session_cookie("sess-cookie")


@respx.mock(assert_all_called=False)
async def test_exchange_session_cookie_http_error(respx_mock):
    respx_mock.get("https://amex.point.me/api/auth/session").mock(
        return_value=httpx.Response(500)
    )
    with pytest.raises(AuthError, match="HTTP 500"):
        await PointMeClient.exchange_session_cookie("sess-cookie")


@respx.mock(assert_all_called=False)
async def test_search_malformed_event_skipped(respx_mock):
    mock_search_api(
        respx_mock,
        sse_events=[
            ("SEARCH", "not valid json {{{"),
            ("SEARCH", SAMPLE_ROUTE_JSON),
            ("COMPLETE", ""),
        ],
    )
    client = PointMeClient(token="test-token")
    results = await client.search("JFK", "LHR", "2026-05-01", "business")
    assert len(results) == 1
