"""Integration tests — full stack: SDK → Rungate server → Mock Provider.

Run with: RUNGATE_SERVER_PATH=/path/to/rungate pytest -m integration -v
Requires: Node.js, built rungate server (npm run build:backend)
"""

from __future__ import annotations

import uuid
from typing import TYPE_CHECKING

import pytest

from rungate import Admin, Rungate
from rungate._exceptions import (
    AgentDisabledError,
    ApprovalRequiredError,
    AuthenticationError,
    ModelNotAllowedError,
    NotFoundError,
    ToolBlockedError,
)

pytest_plugins = ["tests.conftest_integration"]

if TYPE_CHECKING:
    from .conftest_integration import RunningServer
    from .mock_provider import MockOpenAIProvider

pytestmark = pytest.mark.integration


# ─── Helper ─────────────────────────────────────────────


def _create_agent(
    admin: Admin,
    mock_provider_port: int = 18081,
    *,
    allowed_models: list[str] | None = None,
    blocked_tools: list[str] | None = None,
    require_approval: list[str] | None = None,
    budget: dict | None = None,
) -> dict:
    """Create a test agent with unique names. Returns dict with api_key, agent_id, policy_id."""
    prefix = uuid.uuid4().hex[:8]

    key_result = admin.provider_keys.create(
        provider="openai",
        name=f"mock-{prefix}",
        api_key="sk-mock-test",
        base_url=f"http://127.0.0.1:{mock_provider_port}",
    )

    policy_kwargs: dict = {
        "name": f"policy-{prefix}",
        "allowed_models": allowed_models or ["gpt-4o"],
        "routing": {
            "default_chain": ["openai"],
            "timeout_ms": 10000,
            "retry_on": [],
            "max_retries": 1,
            "on_rate_limit": "failover_first",
        },
    }
    if blocked_tools:
        policy_kwargs["blocked_tools"] = blocked_tools
    if require_approval:
        policy_kwargs["require_approval"] = require_approval
    if budget:
        policy_kwargs["budget"] = budget

    policy_result = admin.policies.create(**policy_kwargs)

    agent_result = admin.agents.create(
        name=f"agent-{prefix}",
        policy_id=policy_result["data"]["id"],
        provider_keys={"openai": key_result["data"]["id"]},
    )

    return {
        "api_key": agent_result["data"]["api_key"],
        "agent_id": agent_result["data"]["id"],
        "policy_id": policy_result["data"]["id"],
        "provider_key_id": key_result["data"]["id"],
    }


# ─── Basic Proxy ─────────────────────────────────────────


class TestBasicProxy:
    def test_non_streaming_completion(self, agent_client: Rungate, mock_provider: MockOpenAIProvider) -> None:
        mock_provider.set_response(content="Hello from integration test!")

        response = agent_client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
        )

        assert response.model == "gpt-4o"
        assert response.choices[0]["message"]["content"] == "Hello from integration test!"
        assert response.rungate.run_id is not None
        assert response.rungate.cost_usd is not None
        assert response.rungate.provider == "openai"
        mock_provider.reset()

    def test_streaming_completion(self, agent_client: Rungate, mock_provider: MockOpenAIProvider) -> None:
        mock_provider.set_streaming_response(chunks=["Hello", " stream", "!"])

        with agent_client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
            stream=True,
        ) as stream:
            chunks = list(stream)

        assert len(chunks) >= 3
        assert stream.rungate.run_id is not None
        assert stream.rungate.cost_usd is not None
        mock_provider.reset()

    def test_high_level_stream(self, agent_client: Rungate, mock_provider: MockOpenAIProvider) -> None:
        mock_provider.set_streaming_response(chunks=["High", " level", " stream"])

        with agent_client.chat.completions.stream(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
        ) as stream:
            texts = list(stream.text_stream)

        assert texts == ["High", " level", " stream"]
        assert stream.rungate.cost_usd is not None
        mock_provider.reset()


# ─── Run Lifecycle ───────────────────────────────────────


