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

from __future__ import annotations

import json
from unittest.mock import patch
import httpx
import pytest
import respx

from influxion import InfluxionClient
from influxion._exceptions import (
    APIError,
    AuthenticationError,
    ConflictError,
    InfluxionError,
    NetworkError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

from .conftest import (
    BASE_URL,
    TEST_API_KEY,
    TEST_AGENT_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={})
        )
        c = InfluxionClient(fail_silently=False)
        result = c.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
        c.close()
    assert result is None


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={})
        )
        c = InfluxionClient(api_key=TEST_API_KEY, fail_silently=False)
        c.create_session(
            session_id="x", 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_succeeds_returns_none(client):
    respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={"session_id": "ignored"})
    )
    result = client.create_session(
        session_id="test-run",
        name="test-run",
        project_id=TEST_PROJECT_ID,
        agent_id="my-agent",
    )
    assert result is None


@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={})
    )
    client.create_session(
        session_id="my-run",
        name="my-run",
        project_id=TEST_PROJECT_ID,
        agent_id="agent-1",
    )
    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"
    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={})
    )
    client.create_session(
        session_id="run",
        name="run",
        tags=["weekly", "prod"],
        metadata={"env": "prod", "version": 2},
        project_id=TEST_PROJECT_ID,
        agent_id="a",
    )
    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_sends_project_id_in_body(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(201, json={})
    )
    client.create_session(
        session_id="run",
        name="run",
        project_id=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={})
    )
    client.create_session(
        session_id="x", 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_succeeds_returns_none(client):
    respx.post(f"{BASE_URL}/agent_sessions/artifacts").mock(
        return_value=httpx.Response(201)
    )
    result = client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        name="report",
        artifact="The results show...",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    assert result is None


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


@respx.mock
def test_create_session_artifact_custom_type(client):
    route = respx.post(f"{BASE_URL}/agent_sessions/artifacts").mock(
        return_value=httpx.Response(201)
    )
    client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        name="img",
        artifact="<base64>",
        type="image",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    body = json.loads(route.calls.last.request.content)
    assert body["type"] == "image"


@respx.mock
def test_create_session_artifact_session_id_in_body(client):
    """session_id, project_id, and agent_id are sent in the request body; path is flat."""
    route = respx.post(f"{BASE_URL}/agent_sessions/artifacts").mock(
        return_value=httpx.Response(201)
    )
    result = client.create_session_artifact(
        session_id="my-session-id",
        name="report",
        artifact="...",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    assert result is None
    assert route.called
    body = json.loads(route.calls.last.request.content)
    assert body["session_id"] == "my-session-id"
    assert body["project_id"] == TEST_PROJECT_ID
    assert body["agent_id"] == TEST_AGENT_ID


@respx.mock
def test_create_session_artifact_requires_project_id_when_not_in_init(reset_global):
    """project_id and agent_id must be set via init() or passed when not in global."""
    c = InfluxionClient(api_key=TEST_API_KEY, fail_silently=False)
    try:
        with pytest.raises(InfluxionError, match="project_id is required"):
            c.create_session_artifact(
                session_id="x",
                name="x",
                artifact="y",
            )
    finally:
        c.close()


@respx.mock
def test_create_session_artifact_with_optional_fields(client):
    route = respx.post(f"{BASE_URL}/agent_sessions/artifacts").mock(
        return_value=httpx.Response(201)
    )
    client.create_session_artifact(
        session_id=TEST_SESSION_ID,
        name="output",
        artifact="...",
        tags=["final"],
        metadata={"step": "3"},
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    body = json.loads(route.calls.last.request.content)
    assert body["tags"] == ["final"]
    assert body["metadata"] == {"step": "3"}


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


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


@respx.mock
def test_create_session_tool_call_minimal_payload(client):
    route = respx.post(f"{BASE_URL}/agent_sessions/tool_calls").mock(
        return_value=httpx.Response(201)
    )
    client.create_session_tool_call(
        session_id=TEST_SESSION_ID,
        name="fetch",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    body = json.loads(route.calls.last.request.content)
    assert body["session_id"] == TEST_SESSION_ID
    assert body["name"] == "fetch"
    assert body["project_id"] == TEST_PROJECT_ID
    assert body["agent_id"] == TEST_AGENT_ID
    assert "input" not in body
    assert "output" not in body
    assert "tags" not in body
    assert "metadata" not in body


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


@respx.mock
def test_create_session_tool_call_404_raises(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:
        client.create_session_tool_call(
            session_id=TEST_SESSION_ID,
            name="x",
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
        )
    assert exc_info.value.status_code == 404


@respx.mock
def test_create_session_tool_call_fail_silently_returns_none(silent_client):
    respx.post(f"{BASE_URL}/agent_sessions/tool_calls").mock(
        return_value=httpx.Response(500, json={"detail": "Internal error"})
    )
    result = silent_client.create_session_tool_call(
        session_id=TEST_SESSION_ID,
        name="x",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    assert result is None  # same as success: no ID returned


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


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


@respx.mock
def test_end_session_failure_with_message(client):
    route = respx.patch(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(200, json={})
    )
    client.end_session(
        session_id=TEST_SESSION_ID,
        success=False,
        failure_message="Timed out",
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    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").mock(
        return_value=httpx.Response(200, json={})
    )
    client.update_session(
        session_id=TEST_SESSION_ID,
        tags=["new-tag"],
        project_id=TEST_PROJECT_ID,
        agent_id=TEST_AGENT_ID,
    )
    body = json.loads(route.calls.last.request.content)
    assert body["session_id"] == TEST_SESSION_ID
    assert body["tags"] == ["new-tag"]
    assert body["project_id"] == TEST_PROJECT_ID
    assert body["agent_id"] == TEST_AGENT_ID


def test_update_session_noop_makes_no_request(client):
    """update_session with no mutable fields should not make any HTTP request."""
    with respx.mock:
        # No routes registered — any request would raise a respx error.
        client.update_session(session_id=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(
        session_id="x", 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(
        session_id="x", 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/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,
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
            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(
            session_id="x", name="x" * 300, project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert exc_info.value.status_code == 422


@respx.mock
def test_409_raises_conflict_error(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:
        client.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert exc_info.value.status_code == 409
    assert "Session already exists" in exc_info.value.message


@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(
        session_id="x", 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(
        session_id="x", 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(
            session_id="x", 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/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,
            project_id=TEST_PROJECT_ID,
            agent_id=TEST_AGENT_ID,
            name="x",
            artifact="y",
        )
    assert result is None  # same as success: no ID returned


# ---------------------------------------------------------------------------
# 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={}),
        ]
    )
    with patch("time.sleep"):
        result = client.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert result is None
    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(
        session_id="x", 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(
        session_id="x", 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(
        session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
    )
    assert route.call_count == 1


@respx.mock
def test_409_not_retried(client):
    route = respx.post(f"{BASE_URL}/agent_sessions").mock(
        return_value=httpx.Response(409, json={"detail": "Session already exists"})
    )
    with pytest.raises(ConflictError):
        client.create_session(
            session_id="x", 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(
        session_id="x", 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={}),
        ]
    )
    with patch("time.sleep"):
        result = client.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert result is None
    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={})
    )
    result = None
    with InfluxionClient(api_key=TEST_API_KEY, fail_silently=False) as c:
        result = c.create_session(
            session_id="x", name="x", project_id=TEST_PROJECT_ID, agent_id="a"
        )
    assert result is None
