"""
Tests for all service clients — covers all 14 API endpoints (sync + async).
Uses `responses` for sync mocking and `respx` for async mocking.
"""

from __future__ import annotations

import pytest
import responses as resp_lib
import respx
import httpx

from sentryagent_idp.errors import AgentIdPError
from sentryagent_idp.services.agents import AgentRegistryClient, AsyncAgentRegistryClient
from sentryagent_idp.services.credentials import CredentialClient, AsyncCredentialClient
from sentryagent_idp.services.token import TokenClient, AsyncTokenClient
from sentryagent_idp.services.audit import AuditClient, AsyncAuditClient
from sentryagent_idp.types import RegisterAgentRequest, UpdateAgentRequest

BASE = "http://localhost:3000"
TOKEN = "test-bearer-token"


def get_token() -> str:
    return TOKEN


async def async_get_token() -> str:
    return TOKEN


# ─── Fixtures ─────────────────────────────────────────────────────────────────

AGENT = {
    "agentId": "uuid-1", "email": "a@b.ai", "agentType": "screener",
    "version": "1.0.0", "capabilities": ["read"], "owner": "team",
    "deploymentEnv": "production", "status": "active",
    "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z",
}
PAGINATED_AGENTS = {"data": [AGENT], "total": 1, "page": 1, "limit": 20}

CRED = {
    "credentialId": "cred-1", "clientId": "uuid-1", "status": "active",
    "createdAt": "2026-01-01T00:00:00Z", "expiresAt": None, "revokedAt": None,
}
CRED_WITH_SECRET = {**CRED, "clientSecret": "sk_live_abc"}
PAGINATED_CREDS = {"data": [CRED], "total": 1, "page": 1, "limit": 20}

INTROSPECT_ACTIVE = {"active": True, "sub": "uuid-1", "exp": 9999999999}
INTROSPECT_INACTIVE = {"active": False}

AUDIT_EVENT = {
    "eventId": "ev-1", "agentId": "uuid-1", "action": "token.issued",
    "outcome": "success", "ipAddress": "1.2.3.4", "userAgent": "curl",
    "metadata": {}, "timestamp": "2026-01-01T00:00:00Z",
}
PAGINATED_AUDIT = {"data": [AUDIT_EVENT], "total": 1, "page": 1, "limit": 20}


# ─── Agent Registry — Sync ────────────────────────────────────────────────────

@resp_lib.activate
def test_register_agent() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/agents", json=AGENT, status=201)
    client = AgentRegistryClient(BASE, get_token)
    agent = client.register_agent(RegisterAgentRequest(
        email="a@b.ai", agent_type="screener", version="1.0.0",
        capabilities=["read"], owner="team", deployment_env="production",
    ))
    assert agent.agent_id == "uuid-1"


@resp_lib.activate
def test_list_agents() -> None:
    resp_lib.add(resp_lib.GET, f"{BASE}/api/v1/agents", json=PAGINATED_AGENTS, status=200)
    client = AgentRegistryClient(BASE, get_token)
    result = client.list_agents()
    assert result.total == 1


@resp_lib.activate
def test_get_agent() -> None:
    resp_lib.add(resp_lib.GET, f"{BASE}/api/v1/agents/uuid-1", json=AGENT, status=200)
    client = AgentRegistryClient(BASE, get_token)
    agent = client.get_agent("uuid-1")
    assert agent.agent_id == "uuid-1"


@resp_lib.activate
def test_update_agent() -> None:
    resp_lib.add(resp_lib.PATCH, f"{BASE}/api/v1/agents/uuid-1", json=AGENT, status=200)
    client = AgentRegistryClient(BASE, get_token)
    agent = client.update_agent("uuid-1", UpdateAgentRequest(version="2.0.0"))
    assert agent.agent_id == "uuid-1"


@resp_lib.activate
def test_decommission_agent() -> None:
    resp_lib.add(resp_lib.DELETE, f"{BASE}/api/v1/agents/uuid-1", status=204)
    client = AgentRegistryClient(BASE, get_token)
    result = client.decommission_agent("uuid-1")
    assert result is None


@resp_lib.activate
def test_agent_not_found_raises() -> None:
    resp_lib.add(
        resp_lib.GET, f"{BASE}/api/v1/agents/bad-id",
        json={"code": "AgentNotFoundError", "message": "Not found."}, status=404,
    )
    client = AgentRegistryClient(BASE, get_token)
    with pytest.raises(AgentIdPError) as exc_info:
        client.get_agent("bad-id")
    assert exc_info.value.code == "AgentNotFoundError"
    assert exc_info.value.http_status == 404