class TestRunLifecycle:
    def test_run_context_manager_completes(
        self, agent_client: Rungate, mock_provider: MockOpenAIProvider, admin: Admin
    ) -> None:
        mock_provider.set_response(content="Run test")

        with agent_client.run(name="integration-run") as run:
            response = run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
            )
            assert response.rungate.run_id is not None
            run_id = run.id

        # Verify run was completed on the server
        server_run = admin.runs.get(run_id)
        assert server_run["data"]["status"] == "completed"
        mock_provider.reset()

    def test_run_budget_tracking(self, agent_client: Rungate, mock_provider: MockOpenAIProvider) -> None:
        mock_provider.set_response(content="Budget test")

        with agent_client.run(name="budget-run") as run:
            run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
            )
            assert run.budget.total_cost > 0
            assert run.budget.total_tokens > 0
        mock_provider.reset()

    def test_run_auto_fail_on_exception(
        self, agent_client: Rungate, mock_provider: MockOpenAIProvider, admin: Admin
    ) -> None:
        mock_provider.set_response(content="Fail test")

        run_id = None
        with pytest.raises(ValueError, match="intentional"):
            with agent_client.run(name="fail-run") as run:
                run.chat.completions.create(
                    model="gpt-4o",
                    messages=[{"role": "user", "content": "Hi"}],
                )
                run_id = run.id
                raise ValueError("intentional error")

        if run_id:
            server_run = admin.runs.get(run_id)
            assert server_run["data"]["status"] == "completed"
            assert "exception" in server_run["data"]["ended_reason"]
        mock_provider.reset()

    def test_tool_call_logging(self, agent_client: Rungate, mock_provider: MockOpenAIProvider, admin: Admin) -> None:
        mock_provider.set_response(content="Tool test")

        with agent_client.run(name="tool-run") as run:
            run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
            )

            run.log_tool_call(
                name="web_search",
                request={"query": "integration test"},
                response={"results": ["result1"]},
                latency_ms=100,
            )

            run_id = run.id

        # Verify steps include the tool call
        steps = admin.runs.steps(run_id)
        tool_steps = [s for s in steps["data"] if s["type"] == "tool_call"]
        assert len(tool_steps) == 1
        assert tool_steps[0]["model"] == "web_search"
        mock_provider.reset()

    def test_multiple_calls_in_run(
        self, agent_client: Rungate, mock_provider: MockOpenAIProvider, admin: Admin
    ) -> None:
        """Multiple LLM calls in one run — cost accumulates, step count increments."""
        mock_provider.set_response(content="Call 1")

        with agent_client.run(name="multi-call-run") as run:
            run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "First"}],
            )
            cost_after_first = run.budget.total_cost
            tokens_after_first = run.budget.total_tokens
            assert cost_after_first > 0

            mock_provider.set_response(content="Call 2")
            run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Second"}],
            )
            assert run.budget.total_cost > cost_after_first
            assert run.budget.total_tokens > tokens_after_first

            run_id = run.id

        # Verify server sees multiple steps
        steps = admin.runs.steps(run_id)
        llm_steps = [s for s in steps["data"] if s["type"] == "llm_call"]
        assert len(llm_steps) == 2
        mock_provider.reset()


# ─── Governance Enforcement ──────────────────────────────


