#!/usr/bin/env python3
"""
D3cipher Gateway Integration Tests
====================================

Tests the gateway's own logic: agent routing, middleware lifecycle,
multi-agent isolation, error handling, and end-to-end chain verification.

The 195 unit tests in this directory validate the middleware layer.
These tests validate the gateway that wraps it.

Requires:
    - Gateway running on localhost:4000
    - Upstream serving /v1/chat/completions (adapter:8002 or model:8000)
    - LockStock server reachable
    - LOCKSTOCK_API_KEY environment variable set

Run:
    LOCKSTOCK_API_KEY=lsk_admin_... pytest test_gateway_integration.py -v
"""

import os
import time

import httpx
import pytest

GATEWAY_URL = os.environ.get("GATEWAY_URL", "http://localhost:4000")
LOCKSTOCK_URL = os.environ.get(
    "LOCKSTOCK_URL", "https://lockstock-api-i9kp.onrender.com"
)
API_KEY = os.environ.get("LOCKSTOCK_API_KEY", "")
EXISTING_AGENT = os.environ.get(
    "EXISTING_AGENT_ID", "agent_76fad4de_gatewaytestagent1"
)

CHAT_PAYLOAD = {
    "model": "trinity",
    "messages": [{"role": "user", "content": "What is 2+2? One word answer."}],
    "max_tokens": 32,
}


@pytest.fixture(scope="module")
def client():
    with httpx.Client(timeout=180.0) as c:
        yield c


@pytest.fixture(scope="module")
def auth_headers():
    assert API_KEY, "Set LOCKSTOCK_API_KEY environment variable"
    return {"Authorization": f"Bearer {API_KEY}"}


@pytest.fixture(scope="module")
def second_agent(client, auth_headers):
    """Provision and claim a throwaway agent for multi-agent tests."""
    resp = client.post(
        f"{LOCKSTOCK_URL}/v1/agents/provision",
        json={
            "agent_name": "gateway-isolation-test",
            "authorized_tasks": [
                "CHATCOMPLETION",
                "CHATCOMPLETIONRESPONSE",
            ],
        },
        headers={**auth_headers, "Content-Type": "application/json"},
    )
    assert resp.status_code == 200, f"Provision failed: {resp.text}"
    data = resp.json()
    agent_id = data["agent_id"]
    genesis_token = data["genesis_token"]

    resp = client.post(
        f"{LOCKSTOCK_URL}/v1/agents/claim",
        json={
            "genesis_token": genesis_token,
            "hardware_fingerprint": "gateway-isolation-test",
        },
        headers={"Content-Type": "application/json"},
    )
    assert resp.status_code == 200, f"Claim failed: {resp.text}"

    yield agent_id


def _get_agent_state(client, agent_id, auth_headers):
    """Helper: fetch agent chain state from server."""
    resp = client.get(
        f"{LOCKSTOCK_URL}/api/guard/agent/{agent_id}/state",
        headers=auth_headers,
    )
    assert resp.status_code == 200, f"State fetch failed: {resp.text}"
    return resp.json()


def _send_chat(client, agent_id):
    """Helper: send a chat completion through the gateway."""
    resp = client.post(
        f"{GATEWAY_URL}/v1/chat/completions",
        json=CHAT_PAYLOAD,
        headers={
            "Content-Type": "application/json",
            "X-D3cipher-Agent": agent_id,
        },
    )
    return resp


# ── Test 1: Health endpoint ──


def test_healthz(client):
    """GET /healthz returns status, agents_active, and inflight."""
    resp = client.get(f"{GATEWAY_URL}/healthz")
    assert resp.status_code == 200
    data = resp.json()
    assert data["status"] in ("ok", "degraded")
    assert isinstance(data["agents_active"], int)
    assert isinstance(data["inflight"], int)


# ── Test 2: Metrics endpoint ──


def test_metrics_endpoint(client):
    """GET /metrics returns Prometheus counters and gauges."""
    resp = client.get(f"{GATEWAY_URL}/metrics")
    assert resp.status_code == 200
    body = resp.text
    for metric in [
        "d3cipher_requests_total",
        "d3cipher_rejections_total",
        "d3cipher_request_duration_seconds_sum",
        "d3cipher_request_duration_seconds_count",
        "d3cipher_inflight",
        "d3cipher_agents_active",
    ]:
        assert metric in body, f"Missing metric: {metric}"


# ── Test 3: Missing header → 400 ──


