"""Unit tests for AsyncNovyx SDK client."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from novyx import NovyxAuthError

# Import AsyncNovyx directly (triggers httpx requirement)
from novyx.async_client import AsyncNovyx


@pytest.fixture
def nx():
    """Create AsyncNovyx client with test API key."""
    return AsyncNovyx(api_key="nram_test_12345678", api_url="https://test.example.com")


def _mock_response(status_code=200, json_data=None, text="", content=b""):
    """Create a mock httpx response."""
    resp = MagicMock()
    resp.status_code = status_code
    resp.text = text or (str(json_data) if json_data else "")
    resp.json.return_value = json_data or {}
    resp.content = content
    resp.raise_for_status = MagicMock()
    return resp


def _set_mock_client(nx, mock_resp):
    """Set a mock httpx client on the AsyncNovyx instance."""
    mock_client = MagicMock()
    mock_client.is_closed = False
    mock_client.request = AsyncMock(return_value=mock_resp)
    nx._client = mock_client
    return mock_client


# ============================================================================
# Initialization Tests
# ============================================================================

def test_async_init_valid_key():
    """Test initialization with valid API key."""
    client = AsyncNovyx(api_key="nram_tenant_signature")
    assert client.api_key == "nram_tenant_signature"
    assert client.api_url == "https://novyx-ram-api.fly.dev"


def test_async_init_invalid_key():
    """Test initialization with invalid API key."""
    with pytest.raises(NovyxAuthError):
        AsyncNovyx(api_key="invalid_key")


def test_async_headers(nx):
    """Test request headers."""
    headers = nx._headers()
    assert headers["Authorization"] == "Bearer nram_test_12345678"
    assert headers["Content-Type"] == "application/json"


# ============================================================================
# Memory Methods Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_remember(nx):
    """Test remember() stores a memory."""
    mock_resp = _mock_response(json_data={"uuid": "test-uuid", "message": "Stored"})
    _set_mock_client(nx, mock_resp)

    result = await nx.remember("Test memory", tags=["test"], importance=7)

    assert result["uuid"] == "test-uuid"
    call_kwargs = nx._client.request.call_args[1]
    assert call_kwargs["json"]["observation"] == "Test memory"


@pytest.mark.asyncio
async def test_async_recall(nx):
    """Test recall() searches memories."""
    mock_resp = _mock_response(
        json_data=[{"observation": "Test memory", "score": 0.95, "uuid": "test-uuid"}]
    )
    _set_mock_client(nx, mock_resp)

    result = await nx.recall("test query", limit=5)

    assert len(result) == 1
    assert result.memories[0].observation == "Test memory"


@pytest.mark.asyncio
async def test_async_memories(nx):
    """Test memories() lists all memories."""
    mock_resp = _mock_response(
        json_data={"memories": [{"uuid": "test-1", "observation": "Memory 1"}]}
    )
    _set_mock_client(nx, mock_resp)

    result = await nx.memories(limit=100)

    assert len(result) == 1


@pytest.mark.asyncio
async def test_async_forget(nx):
    """Test forget() deletes a memory."""
    mock_resp = _mock_response(json_data={"success": True})
    _set_mock_client(nx, mock_resp)

    result = await nx.forget("test-uuid")

    assert result is True


# ============================================================================
# Rollback Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_rollback(nx):
    """Test rollback() executes rollback."""
    mock_resp = _mock_response(json_data={
        "success": True, "rolled_back_to": "2026-01-15T10:00:00Z", "artifacts_restored": 5
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.rollback("2 hours ago")

    assert result["success"] is True
    assert result["artifacts_restored"] == 5


# ============================================================================
# Audit Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_audit(nx):
    """Test audit() retrieves audit entries."""
    mock_resp = _mock_response(json_data={
        "entries": [{"timestamp": "2026-01-15T10:00:00Z", "operation": "CREATE"}]
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.audit(limit=50)

    assert len(result) == 1
    assert result[0]["operation"] == "CREATE"


# ============================================================================
# Eval Methods Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_eval_run(nx):
    """Test eval_run() runs health evaluation."""
    mock_resp = _mock_response(json_data={
        "eval_id": "eval_abc123",
        "health_score": 91.2,
        "breakdown": {
            "recall_consistency": 95.0,
            "drift_score": 88.0,
            "conflict_score": 100.0,
            "staleness_score": 72.0,
        },
        "memory_count": 500,
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_run()

    assert result["health_score"] == 91.2
    assert result["breakdown"]["recall_consistency"] == 95.0


@pytest.mark.asyncio
async def test_async_eval_gate(nx):
    """Test eval_gate() CI/CD quality gate."""
    mock_resp = _mock_response(json_data={
        "eval_id": "eval_abc123",
        "health_score": 95.0,
        "passed": True,
        "min_score": 80.0,
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_gate(min_score=80.0)

    assert result["passed"] is True
    call_kwargs = nx._client.request.call_args[1]
    assert call_kwargs["json"]["min_score"] == 80.0


@pytest.mark.asyncio
async def test_async_eval_history(nx):
    """Test eval_history() lists past evals."""
    mock_resp = _mock_response(json_data={
        "entries": [
            {"eval_id": "eval_1", "health_score": 90.0},
            {"eval_id": "eval_2", "health_score": 85.0},
        ],
        "total_count": 2,
        "has_more": False,
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_history(limit=50)

    assert len(result["entries"]) == 2


@pytest.mark.asyncio
async def test_async_eval_drift(nx):
    """Test eval_drift() returns drift analysis."""
    mock_resp = _mock_response(json_data={
        "memory_count_delta": 12,
        "avg_importance_delta": 0.3,
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_drift(days=7)

    assert result["memory_count_delta"] == 12


@pytest.mark.asyncio
async def test_async_eval_baseline_create(nx):
    """Test eval_baseline_create() saves a baseline."""
    mock_resp = _mock_response(json_data={
        "id": "bl_abc123",
        "query": "capital of France",
        "expected_observation": "Paris",
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_baseline_create("capital of France", "Paris")

    assert result["id"] == "bl_abc123"


@pytest.mark.asyncio
async def test_async_eval_baselines(nx):
    """Test eval_baselines() lists all baselines."""
    mock_resp = _mock_response(json_data={
        "baselines": [{"id": "bl_1", "query": "test"}],
        "total_count": 1,
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_baselines()

    assert len(result["baselines"]) == 1


@pytest.mark.asyncio
async def test_async_eval_baseline_delete(nx):
    """Test eval_baseline_delete() deletes a baseline."""
    mock_resp = _mock_response(json_data={"success": True})
    _set_mock_client(nx, mock_resp)

    result = await nx.eval_baseline_delete("bl_abc123")

    assert result is True


# ============================================================================
# Trace Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_trace_create(nx):
    """Test trace_create() creates trace session."""
    mock_resp = _mock_response(json_data={
        "trace_id": "trace-123", "agent_id": "my-agent"
    })
    _set_mock_client(nx, mock_resp)

    result = await nx.trace_create("my-agent")

    assert result["trace_id"] == "trace-123"


# ============================================================================
# Context Manager Test
# ============================================================================

@pytest.mark.asyncio
async def test_async_context_manager():
    """Test async context manager returns self and calls close."""
    client = AsyncNovyx(api_key="nram_test_12345678", api_url="https://test.example.com")

    # Pre-set mock client so close() has something to close
    mock_http = MagicMock()
    mock_http.is_closed = False
    mock_http.aclose = AsyncMock()
    client._client = mock_http

    async with client as ctx:
        assert ctx is client

    mock_http.aclose.assert_called_once()


# ============================================================================
# Error Handling Tests
# ============================================================================

@pytest.mark.asyncio
async def test_async_error_401(nx):
    """Test 401 raises NovyxAuthError."""
    from novyx import NovyxAuthError
    mock_resp = _mock_response(status_code=401)
    _set_mock_client(nx, mock_resp)

    with pytest.raises(NovyxAuthError):
        await nx._request("GET", "/test")


@pytest.mark.asyncio
async def test_async_error_403(nx):
    """Test 403 raises NovyxForbiddenError."""
    from novyx import NovyxForbiddenError
    mock_resp = _mock_response(
        status_code=403,
        json_data={"error": "Pro tier required"},
        text='{"error": "Pro tier required"}'
    )
    _set_mock_client(nx, mock_resp)

    with pytest.raises(NovyxForbiddenError):
        await nx._request("GET", "/test")


@pytest.mark.asyncio
async def test_async_error_429(nx):
    """Test 429 raises NovyxRateLimitError."""
    from novyx import NovyxRateLimitError
    mock_resp = _mock_response(
        status_code=429,
        json_data={"error": "Rate limit exceeded", "current": 100, "limit": 100},
        text='{"error": "Rate limit exceeded"}'
    )
    _set_mock_client(nx, mock_resp)

    with pytest.raises(NovyxRateLimitError):
        await nx._request("GET", "/test")


# =========================================================================
# Sentinel Intel — Threat Intelligence
# =========================================================================


@pytest.mark.asyncio
async def test_async_threat_feed(nx):
    mock_resp = _mock_response(json_data=[{"signature_id": "sig_1", "severity": "high"}])
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_feed(hours=24, min_severity="high")
    assert len(result) == 1


@pytest.mark.asyncio
async def test_async_threat_stats(nx):
    mock_resp = _mock_response(json_data={"total_signatures": 5, "mitigated_count": 2})
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_stats()
    assert result["total_signatures"] == 5


@pytest.mark.asyncio
async def test_async_threat_record(nx):
    mock_resp = _mock_response(json_data={"signature_id": "sig_new", "severity": "critical"})
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_record({"pattern_type": "injection", "severity": "critical"})
    assert result["signature_id"] == "sig_new"


@pytest.mark.asyncio
async def test_async_threat_trending(nx):
    mock_resp = _mock_response(json_data=[{"signature_id": "sig_1", "occurrence_count": 10}])
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_trending(hours=48, min_occurrences=3)
    assert len(result) == 1


@pytest.mark.asyncio
async def test_async_threat_match(nx):
    mock_resp = _mock_response(json_data=[{"signature_id": "sig_1"}])
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_match({"pattern_type": "xss"}, min_similarity=0.9)
    assert len(result) == 1


@pytest.mark.asyncio
async def test_async_threat_signature(nx):
    mock_resp = _mock_response(json_data={"signature_id": "sig_1", "severity": "critical"})
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_signature("sig_1")
    assert result["severity"] == "critical"


@pytest.mark.asyncio
async def test_async_threat_mitigate(nx):
    mock_resp = _mock_response(json_data={"mitigated": True})
    _set_mock_client(nx, mock_resp)
    result = await nx.threat_mitigate("sig_1")
    assert result["mitigated"] is True


# =========================================================================
# Sentinel Intel — Auto Defense
# =========================================================================


@pytest.mark.asyncio
async def test_async_defense_list(nx):
    mock_resp = _mock_response(json_data=[{"defense_id": "def_1", "rule_type": "block"}])
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_list()
    assert len(result) == 1


@pytest.mark.asyncio
async def test_async_defense_deploy(nx):
    mock_resp = _mock_response(json_data={"defense_id": "def_new", "rule_type": "block"})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_deploy("sig_1", "block")
    assert result["defense_id"] == "def_new"


@pytest.mark.asyncio
async def test_async_defense_remove(nx):
    mock_resp = _mock_response(json_data={"removed": True})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_remove("def_1")
    assert result["removed"] is True


@pytest.mark.asyncio
async def test_async_defense_effectiveness(nx):
    mock_resp = _mock_response(json_data={"defense_id": "def_1", "effectiveness": 0.85})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_effectiveness("def_1")
    assert result["effectiveness"] == 0.85


@pytest.mark.asyncio
async def test_async_defense_record_block(nx):
    mock_resp = _mock_response(json_data={"recorded": True})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_record_block("def_1", is_false_positive=True)
    assert result["recorded"] is True


@pytest.mark.asyncio
async def test_async_defense_stats(nx):
    mock_resp = _mock_response(json_data={"total_defenses": 10, "active_defenses": 7})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_stats()
    assert result["active_defenses"] == 7


@pytest.mark.asyncio
async def test_async_defense_recommend(nx):
    mock_resp = _mock_response(json_data={"rule_type": "block", "confidence": 0.9})
    _set_mock_client(nx, mock_resp)
    result = await nx.defense_recommend("sig_1")
    assert result["confidence"] == 0.9


# =========================================================================
# Sentinel Intel — Correlation
# =========================================================================


@pytest.mark.asyncio
async def test_async_correlate_threat(nx):
    mock_resp = _mock_response(json_data={"is_correlated": True, "matching_signatures": []})
    _set_mock_client(nx, mock_resp)
    result = await nx.correlate_threat({"pattern_type": "sqli"})
    assert result["is_correlated"] is True


@pytest.mark.asyncio
async def test_async_detect_campaign(nx):
    mock_resp = _mock_response(json_data={"campaign_detected": False})
    _set_mock_client(nx, mock_resp)
    result = await nx.detect_campaign(hours=48)
    assert result["campaign_detected"] is False


@pytest.mark.asyncio
async def test_async_coordinated_attack_check(nx):
    mock_resp = _mock_response(json_data={"is_coordinated": True})
    _set_mock_client(nx, mock_resp)
    result = await nx.coordinated_attack_check([{"type": "xss"}, {"type": "xss"}])
    assert result["is_coordinated"] is True


@pytest.mark.asyncio
async def test_async_related_signatures(nx):
    mock_resp = _mock_response(json_data=[{"signature_id": "sig_2"}])
    _set_mock_client(nx, mock_resp)
    result = await nx.related_signatures("sig_1", similarity_threshold=0.8)
    assert len(result) == 1


# =========================================================================
# Streams
# =========================================================================


@pytest.mark.asyncio
async def test_async_stream_status(nx):
    mock_resp = _mock_response(json_data={"active_connections": 3, "max_connections": 10})
    _set_mock_client(nx, mock_resp)
    result = await nx.stream_status()
    assert result["active_connections"] == 3
