"""Tests for Agent.from_config YAML loading."""
import os
from pathlib import Path
import pytest
from logos_adk.agent import Agent
from logos_adk.errors import ConfigError, ToolNotFoundError
from logos_adk.memory import LongTermMemory, SessionMemory, SharedMemory
from logos_adk.memory.backends import InMemoryBackend, SQLiteBackend
from logos_adk.observe import ConsoleExporter, InMemoryExporter, OpenTelemetryExporter
from logos_adk.guardrails import ContentModerationGuard, HallucinationGuard, ToxicityGuard
from logos_adk.tools import tool


FIXTURE_DIR = os.path.join(os.path.dirname(__file__), "fixtures")


def _register_yaml_tools():
    """Register test tools in the registry for YAML loading tests."""

    @tool
    async def yaml_search(query: str) -> str:
        """Search for YAML test."""
        return query

    @tool
    async def yaml_ticket(title: str) -> dict:
        """Create ticket for YAML test."""
        return {"title": title}


def test_from_config_basic():
    _register_yaml_tools()
    path = os.path.join(FIXTURE_DIR, "test_agent.yaml")
    agent = Agent.from_config(path)
    assert agent.name == "test_bot"
    assert agent._config.model == "claude-sonnet"
    assert agent._config.system_prompt == "You are a test bot"


def test_from_config_loads_provider_proxy(tmp_path):
    config_path = tmp_path / "proxy_agent.yaml"
    config_path.write_text(
        "\n".join(
            [
                "name: proxy_bot",
                "provider:",
                "  type: openai-compatible",
                "  model: local-model",
                "  base_url: http://localhost:1234/v1",
                "  proxy: http://proxy.internal:8080",
            ]
        )
    )

    agent = Agent.from_config(str(config_path))

    assert agent._config.provider is not None
    assert agent._config.provider.proxy == "http://proxy.internal:8080"


def test_from_config_loads_tools():
    _register_yaml_tools()
    path = os.path.join(FIXTURE_DIR, "test_agent.yaml")
    agent = Agent.from_config(path)
    tool_names = [t.name for t in agent._config.tools]
    assert "yaml_search" in tool_names
    assert "yaml_ticket" in tool_names


def test_from_config_unknown_tool():
    path = os.path.join(FIXTURE_DIR, "bad_tools_agent.yaml")
    with pytest.raises(ToolNotFoundError, match="nonexistent_tool"):
        Agent.from_config(path)


