"""Tests for the synchronous InfluxionClient."""

from __future__ import annotations

import json
from unittest.mock import patch
from uuid import UUID

import httpx
import pytest
import respx

from influxion import (
    AsyncInfluxionClient,
    InfluxionClient,
)
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
# ---------------------------------------------------------------------------


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


def test_api_key_from_env(monkeypatch):
    monkeypatch.setenv("INFLUXION_API_KEY", TEST_API_KEY)
    monkeypatch.delenv("INFLUXION_BASE_URL", raising=False)
    with respx.mock:
        respx.post(f"{BASE_URL}/agent_sessions").mock(
            return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
        )
        c = InfluxionClient(fail_silently=False)
        result = c.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
        c.close()
    assert result == TEST_SESSION_ID


def test_base_url_from_env(monkeypatch):
    custom_url = "https://staging.api.influxion.io/v1"
    monkeypatch.setenv("INFLUXION_BASE_URL", custom_url)
    with respx.mock:
        route = respx.post(f"{custom_url}/agent_sessions").mock(
            return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
        )
        c = InfluxionClient(api_key=TEST_API_KEY, fail_silently=False)
        c.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
        c.close()
    assert route.called


def test_auth_error_at_init_ignores_fail_silently(monkeypatch):
    """AuthenticationError raised at construction even with fail_silently=True."""
    monkeypatch.delenv("INFLUXION_API_KEY", raising=False)
    with pytest.raises(AuthenticationError):
        InfluxionClient(fail_silently=True)


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


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


@respx.mock
def test_create_session_sends_required_fields(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    client.create_session(name="my-run", project_id=TEST_PROJECT_ID, agent_id="agent-1")
    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"
    assert "tags" not in body
    assert "metadata" not in body


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


@respx.mock
def test_create_session_uuid_project_id_is_stringified(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    client.create_session(
        name="run",
        project_id=UUID(TEST_PROJECT_ID),
        agent_id="a",
    )
    body = json.loads(route.calls.last.request.content)
    assert body["project_id"] == TEST_PROJECT_ID


@respx.mock
def test_create_session_sends_bearer_token(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert route.calls.last.request.headers["authorization"] == f"Bearer {TEST_API_KEY}"


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


@respx.mock
def test_create_session_artifact_returns_artifact_id(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 = client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        artifact_name="report",
        artifact="The results show...",
    )
    assert result == TEST_ARTIFACT_ID


@respx.mock
def test_create_session_artifact_default_type(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})
    )
    client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        artifact_name="report",
        artifact="...",
    )
    body = json.loads(route.calls.last.request.content)
    assert body["artifact_type"] == "text"


@respx.mock
def test_create_session_artifact_custom_type(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})
    )
    client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        artifact_name="img",
        artifact="<base64>",
        artifact_type="image",
    )
    body = json.loads(route.calls.last.request.content)
    assert body["artifact_type"] == "image"


@respx.mock
def test_create_session_artifact_with_optional_fields(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})
    )
    client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        artifact_name="output",
        artifact="...",
        tags=["final"],
        metadata={"step": "3"},
    )
    body = json.loads(route.calls.last.request.content)
    assert body["tags"] == ["final"]
    assert body["metadata"] == {"step": "3"}


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


@respx.mock
def test_end_session_success(client):
    route = respx.patch(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}").mock(
        return_value=httpx.Response(200, json={})
    )
    client.end_session(TEST_SESSION_ID, success=True)
    body = json.loads(route.calls.last.request.content)
    assert body["success"] is True
    assert "failure_message" not in body


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


@respx.mock
def test_update_session_partial_tags(client):
    route = respx.patch(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}").mock(
        return_value=httpx.Response(200, json={})
    )
    client.update_session(TEST_SESSION_ID, tags=["new-tag"])
    body = json.loads(route.calls.last.request.content)
    assert body == {"tags": ["new-tag"]}


def test_update_session_noop_makes_no_request(client):
    """update_session with no fields should not make any HTTP request."""
    with respx.mock:
        # No routes registered — any request would raise a respx error.
        client.update_session(TEST_SESSION_ID)
        assert respx.calls.call_count == 0


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


@respx.mock
def test_401_raises_authentication_error(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(401, json={"detail": "Invalid API key"})
    )
    with pytest.raises(AuthenticationError, match="Authentication failed"):
        client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")


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


@respx.mock
def test_404_raises_not_found_error(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:
        client.create_session_artifact(
            session_id=TEST_SESSION_ID, artifact_name="x", artifact="y"
        )
    assert exc_info.value.status_code == 404


@respx.mock
def test_422_raises_validation_error(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(422, json={"detail": "name too long"})
    )
    with pytest.raises(ValidationError) as exc_info:
        client.create_session(name="x" * 300, project_id=TEST_PROJECT_ID, agent_id="a")
    assert exc_info.value.status_code == 422


@respx.mock
def test_429_raises_rate_limit_error(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(429, json={"detail": "Rate limit exceeded"})
    )
    with patch("time.sleep"):
        with pytest.raises(RateLimitError) as exc_info:
            client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert exc_info.value.status_code == 429


@respx.mock
def test_500_raises_api_error(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(500, json={"detail": "Internal server error"})
    )
    with patch("time.sleep"):
        with pytest.raises(APIError) as exc_info:
            client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert exc_info.value.status_code == 500


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


@respx.mock
def test_fail_silently_create_artifact_returns_none(silent_client):
    respx.post(f"{BASE_URL}/agent_sessions/{TEST_SESSION_ID}/artifacts").mock(
        return_value=httpx.Response(500, json={"detail": "error"})
    )
    with patch("time.sleep"):
        result = silent_client.create_session_artifact(
            session_id=TEST_SESSION_ID, artifact_name="x", artifact="y"
        )
    assert result is None


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


@respx.mock
def test_retry_succeeds_on_third_attempt(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("time.sleep"):
        result = client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert result == TEST_SESSION_ID
    assert route.call_count == 3


@respx.mock
def test_retry_exhausted_raises(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(500, json={"detail": "persistent error"})
    )
    with patch("time.sleep"):
        with pytest.raises(APIError) as exc_info:
            client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert exc_info.value.status_code == 500


@respx.mock
def test_404_not_retried(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(404, json={"detail": "not found"})
    )
    with pytest.raises(NotFoundError):
        client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert route.call_count == 1


@respx.mock
def test_422_not_retried(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(422, json={"detail": "invalid"})
    )
    with pytest.raises(ValidationError):
        client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert route.call_count == 1


@respx.mock
def test_network_error_retried_then_raises(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        side_effect=httpx.ConnectError("connection refused")
    )
    with patch("time.sleep"):
        with pytest.raises(NetworkError, match="Network error"):
            client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")


@respx.mock
def test_network_error_succeeds_after_retry(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        side_effect=[
            httpx.ConnectError("connection refused"),
            httpx.Response(201, json={"session_id": TEST_SESSION_ID}),
        ]
    )
    with patch("time.sleep"):
        result = client.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert result == TEST_SESSION_ID
    assert route.call_count == 2


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


@respx.mock
def test_context_manager(monkeypatch):
    monkeypatch.delenv("INFLUXION_BASE_URL", raising=False)
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": TEST_SESSION_ID})
    )
    with InfluxionClient(api_key=TEST_API_KEY, fail_silently=False) as c:
        result = c.create_session(name="x", project_id=TEST_PROJECT_ID, agent_id="a")
    assert result == TEST_SESSION_ID
