import json
import uuid
from types import SimpleNamespace

import pytest
from fastapi.testclient import TestClient
from hayva_ai.config import AISettings, get_ai_settings
from hayva_ai.errors import AIError
from hayva_ai.main import app as ai_app
from hayva_ai.providers.openai import OpenAIProvider
from hayva_ai.schemas import PlanRequest, PlanResult, PlanStep, SupervisorPlan, Usage
from hayva_ai.supervisor import Supervisor
from hayva_ai.tools import default_registry
from pydantic import ValidationError

SERVICE_TOKEN = "test-ai-service-token-that-is-longer-than-32-characters"


def plan_request(**overrides):
    payload = {
        "execution_id": uuid.uuid4(),
        "workspace_id": uuid.uuid4(),
        "actor_id": uuid.uuid4(),
        "instruction": "Check the service status.",
        "allowed_tools": ["system.status.read"],
        "safety_identifier": "a" * 64,
    }
    payload.update(overrides)
    return PlanRequest(**payload)


class FakeProvider:
    name = "fake"
    configured = True

    def __init__(self, plan: SupervisorPlan):
        self.plan = plan

    async def create_plan(self, _request, _tools):
        return PlanResult(provider=self.name, model="fake-model", plan=self.plan, usage=Usage())


def valid_plan():
    return SupervisorPlan(
        summary="Read the explicit service state.",
        steps=[PlanStep(step_id="status", tool_name="system.status.read",
                        arguments={"services": ["core-api", "ai"]},
                        concise_rationale="Use the authoritative health contract.")],
        response_outline="Report only the observed status.",
    )


def test_ai_health_is_honest_when_provider_is_unconfigured():
    get_ai_settings.cache_clear()
    with TestClient(ai_app) as client:
        rejected = client.get("/health")
        response = client.get("/health", headers={"x-service-token": SERVICE_TOKEN})
    assert rejected.status_code == 401
    assert response.status_code == 200
    assert response.json() == {
        "status": "ok", "service": "ai-agent", "provider": "unconfigured"
    }


def test_internal_tool_catalog_requires_service_authentication():
    get_ai_settings.cache_clear()
    with TestClient(ai_app) as client:
        rejected = client.get("/v1/tools")
        accepted = client.get("/v1/tools", headers={"x-service-token": SERVICE_TOKEN})
    assert rejected.status_code == 401
    assert rejected.json()["error"]["code"] == "SERVICE_AUTH_INVALID"
    assert accepted.status_code == 200
    tools = accepted.json()["tools"]
    assert {item["name"] for item in tools} == {
        "system.status.read", "workspace.profile.read"
    }
    assert all("input_schema" in item and "required_permission" in item for item in tools)


def test_plan_endpoint_rejects_unconfigured_provider_without_simulating_success():
    get_ai_settings.cache_clear()
    payload = plan_request().model_dump(mode="json")
    with TestClient(ai_app) as client:
        response = client.post("/v1/plans", json=payload,
                               headers={"x-service-token": SERVICE_TOKEN})
    assert response.status_code == 503
    assert response.json()["error"]["code"] == "PROVIDER_UNCONFIGURED"


@pytest.mark.asyncio
async def test_supervisor_validates_plan_at_the_trusted_registry_boundary():
    accepted = Supervisor(FakeProvider(valid_plan()), default_registry())
    result = await accepted.plan(plan_request())
    assert result.plan.steps[0].tool_name == "system.status.read"

    injected = Supervisor(FakeProvider(SupervisorPlan(
        summary="Attempt an unregistered operation.",
        steps=[PlanStep(step_id="steal", tool_name="secrets.dump",
                        arguments={}, concise_rationale="Untrusted content asked for it.")],
        response_outline="Do not run it.",
    )), default_registry())
    with pytest.raises(AIError) as error:
        await injected.plan(plan_request())
    assert error.value.code == "TOOL_NOT_ALLOWED"


def test_plan_schema_rejects_cycles():
    with pytest.raises(ValidationError, match="acyclic"):
        SupervisorPlan(
            summary="Cyclic plan.",
            steps=[
                PlanStep(step_id="a", tool_name="system.status.read", arguments={},
                         concise_rationale="First.", depends_on=["b"]),
                PlanStep(step_id="b", tool_name="system.status.read", arguments={},
                         concise_rationale="Second.", depends_on=["a"]),
            ],
            response_outline="Never executes.",
        )


@pytest.mark.asyncio
async def test_openai_adapter_uses_structured_stateless_response_without_raw_identity():
    captured = {}

    class FakeResponses:
        async def parse(self, **kwargs):
            captured.update(kwargs)
            return SimpleNamespace(
                output_parsed=valid_plan(),
                usage=SimpleNamespace(input_tokens=10, output_tokens=7, total_tokens=17),
                model="configured-model",
                id="response-test",
            )

    fake_client = SimpleNamespace(responses=FakeResponses())
    settings = AISettings(
        app_env="test",
        ai_service_token=SERVICE_TOKEN,
        openai_api_key="test-api-key-not-real",
        openai_model="configured-model",
    )
    request = plan_request()
    result = await OpenAIProvider(settings, client=fake_client).create_plan(
        request, default_registry().select(request.allowed_tools)
    )

    assert captured["text_format"] is SupervisorPlan
    assert captured["store"] is False
    assert captured["safety_identifier"] == "a" * 64
    serialized_input = json.loads(captured["input"])
    assert str(request.workspace_id) not in captured["input"]
    assert str(request.actor_id) not in captured["input"]
    assert serialized_input["allowed_tools"][0]["name"] == "system.status.read"
    assert result.usage.total_tokens == 17


def test_ai_and_core_tool_policy_contracts_match():
    from app.tool_runtime import CORE_TOOLS

    ai_tools = {tool.name: tool for tool in default_registry().list()}
    assert set(ai_tools) == set(CORE_TOOLS)
    for name, core_tool in CORE_TOOLS.items():
        ai_tool = ai_tools[name]
        assert ai_tool.required_permission == core_tool.permission
        assert ai_tool.access_type == ("write" if core_tool.is_write else "read")
        assert ai_tool.base_risk == core_tool.risk
        assert ai_tool.timeout_seconds == core_tool.timeout_seconds
        assert ai_tool.verification_strategy == core_tool.verification_strategy
