"""Tests for the asynchronous AsyncInfluxionClient."""

from __future__ import annotations

import json
from unittest.mock import patch

import httpx
import pytest
import respx

from influxion import AsyncInfluxionClient
from influxion._exceptions import (
    APIError,
    AuthenticationError,
    NetworkError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

from .conftest import (
    BASE_URL,
    TEST_API_KEY,
    TEST_ARTIFACT_ID,
    TEST_PROJECT_ID,
    TEST_SESSION_ID,
)


# ---------------------------------------------------------------------------
# Construction
# ---------------------------------------------------------------------------


async def test_missing_api_key_raises(monkeypatch):
    monkeypatch.delenv("INFLUXION_API_KEY", raising=False)
    with pytest.raises(AuthenticationError, match="No API key"):
        AsyncInfluxionClient()


async def test_auth_error_ignores_fail_silently(monkeypatch):
    monkeypatch.delenv("INFLUXION_API_KEY", raising=False)
    with pytest.raises(AuthenticationError):
        AsyncInfluxionClient(fail_silently=True)


# ---------------------------------------------------------------------------
# create_session
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_create_session_returns_session_id(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    result = await async_client.create_session(
        name="test-run",
        project_id=TEST_PROJECT_ID,
        agent_id="my-agent",
    )
    await async_client.aclose()
    assert result == TEST_SESSION_ID


@respx.mock
async def test_async_create_session_sends_required_fields(async_client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    await async_client.create_session(
        name="my-run", project_id=TEST_PROJECT_ID, agent_id="agent-1"
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["name"] == "my-run"
    assert body["project_id"] == TEST_PROJECT_ID
    assert body["agent_id"] == "agent-1"


@respx.mock
async def test_async_create_session_with_optional_fields(async_client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    await async_client.create_session(
        name="run",
        project_id=TEST_PROJECT_ID,
        agent_id="a",
        tags=["weekly"],
        metadata={"env": "prod"},
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["tags"] == ["weekly"]
    assert body["metadata"] == {"env": "prod"}


# ---------------------------------------------------------------------------
# create_session_artifact
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_create_session_artifact_returns_artifact_id(async_client):
    respx.post(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}/artifacts").mock(
        return_value=httpx.Response(201, json={"artifact_id": TEST_ARTIFACT_ID})
    )
    result = await async_client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        artifact_name="report",
        artifact="The results...",
    )
    await async_client.aclose()
    assert result == TEST_ARTIFACT_ID


@respx.mock
async def test_async_create_session_artifact_default_type(async_client):
    route = respx.post(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}/artifacts").mock(
        return_value=httpx.Response(201, json={"artifact_id": TEST_ARTIFACT_ID})
    )
    await async_client.create_session_artifact(
        session_id=TEST_SESSION_ID, artifact_name="x", artifact="y"
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["artifact_type"] == "text"


# ---------------------------------------------------------------------------
# end_session / update_session
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_end_session_success(async_client):
    route = respx.patch(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}").mock(
        return_value=httpx.Response(200, json={})
    )
    await async_client.end_session(TEST_SESSION_ID, success=True)
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["success"] is True


@respx.mock
async def test_async_end_session_failure(async_client):
    route = respx.patch(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}").mock(
        return_value=httpx.Response(200, json={})
    )
    await async_client.end_session(
        TEST_SESSION_ID, success=False, failure_message="Timed out"
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["success"] is False
    assert body["failure_message"] == "Timed out"


async def test_async_update_session_noop_makes_no_request(async_client):
    with respx.mock:
        await async_client.update_session(TEST_SESSION_ID)
        assert respx.calls.call_count == 0
    await async_client.aclose()


# ---------------------------------------------------------------------------
# Error handling
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_401_raises_authentication_error(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(401, json={"detail": "Invalid API key"})
    )
    with pytest.raises(AuthenticationError):
        await async_client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    await async_client.aclose()


@respx.mock
async def test_async_401_raises_even_with_fail_silently(silent_async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(401, json={"detail": "bad key"})
    )
    with pytest.raises(AuthenticationError):
        await silent_async_client.create_session(
            name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await silent_async_client.aclose()


@respx.mock
async def test_async_404_raises_not_found(async_client):
    respx.post(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}/artifacts").mock(
        return_value=httpx.Response(404, json={"detail": "Session not found"})
    )
    with pytest.raises(NotFoundError) as exc_info:
        await async_client.create_session_artifact(
            session_id=TEST_SESSION_ID, artifact_name="x", artifact="y"
        )
    await async_client.aclose()
    assert exc_info.value.status_code == 404


@respx.mock
async def test_async_422_raises_validation_error(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(422, json={"detail": "field too long"})
    )
    with pytest.raises(ValidationError):
        await async_client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    await async_client.aclose()


@respx.mock
async def test_async_fail_silently_create_session_returns_none(silent_async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(500, json={"detail": "error"})
    )
    with patch("asyncio.sleep"):
        result = await silent_async_client.create_session(
            name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await silent_async_client.aclose()
    assert result is None


# ---------------------------------------------------------------------------
# Retry logic
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_retry_succeeds_on_third_attempt(async_client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        side_effect=[
            httpx.Response(500, json={"detail": "server error"}),
            httpx.Response(500, json={"detail": "server error"}),
            httpx.Response(201, json={"session_id": TEST_SESSION_ID}),
        ]
    )
    with patch("asyncio.sleep"):
        result = await async_client.create_session(
            name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await async_client.aclose()
    assert result == TEST_SESSION_ID
    assert route.call_count == 3


@respx.mock
async def test_async_retry_exhausted_raises(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(500, json={"detail": "persistent error"})
    )
    with patch("asyncio.sleep"):
        with pytest.raises(APIError):
            await async_client.create_session(
                name="x", project_id=TEST_PROJECT_ID, agent_id="a"
            )
    await async_client.aclose()


@respx.mock
async def test_async_network_error_retried_then_raises(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        side_effect=httpx.ConnectError("connection refused")
    )
    with patch("asyncio.sleep"):
        with pytest.raises(NetworkError):
            await async_client.create_session(
                name="x", project_id=TEST_PROJECT_ID, agent_id="a"
            )
    await async_client.aclose()


@respx.mock
async def test_async_429_raises_rate_limit_error(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(429, json={"detail": "too many requests"})
    )
    with patch("asyncio.sleep"):
        with pytest.raises(RateLimitError):
            await async_client.create_session(
                name="x", project_id=TEST_PROJECT_ID, agent_id="a"
            )
    await async_client.aclose()


# ---------------------------------------------------------------------------
# Context manager
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_context_manager():
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    result: str | None = None
    async with AsyncInfluxionClient(api_key=TEST_API_KEY, fail_silently=False) as c:
        result = await c.create_session(
            name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert result == TEST_SESSION_ID