def test_missing_agent_header_returns_400(client):
    """Request without X-D3cipher-Agent header gets 400, not a crash."""
    resp = client.post(
        f"{GATEWAY_URL}/v1/chat/completions",
        json=CHAT_PAYLOAD,
        headers={"Content-Type": "application/json"},
    )
    assert resp.status_code == 400
    data = resp.json()
    assert "agent" in data["error"].lower()


# ── Test 4: Unprovisioned agent → 503 ──


def test_unprovisioned_agent_returns_503(client):
    """Request for non-existent agent returns 503, not a crash or hang."""
    resp = client.post(
        f"{GATEWAY_URL}/v1/chat/completions",
        json=CHAT_PAYLOAD,
        headers={
            "Content-Type": "application/json",
            "X-D3cipher-Agent": "agent_00000000_does_not_exist",
        },
    )
    assert resp.status_code == 503
    data = resp.json()
    assert "error" in data


# ── Test 5: Agent resolution from header ──


def test_agent_resolution_from_header(client, auth_headers):
    """X-D3cipher-Agent header routes to correct agent's chain."""
    state_before = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    seq_before = state_before["last_sequence"]

    resp = _send_chat(client, EXISTING_AGENT)
    assert resp.status_code == 200

    # Response stamp is fire-and-forget async — give it time
    time.sleep(5)

    state_after = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    seq_after = state_after["last_sequence"]
    # Request stamp + response stamp = +2
    assert seq_after >= seq_before + 2, (
        f"Chain did not advance: {seq_before} -> {seq_after}"
    )


# ── Test 6: Dynamic middleware creation ──


def test_dynamic_middleware_creation(client, second_agent):
    """First request for a new agent creates its middleware on the fly."""
    resp = _send_chat(client, second_agent)
    assert resp.status_code == 200

    # Agent should now appear in metrics
    metrics = client.get(f"{GATEWAY_URL}/metrics").text
    assert second_agent in metrics, (
        f"Agent {second_agent} not in /metrics after first request"
    )

    # agents_active should include it
    health = client.get(f"{GATEWAY_URL}/healthz").json()
    assert health["agents_active"] >= 2  # at least existing + second


# ── Test 7: Multi-agent isolation ──


def test_multi_agent_isolation(client, second_agent, auth_headers):
    """Two agents have independent chains — advancing one does not touch the other."""
    state1_before = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    state2_before = _get_agent_state(client, second_agent, auth_headers)

    # Send a request ONLY through agent 2
    resp = _send_chat(client, second_agent)
    assert resp.status_code == 200
    time.sleep(5)

    # Agent 2 chain must advance
    state2_after = _get_agent_state(client, second_agent, auth_headers)
    assert state2_after["last_sequence"] > state2_before["last_sequence"], (
        "Agent 2 chain did not advance"
    )

    # Agent 1 chain must NOT advance
    state1_after = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    assert state1_after["last_sequence"] == state1_before["last_sequence"], (
        f"Agent 1 chain advanced unexpectedly: "
        f"{state1_before['last_sequence']} -> {state1_after['last_sequence']}"
    )
    assert state1_after["last_hash"] == state1_before["last_hash"], (
        "Agent 1 hash changed without a request"
    )


# ── Test 8: End-to-end with response + chain + matrix ──


def test_e2e_response_chain_and_matrix(client, auth_headers):
    """Full loop: gateway → stamp → upstream → response → response stamp → matrix evolves."""
    state_before = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    seq_before = state_before["last_sequence"]
    matrix_before = state_before["state_matrix"]

    resp = _send_chat(client, EXISTING_AGENT)
    assert resp.status_code == 200
    data = resp.json()

    # Response structure (OpenAI-compatible)
    assert "choices" in data, "Missing 'choices' in response"
    assert len(data["choices"]) > 0, "Empty choices array"
    assert "message" in data["choices"][0]
    assert "content" in data["choices"][0]["message"]
    assert len(data["choices"][0]["message"]["content"]) > 0

    time.sleep(5)

    state_after = _get_agent_state(client, EXISTING_AGENT, auth_headers)
    # Chain advanced by at least 2 (request + response)
    assert state_after["last_sequence"] >= seq_before + 2
    # Hash changed
    assert state_after["last_hash"] != state_before["last_hash"]
    # Matrix evolved (at least one element changed)
    assert state_after["state_matrix"] != matrix_before, (
        "State matrix did not evolve after stamp"
    )