# ─── Credentials — Sync ───────────────────────────────────────────────────────

@resp_lib.activate
def test_generate_credential() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/agents/uuid-1/credentials", json=CRED_WITH_SECRET, status=201)
    client = CredentialClient(BASE, get_token)
    cred = client.generate_credential("uuid-1")
    assert cred.client_secret == "sk_live_abc"


@resp_lib.activate
def test_list_credentials() -> None:
    resp_lib.add(resp_lib.GET, f"{BASE}/api/v1/agents/uuid-1/credentials", json=PAGINATED_CREDS, status=200)
    client = CredentialClient(BASE, get_token)
    result = client.list_credentials("uuid-1")
    assert result.total == 1


@resp_lib.activate
def test_rotate_credential() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/agents/uuid-1/credentials/cred-1/rotate", json=CRED_WITH_SECRET, status=200)
    client = CredentialClient(BASE, get_token)
    cred = client.rotate_credential("uuid-1", "cred-1")
    assert cred.client_secret == "sk_live_abc"


@resp_lib.activate
def test_revoke_credential() -> None:
    revoked = {**CRED, "status": "revoked", "revokedAt": "2026-01-02T00:00:00Z"}
    resp_lib.add(resp_lib.DELETE, f"{BASE}/api/v1/agents/uuid-1/credentials/cred-1", json=revoked, status=200)
    client = CredentialClient(BASE, get_token)
    cred = client.revoke_credential("uuid-1", "cred-1")
    assert cred.status == "revoked"


# ─── Token — Sync ─────────────────────────────────────────────────────────────

@resp_lib.activate
def test_introspect_token_active() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/token/introspect", json=INTROSPECT_ACTIVE, status=200)
    client = TokenClient(BASE, get_token)
    result = client.introspect_token("some-token")
    assert result.active is True
    assert result.sub == "uuid-1"


@resp_lib.activate
def test_introspect_token_inactive() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/token/introspect", json=INTROSPECT_INACTIVE, status=200)
    client = TokenClient(BASE, get_token)
    result = client.introspect_token("expired-token")
    assert result.active is False


@resp_lib.activate
def test_revoke_token() -> None:
    resp_lib.add(resp_lib.POST, f"{BASE}/api/v1/token/revoke", json={}, status=200)
    client = TokenClient(BASE, get_token)
    result = client.revoke_token("some-token")
    assert result is None


# ─── Audit — Sync ─────────────────────────────────────────────────────────────

@resp_lib.activate
def test_query_audit_log() -> None:
    resp_lib.add(resp_lib.GET, f"{BASE}/api/v1/audit", json=PAGINATED_AUDIT, status=200)
    client = AuditClient(BASE, get_token)
    result = client.query_audit_log(agent_id="uuid-1", action="token.issued")
    assert result.total == 1
    assert result.data[0].event_id == "ev-1"


@resp_lib.activate
def test_get_audit_event() -> None:
    resp_lib.add(resp_lib.GET, f"{BASE}/api/v1/audit/ev-1", json=AUDIT_EVENT, status=200)
    client = AuditClient(BASE, get_token)
    event = client.get_audit_event("ev-1")
    assert event.event_id == "ev-1"


# ─── Async — all 14 endpoints ─────────────────────────────────────────────────

@pytest.mark.asyncio
async def test_async_register_agent() -> None:
    with respx.mock:
        respx.post(f"{BASE}/api/v1/agents").mock(return_value=httpx.Response(201, json=AGENT))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        agent = await client.register_agent(RegisterAgentRequest(
            email="a@b.ai", agent_type="screener", version="1.0.0",
            capabilities=["read"], owner="team", deployment_env="production",
        ))
        assert agent.agent_id == "uuid-1"


@pytest.mark.asyncio
async def test_async_list_agents() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/agents").mock(return_value=httpx.Response(200, json=PAGINATED_AGENTS))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        result = await client.list_agents()
        assert result.total == 1


@pytest.mark.asyncio
async def test_async_get_agent() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/agents/uuid-1").mock(return_value=httpx.Response(200, json=AGENT))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        agent = await client.get_agent("uuid-1")
        assert agent.agent_id == "uuid-1"


@pytest.mark.asyncio
async def test_async_update_agent() -> None:
    with respx.mock:
        respx.patch(f"{BASE}/api/v1/agents/uuid-1").mock(return_value=httpx.Response(200, json=AGENT))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        agent = await client.update_agent("uuid-1", UpdateAgentRequest(version="2.0.0"))
        assert agent.agent_id == "uuid-1"