class TestGovernance:
    def test_model_not_allowed(self, agent_client: Rungate, mock_provider: MockOpenAIProvider) -> None:
        """Policy only allows gpt-4o; requesting a different model should fail."""
        mock_provider.set_response()

        with pytest.raises(ModelNotAllowedError):
            agent_client.chat.completions.create(
                model="gpt-3.5-turbo",  # Not in allowed_models
                messages=[{"role": "user", "content": "Hi"}],
            )
        mock_provider.reset()

    def test_tool_blocked(
        self,
        rungate_server: RunningServer,
        mock_provider: MockOpenAIProvider,
    ) -> None:
        """Create agent with blocked tool, verify ToolBlockedError."""
        admin = Admin(token="integration-test-password", base_url=rungate_server.url)

        setup = _create_agent(admin, blocked_tools=["dangerous_tool"])
        client = Rungate(api_key=setup["api_key"], base_url=rungate_server.url)
        mock_provider.set_response()

        with pytest.raises(ToolBlockedError):
            client.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
                tools=[{"type": "function", "function": {"name": "dangerous_tool", "description": "Bad tool"}}],
            )

        client.close()
        admin.close()
        mock_provider.reset()

    def test_disabled_agent(
        self,
        rungate_server: RunningServer,
        mock_provider: MockOpenAIProvider,
    ) -> None:
        """Disabled agent should raise AgentDisabledError."""
        admin = Admin(token="integration-test-password", base_url=rungate_server.url)

        setup = _create_agent(admin)
        client = Rungate(api_key=setup["api_key"], base_url=rungate_server.url)

        # Disable the agent
        admin.agents.update(setup["agent_id"], status="disabled")
        mock_provider.set_response()

        with pytest.raises(AgentDisabledError):
            client.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
            )

        client.close()
        admin.close()
        mock_provider.reset()

    def test_approval_required_flow(
        self,
        rungate_server: RunningServer,
        mock_provider: MockOpenAIProvider,
    ) -> None:
        """Tool requiring approval → 202 → approve via admin → retry → success."""
        admin = Admin(token="integration-test-password", base_url=rungate_server.url)

        setup = _create_agent(admin, require_approval=["execute_code"])
        client = Rungate(api_key=setup["api_key"], base_url=rungate_server.url)
        mock_provider.set_response(content="Approved result")

        # First request — should get approval required
        with pytest.raises(ApprovalRequiredError) as exc_info:
            client.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Run code"}],
                tools=[{"type": "function", "function": {"name": "execute_code", "description": "Execute code"}}],
            )

        # Verify approval requests are returned
        assert len(exc_info.value.approval_requests) > 0
        approval_id = exc_info.value.approval_requests[0]["id"]

        # Approve via admin
        approved = admin.approvals.approve(approval_id)
        assert approved["data"]["status"] == "approved"

        # Retry — should succeed now
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Run code"}],
            tools=[{"type": "function", "function": {"name": "execute_code", "description": "Execute code"}}],
        )
        assert response.choices[0]["message"]["content"] == "Approved result"

        client.close()
        admin.close()
        mock_provider.reset()


# ─── Admin Client ────────────────────────────────────────


class TestAdminIntegration:
    def test_policy_crud_lifecycle(self, admin: Admin) -> None:
        """Create, list, get, delete a policy through the real server."""
        name = f"integration-policy-{uuid.uuid4().hex[:8]}"

        # Create
        created = admin.policies.create(name=name, allowed_models=["gpt-4o"])
        policy_id = created["data"]["id"]
        assert created["data"]["name"] == name

        # List
        listed = admin.policies.list()
        names = [p["name"] for p in listed["data"]]
        assert name in names

        # Get
        fetched = admin.policies.get(policy_id)
        assert fetched["data"]["name"] == name

        # Delete
        admin.policies.delete(policy_id)
        listed_after = admin.policies.list()
        names_after = [p["name"] for p in listed_after["data"]]
        assert name not in names_after

    def test_provider_key_crud_lifecycle(self, admin: Admin) -> None:
        """Create, list, get, revoke a provider key."""
        name = f"integration-key-{uuid.uuid4().hex[:8]}"

        created = admin.provider_keys.create(
            provider="openai",
            name=name,
            api_key="sk-test-key-for-crud",
            base_url="http://127.0.0.1:9999",
        )
        key_id = created["data"]["id"]
        assert created["data"]["name"] == name
        assert created["data"]["provider"] == "openai"
        assert created["data"]["status"] == "active"

        # List
        listed = admin.provider_keys.list()
        names = [k["name"] for k in listed["data"]]
        assert name in names

        # Get
        fetched = admin.provider_keys.get(key_id)
        assert fetched["data"]["name"] == name

        # Delete (soft-delete: sets status to 'revoked')
        admin.provider_keys.delete(key_id)
        fetched_after = admin.provider_keys.get(key_id)
        assert fetched_after["data"]["status"] == "revoked"

    def test_agent_crud_lifecycle(self, admin: Admin) -> None:
        """Create, list, get, update, disable an agent."""
        prefix = uuid.uuid4().hex[:8]

        # Need a policy first
        policy = admin.policies.create(name=f"agent-test-policy-{prefix}", allowed_models=["gpt-4o"])
        policy_id = policy["data"]["id"]

        # Create agent
        created = admin.agents.create(name=f"agent-test-{prefix}", policy_id=policy_id)
        agent_id = created["data"]["id"]
        assert created["data"]["name"] == f"agent-test-{prefix}"
        assert created["data"]["status"] == "active"

        # List
        listed = admin.agents.list()
        names = [a["name"] for a in listed["data"]]
        assert f"agent-test-{prefix}" in names

        # Get
        fetched = admin.agents.get(agent_id)
        assert fetched["data"]["name"] == f"agent-test-{prefix}"

        # Disable (soft-delete for agents)
        updated = admin.agents.update(agent_id, status="disabled")
        assert updated["data"]["status"] == "disabled"

    def test_alert_rule_lifecycle(self, admin: Admin) -> None:
        """Create and manage an alert rule."""
        name = f"integration-alert-{uuid.uuid4().hex[:8]}"

        created = admin.alert_rules.create(
            name=name,
            condition_type="cost_threshold",
            condition_config={"threshold_usd": 100, "window_seconds": 3600},
        )
        rule_id = created["data"]["id"]
        assert created["data"]["status"] == "active"

        # Disable
        disabled = admin.alert_rules.disable(rule_id)
        assert disabled["data"]["status"] == "disabled"

        # Enable
        enabled = admin.alert_rules.enable(rule_id)
        assert enabled["data"]["status"] == "active"

        # Cleanup
        admin.alert_rules.delete(rule_id)

    def test_runs_list_and_get(self, agent_client: Rungate, mock_provider: MockOpenAIProvider, admin: Admin) -> None:
        """Runs created by proxy calls appear in admin runs list."""
        mock_provider.set_response(content="Runs test")

        response = agent_client.chat.completions.create(
            model="gpt-4o",
            messages=[{"role": "user", "content": "Hi"}],
        )
        run_id = response.rungate.run_id
        assert run_id is not None

        # Verify via admin
        server_run = admin.runs.get(run_id)
        assert server_run["data"]["id"] == run_id

        # Verify appears in list
        runs = admin.runs.list()
        run_ids = [r["id"] for r in runs["data"]]
        assert run_id in run_ids
        mock_provider.reset()