def test_from_config_memory_session_shorthand():
    path = os.path.join(FIXTURE_DIR, "memory_session_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.memories) == 1
    assert isinstance(agent._config.memories[0], SessionMemory)


def test_from_config_memory_structured():
    path = os.path.join(FIXTURE_DIR, "memory_structured_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.memories) == 2
    assert isinstance(agent._config.memories[0], SessionMemory)
    assert isinstance(agent._config.memories[1], LongTermMemory)
    assert agent._config.memories[0].strategy == "summarize"
    assert agent._config.memories[0].max_messages == 6
    assert agent._config.memories[1].retrieval == "hybrid"
    assert agent._config.memories[1].limit == 3


def test_from_config_memory_sqlite_backend(tmp_path):
    config_path = tmp_path / "sqlite_agent.yaml"
    db_path = tmp_path / "memory.db"
    config_path.write_text(
        "\n".join(
            [
                "name: memory_bot",
                "model: claude-sonnet",
                "memory:",
                "  long_term:",
                "    backend: sqlite",
                f"    path: {db_path}",
                "    retrieval: keyword",
            ]
        )
    )

    agent = Agent.from_config(str(config_path))

    assert len(agent._config.memories) == 1
    assert isinstance(agent._config.memories[0], LongTermMemory)
    assert isinstance(agent._config.memories[0].backend, SQLiteBackend)
    assert Path(agent._config.memories[0].backend.path) == db_path


def test_from_config_memory_shared_backend():
    path = os.path.join(FIXTURE_DIR, "memory_shared_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.memories) == 1
    assert isinstance(agent._config.memories[0], SharedMemory)
    assert isinstance(agent._config.memories[0].backend, InMemoryBackend)
    assert agent._config.memories[0].limit == 4


def test_from_config_unknown_memory_backend():
    path = os.path.join(FIXTURE_DIR, "bad_memory_agent.yaml")

    with pytest.raises(ConfigError, match="Unsupported memory backend"):
        Agent.from_config(path)


def test_from_config_sqlite_requires_path(tmp_path):
    config_path = tmp_path / "bad_sqlite_agent.yaml"
    config_path.write_text(
        "\n".join(
            [
                "name: memory_bot",
                "model: claude-sonnet",
                "memory:",
                "  long_term:",
                "    backend: sqlite",
            ]
        )
    )

    agent = Agent.from_config(str(config_path))
    assert isinstance(agent._config.memories[0].backend, SQLiteBackend)
    assert agent._config.memories[0].backend.path.endswith("long_term_memory.db")


def test_from_config_memory_requires_backend_for_long_term():
    path = os.path.join(FIXTURE_DIR, "bad_memory_missing_backend_agent.yaml")
    agent = Agent.from_config(path)
    assert isinstance(agent._config.memories[0].backend, SQLiteBackend)


def test_from_config_unknown_memory_section():
    path = os.path.join(FIXTURE_DIR, "bad_memory_unknown_section_agent.yaml")

    with pytest.raises(ConfigError, match="Unknown memory config sections"):
        Agent.from_config(path)


def test_from_config_rejects_unknown_session_strategy():
    path = os.path.join(FIXTURE_DIR, "bad_memory_strategy_agent.yaml")

    with pytest.raises(ConfigError, match="Unsupported session memory strategy"):
        Agent.from_config(path)


def test_from_config_token_limit_requires_max_tokens():
    path = os.path.join(FIXTURE_DIR, "bad_memory_token_limit_agent.yaml")

    with pytest.raises(ConfigError, match="requires max_tokens"):
        Agent.from_config(path)


def test_from_config_rejects_bad_session_param_type():
    path = os.path.join(FIXTURE_DIR, "bad_memory_max_messages_type_agent.yaml")

    with pytest.raises(ConfigError, match="max_messages must be an integer"):
        Agent.from_config(path)


def test_from_config_rejects_bad_long_term_retrieval():
    path = os.path.join(FIXTURE_DIR, "bad_memory_retrieval_agent.yaml")

    with pytest.raises(ConfigError, match="Unsupported long-term memory retrieval"):
        Agent.from_config(path)


def test_from_config_observe_console_shorthand():
    path = os.path.join(FIXTURE_DIR, "observe_console_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], ConsoleExporter)


def test_from_config_observe_console_structured():
    path = os.path.join(FIXTURE_DIR, "observe_console_structured_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], ConsoleExporter)


def test_from_config_observe_memory_shorthand():
    path = os.path.join(FIXTURE_DIR, "observe_memory_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], InMemoryExporter)


def test_from_config_observe_memory_structured():
    path = os.path.join(FIXTURE_DIR, "observe_memory_structured_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], InMemoryExporter)


def test_from_config_observe_otel_structured():
    path = os.path.join(FIXTURE_DIR, "observe_otel_agent.yaml")
    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], OpenTelemetryExporter)
    assert agent._config.observers[0].endpoint == "http://collector.test/v1/traces"
    assert agent._config.observers[0].timeout == 7.5
    assert agent._config.observers[0].batch_size == 25
    assert agent._config.observers[0].max_retries == 4
    assert agent._config.observers[0].backoff_seconds == 0.75
    assert agent._config.observers[0].bearer_token == "secret-token"


def test_from_config_loads_guardrails_and_scaling(tmp_path):
    config_path = tmp_path / "guarded_agent.yaml"
    config_path.write_text(
        "\n".join(
            [
                "name: guarded_bot",
                "model: claude-sonnet",
                "guardrails:",
                "  input:",
                "    - type: moderation",
                "      blocked_categories: [violence]",
                "    - type: toxicity",
                "      threshold: 1",
                "  output:",
                "    - type: hallucination",
                "      required_facts: [PostgreSQL]",
                "scaling:",
                "  distributed: true",
                "  session_affinity: true",
            ]
        )
    )

    agent = Agent.from_config(str(config_path))

    assert any(isinstance(guard, ContentModerationGuard) for guard in agent._config.input_guardrails)
    assert any(isinstance(guard, ToxicityGuard) for guard in agent._config.input_guardrails)
    assert any(isinstance(guard, HallucinationGuard) for guard in agent._config.output_guardrails)
    assert agent._config.scaling.session_affinity is True


def test_from_config_observe_otel_env_resolution(monkeypatch):
    monkeypatch.setenv("OTEL_BEARER_TOKEN", "env-secret-token")
    monkeypatch.setenv("OTEL_TENANT_ID", "tenant-from-env")
    monkeypatch.setenv("OTEL_API_KEY", "api-key-from-env")
    path = os.path.join(FIXTURE_DIR, "observe_otel_env_agent.yaml")

    agent = Agent.from_config(path)

    assert len(agent._config.observers) == 1
    assert isinstance(agent._config.observers[0], OpenTelemetryExporter)
    assert agent._config.observers[0].bearer_token == "env-secret-token"
    assert agent._config.observers[0].headers == {
        "x-tenant-id": "tenant-from-env",
        "x-api-key": "api-key-from-env",
    }


def test_from_config_rejects_unsupported_observe_exporter():
    path = os.path.join(FIXTURE_DIR, "bad_observe_unknown_exporter_agent.yaml")

    with pytest.raises(ConfigError, match="Unsupported observe exporter"):
        Agent.from_config(path)


def test_from_config_rejects_observe_without_exporter():
    path = os.path.join(FIXTURE_DIR, "bad_observe_missing_exporter_agent.yaml")

    with pytest.raises(ConfigError, match="requires an 'exporter' field"):
        Agent.from_config(path)


def test_from_config_rejects_otel_without_endpoint():
    path = os.path.join(FIXTURE_DIR, "bad_observe_agent.yaml")

    with pytest.raises(ConfigError, match="requires a non-empty endpoint"):
        Agent.from_config(path)


def test_from_config_rejects_invalid_otel_batch_size():
    path = os.path.join(FIXTURE_DIR, "bad_observe_otel_batch_size_agent.yaml")

    with pytest.raises(ConfigError, match="batch_size must be a positive integer"):
        Agent.from_config(path)


def test_from_config_rejects_invalid_otel_headers():
    path = os.path.join(FIXTURE_DIR, "bad_observe_otel_headers_agent.yaml")

    with pytest.raises(ConfigError, match="headers must be a string-to-string mapping"):
        Agent.from_config(path)


def test_from_config_rejects_invalid_otel_compression():
    path = os.path.join(FIXTURE_DIR, "bad_observe_otel_compression_agent.yaml")

    with pytest.raises(ConfigError, match="compression must be 'none' or 'gzip'"):
        Agent.from_config(path)


def test_from_config_rejects_missing_otel_env(monkeypatch):
    monkeypatch.delenv("OTEL_MISSING_TOKEN", raising=False)
    path = os.path.join(FIXTURE_DIR, "bad_observe_otel_missing_env_agent.yaml")

    with pytest.raises(ConfigError, match="references environment variable 'OTEL_MISSING_TOKEN'"):
        Agent.from_config(path)