@pytest.mark.asyncio
async def test_async_decommission_agent() -> None:
    with respx.mock:
        respx.delete(f"{BASE}/api/v1/agents/uuid-1").mock(return_value=httpx.Response(204))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        result = await client.decommission_agent("uuid-1")
        assert result is None


@pytest.mark.asyncio
async def test_async_generate_credential() -> None:
    with respx.mock:
        respx.post(f"{BASE}/api/v1/agents/uuid-1/credentials").mock(
            return_value=httpx.Response(201, json=CRED_WITH_SECRET))
        client = AsyncCredentialClient(BASE, async_get_token)
        cred = await client.generate_credential("uuid-1")
        assert cred.client_secret == "sk_live_abc"


@pytest.mark.asyncio
async def test_async_list_credentials() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/agents/uuid-1/credentials").mock(
            return_value=httpx.Response(200, json=PAGINATED_CREDS))
        client = AsyncCredentialClient(BASE, async_get_token)
        result = await client.list_credentials("uuid-1")
        assert result.total == 1


@pytest.mark.asyncio
async def test_async_rotate_credential() -> None:
    with respx.mock:
        respx.post(f"{BASE}/api/v1/agents/uuid-1/credentials/cred-1/rotate").mock(
            return_value=httpx.Response(200, json=CRED_WITH_SECRET))
        client = AsyncCredentialClient(BASE, async_get_token)
        cred = await client.rotate_credential("uuid-1", "cred-1")
        assert cred.client_secret == "sk_live_abc"


@pytest.mark.asyncio
async def test_async_revoke_credential() -> None:
    revoked = {**CRED, "status": "revoked", "revokedAt": "2026-01-02T00:00:00Z"}
    with respx.mock:
        respx.delete(f"{BASE}/api/v1/agents/uuid-1/credentials/cred-1").mock(
            return_value=httpx.Response(200, json=revoked))
        client = AsyncCredentialClient(BASE, async_get_token)
        cred = await client.revoke_credential("uuid-1", "cred-1")
        assert cred.status == "revoked"


@pytest.mark.asyncio
async def test_async_introspect_token() -> None:
    with respx.mock:
        respx.post(f"{BASE}/api/v1/token/introspect").mock(
            return_value=httpx.Response(200, json=INTROSPECT_ACTIVE))
        client = AsyncTokenClient(BASE, async_get_token)
        result = await client.introspect_token("tok")
        assert result.active is True


@pytest.mark.asyncio
async def test_async_revoke_token() -> None:
    with respx.mock:
        respx.post(f"{BASE}/api/v1/token/revoke").mock(return_value=httpx.Response(200, json={}))
        client = AsyncTokenClient(BASE, async_get_token)
        await client.revoke_token("tok")


@pytest.mark.asyncio
async def test_async_query_audit_log() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/audit").mock(return_value=httpx.Response(200, json=PAGINATED_AUDIT))
        client = AsyncAuditClient(BASE, async_get_token)
        result = await client.query_audit_log()
        assert result.total == 1


@pytest.mark.asyncio
async def test_async_get_audit_event() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/audit/ev-1").mock(return_value=httpx.Response(200, json=AUDIT_EVENT))
        client = AsyncAuditClient(BASE, async_get_token)
        event = await client.get_audit_event("ev-1")
        assert event.event_id == "ev-1"


# ─── Error propagation ────────────────────────────────────────────────────────

@resp_lib.activate
def test_api_error_propagated_from_service() -> None:
    resp_lib.add(
        resp_lib.GET, f"{BASE}/api/v1/agents/bad",
        json={"code": "AgentNotFoundError", "message": "Not found."}, status=404,
    )
    client = AgentRegistryClient(BASE, get_token)
    with pytest.raises(AgentIdPError) as exc_info:
        client.get_agent("bad")
    assert exc_info.value.http_status == 404


@pytest.mark.asyncio
async def test_async_api_error_propagated() -> None:
    with respx.mock:
        respx.get(f"{BASE}/api/v1/agents/bad").mock(return_value=httpx.Response(
            404, json={"code": "AgentNotFoundError", "message": "Not found."}
        ))
        client = AsyncAgentRegistryClient(BASE, async_get_token)
        with pytest.raises(AgentIdPError) as exc_info:
            await client.get_agent("bad")
        assert exc_info.value.http_status == 404