# ─── Error Handling ──────────────────────────────────────


class TestErrors:
    def test_invalid_api_key(self, rungate_server: RunningServer) -> None:
        client = Rungate(api_key="rg_sk_invalid_key", base_url=rungate_server.url)
        with pytest.raises(AuthenticationError):
            client.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Hi"}],
            )
        client.close()

    def test_admin_invalid_token(self, rungate_server: RunningServer) -> None:
        admin = Admin(token="wrong-token", base_url=rungate_server.url)
        with pytest.raises(AuthenticationError):
            admin.agents.list()
        admin.close()

    def test_admin_not_found(self, admin: Admin) -> None:
        """Get a non-existent resource returns NotFoundError."""
        with pytest.raises(NotFoundError):
            admin.policies.get("non-existent-policy-id-12345")


# ─── Cross-Feature ───────────────────────────────────────


class TestCrossFeature:
    def test_full_lifecycle(
        self,
        rungate_server: RunningServer,
        mock_provider: MockOpenAIProvider,
    ) -> None:
        """Full lifecycle: create agent → run with budget → tool call → complete."""
        admin = Admin(token="integration-test-password", base_url=rungate_server.url)

        setup = _create_agent(admin, budget={"hard_limit_usd": 1.0, "hard_limit_tokens": 100000})
        client = Rungate(api_key=setup["api_key"], base_url=rungate_server.url)

        mock_provider.set_response(content="Lifecycle step 1")

        with client.run(name="lifecycle-run") as run:
            # First LLM call
            r1 = run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Step 1"}],
            )
            assert r1.choices[0]["message"]["content"] == "Lifecycle step 1"
            assert run.budget.total_cost > 0

            # Log a tool call
            run.log_tool_call(
                name="file_read",
                request={"path": "/tmp/test"},
                response={"content": "file contents"},
                latency_ms=50,
            )

            # Second LLM call
            mock_provider.set_response(content="Lifecycle step 2")
            r2 = run.chat.completions.create(
                model="gpt-4o",
                messages=[{"role": "user", "content": "Step 2"}],
            )
            assert r2.choices[0]["message"]["content"] == "Lifecycle step 2"
            assert run.budget.total_cost > 0

            run_id = run.id

        # Verify server state
        server_run = admin.runs.get(run_id)
        assert server_run["data"]["status"] == "completed"

        steps = admin.runs.steps(run_id)
        assert len(steps["data"]) >= 3  # 2 LLM calls + 1 tool call

        client.close()
        admin.close()
        mock_provider.reset()
