"""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,
    ConflictError,
    NetworkError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

from .conftest import (
    BASE_URL,
    TEST_API_KEY,
    TEST_AGENT_ID,
    TEST_SESSION_ID,
    TEST_PROJECT_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_succeeds_returns_none(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={})
    )
    result = await async_client.create_session(
        session_id="test-run",
        name="test-run",
        project_id=TEST_PROJECT_ID,
        agent_id="my-agent",
    )
    await async_client.aclose()
    assert result is None


@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={})
    )
    await async_client.create_session(
        session_id="my-run",
        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["session_id"] == "my-run"
    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={})
    )
    await async_client.create_session(
        session_id="run",
        name="run",
        tags=["weekly"],
        metadata={"env": "prod"},
        project_id=TEST_PROJECT_ID,
        agent_id="a",
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["tags"] == ["weekly"]
    assert body["metadata"] == {"env": "prod"}


# ---------------------------------------------------------------------------
# get_config
# ---------------------------------------------------------------------------

_GET_CONFIG_RESPONSE = {
    "count": 1,
    "configs": [
        {
            "id": "11111111-1111-1111-1111-111111111111",
            "config_id": "default",
            "name": "Default",
            "description": "Default config",
            "is_default": True,
            "models": [
                {"label": "main", "base_url": None, "name": "gpt-4o"}
            ],
            "prompts": [
                {
                    "label": "system",
                    "messages": [
                        {"role": "system", "content": "You are helpful."}
                    ],
                }
            ],
        }
    ],
}


@respx.mock
async def test_async_get_config_succeeds_returns_agent_config(async_client):
    respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json=_GET_CONFIG_RESPONSE)
    )
    config = await async_client.get_config(
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    assert config is not None
    assert config.config_id == "default"
    assert config.name == "Default"
    assert len(config.models) == 1
    assert config.get_model("main") is not None
    assert config.get_model("main").name == "gpt-4o"
    assert config.get_prompt("system") is not None
    assert config.get_model("__nonexistent__") is None


@respx.mock
async def test_async_get_config_sends_default_true_when_no_config_id(async_client):
    route = respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json=_GET_CONFIG_RESPONSE)
    )
    await async_client.get_config(
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    assert route.calls.last.request.url.params.get("default") == "true"


@respx.mock
async def test_async_get_config_sends_config_id_when_provided(async_client):
    route = respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json=_GET_CONFIG_RESPONSE)
    )
    await async_client.get_config(
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
        config_id="my-config-slug",
    )
    await async_client.aclose()
    assert route.calls.last.request.url.params.get("config_id") == "my-config-slug"


@respx.mock
async def test_async_get_config_sends_project_and_agent(async_client):
    route = respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json=_GET_CONFIG_RESPONSE)
    )
    await async_client.get_config(project_id="proj-1", agent_id="agent-1")
    await async_client.aclose()
    params = route.calls.last.request.url.params
    assert params.get("project_id") == "proj-1"
    assert params.get("agent_id") == "agent-1"


async def test_async_get_config_raises_when_both_config_id_and_session_id_provided(
    async_client,
):
    """config_id and session_id are mutually exclusive; SDK raises before calling API."""
    with pytest.raises(ValueError, match="mutually exclusive"):
        await async_client.get_config(
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
            config_id="my-config",
            session_id="my-session",
        )
    await async_client.aclose()


@respx.mock
async def test_async_get_config_sends_session_id_when_provided(async_client):
    route = respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json=_GET_CONFIG_RESPONSE)
    )
    await async_client.get_config(
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
        session_id="my-session-123",
    )
    await async_client.aclose()
    params = route.calls.last.request.url.params
    assert params.get("session_id") == "my-session-123"
    assert "default" not in params  # session_id is exclusive with default


@respx.mock
async def test_async_get_config_empty_configs_returns_none(async_client):
    respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(200, json={"count": 0, "configs": []})
    )
    config = await async_client.get_config(
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    assert config is None


@respx.mock
async def test_async_get_config_404_raises(async_client):
    respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(404, json={"detail": "Config not found"})
    )
    with pytest.raises(NotFoundError) as exc_info:
        await async_client.get_config(
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
        )
    await async_client.aclose()
    assert exc_info.value.status_code == 404


@respx.mock
async def test_async_get_config_fail_silently_returns_none(silent_async_client):
    respx.get(f"{BASE_URL}/agent_configs").mock(
        return_value=httpx.Response(500, json={"detail": "error"})
    )
    with patch("asyncio.sleep"):
        config = await silent_async_client.get_config(
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
        )
    await silent_async_client.aclose()
    assert config is None


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


@respx.mock
async def test_async_create_session_artifact_succeeds_returns_none(async_client):
    respx.post(f"{BASE_URL}/agent_sessions/artifacts").mock(
        return_value=httpx.Response(201)
    )
    result = await async_client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        name="report",
        artifact="The results...",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    assert result is None


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


# ---------------------------------------------------------------------------
# create_session_tool_call
# ---------------------------------------------------------------------------


@respx.mock
async def test_async_create_session_tool_call_succeeds_returns_none(async_client):
    respx.post(f"{BASE_URL}/agent_sessions/tool_calls").mock(
        return_value=httpx.Response(201)
    )
    result = await async_client.create_session_tool_call(
        session_id=TEST_SESSION_ID,
        name="search",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    assert result is None


@respx.mock
async def test_async_create_session_tool_call_with_optional_fields(async_client):
    route = respx.post(f"{BASE_URL}/agent_sessions/tool_calls").mock(
        return_value=httpx.Response(201)
    )
    await async_client.create_session_tool_call(
        session_id=TEST_SESSION_ID,
        name="run_sql",
        input={"query": "SELECT 1"},
        output={"rows": []},
        tags=["db"],
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    await async_client.aclose()
    body = json.loads(route.calls.last.request.content)
    assert body["name"] == "run_sql"
    assert body["input"] == {"query": "SELECT 1"}
    assert body["output"] == {"rows": []}
    assert body["tags"] == ["db"]


@respx.mock
async def test_async_create_session_tool_call_404_raises(async_client):
    respx.post(f"{BASE_URL}/agent_sessions/tool_calls").mock(
        return_value=httpx.Response(404, json={"detail": "Session not found"})
    )
    with pytest.raises(NotFoundError) as exc_info:
        await async_client.create_session_tool_call(
            session_id=TEST_SESSION_ID,
            name="x",
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
        )
    await async_client.aclose()
    assert exc_info.value.status_code == 404


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


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


@respx.mock
async def test_async_end_session_failure(async_client):
    route = respx.patch(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(200, json={})
    )
    await async_client.end_session(
        session_id=TEST_SESSION_ID,
        success=False,
        failure_message="Timed out",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    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(session_id=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(
            session_id="x", 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(
            session_id="x", 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/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,
            name="x",
            artifact="y",
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
        )
    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(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await async_client.aclose()


@respx.mock
async def test_async_409_raises_conflict_error(async_client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(
            409, json={"detail": "Session already exists"}
        )
    )
    with pytest.raises(ConflictError) as exc_info:
        await async_client.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await async_client.aclose()
    assert exc_info.value.status_code == 409


@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(
            session_id="x", 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={}),
        ]
    )
    with patch("asyncio.sleep"):
        result = await async_client.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    await async_client.aclose()
    assert result is None
    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(
                session_id="x", 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(
                session_id="x", 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(
                session_id="x", 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={})
    )
    result = None
    async with AsyncInfluxionClient(api_key=TEST_API_KEY, fail_silently=False) as c:
        result = await c.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert result is None
