"""Direct tests for top-level agent config parsing."""
from __future__ import annotations

import pytest

from logos_adk.config import AgentConfig
from logos_adk.config_parser import parse_agent_config_data
from logos_adk.errors import ConfigError
from logos_adk.memory import SessionMemory
from logos_adk.memory.backends import ShardedMemoryBackend, SQLiteBackend
from logos_adk.observe import ConsoleExporter
from logos_adk.guardrails import ContentModerationGuard, HallucinationGuard, ToxicityGuard
from logos_adk.tools import tool
from logos_adk.tools.registry import tool_registry


@pytest.fixture(autouse=True)
def _clear_tool_registry():
    tool_registry.clear()
    yield
    tool_registry.clear()


def test_parse_agent_config_data_builds_typed_config():
    @tool
    async def parser_search(query: str) -> str:
        return query

    config = parse_agent_config_data(
        {
            "name": "parser_bot",
            "model": "claude-sonnet",
            "system_prompt": "parsed",
            "tools": ["parser_search"],
            "memory": "session",
            "observe": "console",
        }
    )

    assert isinstance(config, AgentConfig)
    assert config.name == "parser_bot"
    assert config.model == "claude-sonnet"
    assert config.system_prompt == "parsed"
    assert [tool.name for tool in config.tools] == ["parser_search"]
    assert isinstance(config.memories[0], SessionMemory)
    assert isinstance(config.observers[0], ConsoleExporter)


def test_parse_agent_config_data_accepts_crewai_style_role_fields():
    config = parse_agent_config_data(
        {
            "name": "researcher",
            "model": "mock",
            "role": "Senior Researcher",
            "goal": "Find the strongest evidence",
            "backstory": "Former analyst focused on synthesis",
        }
    )

    assert config.role == "Senior Researcher"
    assert config.goal == "Find the strongest evidence"
    assert config.backstory == "Former analyst focused on synthesis"


def test_parse_agent_config_data_accepts_custom_agent_loader_fields():
    config = parse_agent_config_data(
        {
            "name": "custom_bot",
            "agent_class": "tests.test_config_loader.ImportPathAgent",
            "agent_module": "tests.test_config_loader",
            "model": "mock",
        }
    )

    assert config.agent_class == "tests.test_config_loader.ImportPathAgent"
    assert config.agent_module == "tests.test_config_loader"


def test_parse_agent_config_data_accepts_agent_factory_field():
    config = parse_agent_config_data(
        {
            "name": "factory_bot",
            "agent_factory": "tests.test_config_loader.import_path_agent_factory",
            "model": "mock",
        }
    )

    assert config.agent_factory == "tests.test_config_loader.import_path_agent_factory"


def test_parse_agent_config_data_rejects_unknown_top_level_fields():
    with pytest.raises(ConfigError, match="Unknown agent config fields"):
        parse_agent_config_data({"name": "parser_bot", "foo": "bar"})


def test_parse_agent_config_data_rejects_missing_or_invalid_name():
    with pytest.raises(ConfigError, match="requires a non-empty string 'name'"):
        parse_agent_config_data({})

    with pytest.raises(ConfigError, match="requires a non-empty string 'name'"):
        parse_agent_config_data({"name": ""})


def test_parse_agent_config_data_rejects_invalid_scalar_types():
    with pytest.raises(ConfigError, match="field 'model' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "model": 123})

    with pytest.raises(ConfigError, match="field 'system_prompt' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "system_prompt": 123})
    with pytest.raises(ConfigError, match="field 'role' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "role": 123})
    with pytest.raises(ConfigError, match="field 'agent_class' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "agent_class": 123})
    with pytest.raises(ConfigError, match="field 'agent_module' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "agent_module": 123})
    with pytest.raises(ConfigError, match="field 'agent_factory' must be a string"):
        parse_agent_config_data({"name": "parser_bot", "agent_factory": 123})


def test_parse_agent_config_data_rejects_class_and_factory_together():
    with pytest.raises(ConfigError, match="may not set both 'agent_class' and 'agent_factory'"):
        parse_agent_config_data(
            {
                "name": "parser_bot",
                "agent_class": "x",
                "agent_factory": "y",
            }
        )


def test_parse_agent_config_data_builds_scaling_config():
    config = parse_agent_config_data(
        {
            "name": "scaled_bot",
            "model": "mock",
            "scaling": {
                "distributed": True,
                "replicas": 3,
                "load_balancer": "least_loaded",
                "task_queue": {
                    "backend": "postgresql",
                    "dsn": "postgresql://queue-db/logos",
                    "path": "var/tasks.db",
                    "concurrency": 8,
                    "max_retries": 3,
                    "idempotency_ttl_seconds": 7200,
                },
            },
        }
    )

    assert config.scaling.distributed is True
    assert config.scaling.replicas == 3
    assert config.scaling.load_balancer == "least_loaded"
    assert config.scaling.task_queue.path == "var/tasks.db"
    assert config.scaling.task_queue.concurrency == 8
    assert config.scaling.task_queue.max_retries == 3
    assert config.scaling.task_queue.idempotency_ttl_seconds == 7200
    assert config.scaling.task_queue.dsn == "postgresql://queue-db/logos"


def test_parse_agent_config_data_defaults_task_queue_to_postgresql():
    config = parse_agent_config_data(
        {
            "name": "scaled_bot",
            "model": "mock",
            "scaling": {},
        }
    )

    assert config.scaling.task_queue.backend == "postgresql"


def test_parse_agent_config_defaults_memory_to_persistent_sqlite():
    config = parse_agent_config_data(
        {
            "name": "memory_bot",
            "model": "mock",
            "memory": {
                "long_term": {},
            },
        }
    )

    assert isinstance(config.memories[0].backend, SQLiteBackend)
    assert config.memories[0].backend.path.endswith("long_term_memory.db")


def test_parse_agent_config_builds_sharded_memory_backend():
    config = parse_agent_config_data(
        {
            "name": "memory_bot",
            "model": "mock",
            "memory": {
                "shared": {
                    "backend": "sharded",
                    "shards": [
                        {"backend": "sqlite", "path": "data/shared-a.db"},
                        {"backend": "sqlite", "path": "data/shared-b.db"},
                    ],
                },
            },
        }
    )

    assert isinstance(config.memories[0].backend, ShardedMemoryBackend)


def test_parse_agent_config_builds_guardrails_and_session_affinity():
    config = parse_agent_config_data(
        {
            "name": "guarded_bot",
            "model": "mock",
            "guardrails": {
                "input": [
                    {"type": "moderation", "blocked_categories": ["violence"]},
                    {"type": "toxicity", "threshold": 1},
                ],
                "output": [
                    {"type": "hallucination", "required_facts": ["PostgreSQL"]},
                ],
            },
            "scaling": {
                "distributed": True,
                "session_affinity": True,
            },
        }
    )

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


def test_parse_agent_config_builds_security_and_reliability_configs():
    config = parse_agent_config_data(
        {
            "name": "secure_bot",
            "model": "mock",
            "security": {
                "input_max_length": 4096,
                "output_max_length": 2048,
                "prompt_injection_threshold": 0.8,
                "pii_blocked_types": ["ssn"],
                "input_pii_action": "warn",
                "output_pii_action": "block",
                "audit_retention_days": 45,
                "rate_limit": {
                    "backend": "postgresql",
                    "dsn": "postgresql://localhost/logos",
                    "limit_per_minute": 10,
                    "burst": 2,
                    "window_seconds": 30,
                },
            },
            "reliability": {
                "timeout_seconds": 15,
                "retry_max_attempts": 4,
                "retry_base_delay": 0.1,
                "retry_backoff": "fixed",
                "circuit_breaker_failure_threshold": 2,
                "circuit_breaker_recovery_timeout": 5,
                "fallback_models": ["mock-fallback"],
            },
        }
    )

    assert config.security.input_max_length == 4096
    assert config.security.output_max_length == 2048
    assert config.security.prompt_injection_threshold == 0.8
    assert config.security.pii_blocked_types == ["ssn"]
    assert config.security.input_pii_action == "warn"
    assert config.security.output_pii_action == "block"
    assert config.security.audit_retention_days == 45
    assert config.security.rate_limit.backend == "postgresql"
    assert config.security.rate_limit.limit_per_minute == 10
    assert config.reliability.timeout_seconds == 15
    assert config.reliability.retry_backoff == "fixed"
    assert config.reliability.fallback_models == ["mock-fallback"]


def test_parse_agent_config_provider_accepts_proxy():
    config = parse_agent_config_data(
        {
            "name": "proxy_bot",
            "provider": {
                "type": "openai",
                "model": "gpt-4o-mini",
                "proxy": "http://proxy.internal:8080",
            },
        }
    )

    assert config.provider is not None
    assert config.provider.type == "openai"
    assert config.provider.proxy == "http://proxy.internal:8080"


def test_parse_agent_config_accepts_gemini_and_openai_responses_providers():
    gemini = parse_agent_config_data(
        {
            "name": "vision_bot",
            "provider": {
                "type": "gemini",
                "model": "gemini-2.0-flash",
            },
        }
    )
    responses = parse_agent_config_data(
        {
            "name": "responses_bot",
            "provider": {
                "type": "openai-responses",
                "model": "gpt-4.1",
            },
        }
    )

    assert gemini.provider is not None
    assert gemini.provider.type == "gemini"
    assert responses.provider is not None
    assert responses.provider.type == "openai-responses"


def test_parse_agent_config_builds_learning_retrieval_policy():
    config = parse_agent_config_data(
        {
            "name": "learning_bot",
            "model": "mock",
            "learning": {
                "retrieval": {
                    "dsn": "postgresql://user:pass@localhost/retrieval",
                    "path": "var/retrieval.db",
                    "table_prefix": "ops_rl",
                    "source_document_ttl_days": 14,
                    "max_source_documents_per_source": 7,
                    "dedupe_window_seconds": 120,
                }
            },
        }
    )

    assert config.learning.retrieval.dsn == "postgresql://user:pass@localhost/retrieval"
    assert config.learning.retrieval.path == "var/retrieval.db"
    assert config.learning.retrieval.table_prefix == "ops_rl"
    assert config.learning.retrieval.source_document_ttl_days == 14
    assert config.learning.retrieval.max_source_documents_per_source == 7
    assert config.learning.retrieval.dedupe_window_seconds == 120


def test_parse_agent_config_builds_self_qa_policy():
    config = parse_agent_config_data(
        {
            "name": "qa_bot",
            "model": "mock",
            "self_qa": {
                "enabled": True,
                "min_assertions": 2,
                "max_assertions": 3,
                "max_retries": 2,
                "fail_closed": False,
            },
        }
    )

    assert config.self_qa.enabled is True
    assert config.self_qa.min_assertions == 2
    assert config.self_qa.max_assertions == 3
    assert config.self_qa.max_retries == 2
    assert config.self_qa.fail_closed is False


def test_parse_agent_config_rejects_invalid_self_qa_policy():
    with pytest.raises(ConfigError, match="self_qa.max_assertions"):
        parse_agent_config_data(
            {
                "name": "qa_bot",
                "model": "mock",
                "self_qa": {
                    "min_assertions": 3,
                    "max_assertions": 2,
                },
            }
        )


def test_parse_agent_config_builds_routing_policy():
    config = parse_agent_config_data(
        {
            "name": "router_bot",
            "model": "mock",
            "routing": {
                "enabled": True,
                "budget_preference": "cheap",
                "task_budget_usd": 0.02,
                "task_budget_tokens": 900,
                "fail_on_budget_exhausted": True,
                "ledger_backend": "sqlite",
                "ledger_path": ".logos_adk/runtime_spend.db",
                "workspace_soft_limit_usd": 0.05,
                "session_soft_limit_usd": 0.01,
                "threshold_ratio": 0.75,
                "simple_task_types": ["support"],
                "critical_task_types": ["analysis"],
                "medium_length_threshold": 120,
                "high_length_threshold": 500,
            },
        }
    )

    assert config.routing.enabled is True
    assert config.routing.budget_preference == "cheap"
    assert config.routing.task_budget_usd == 0.02
    assert config.routing.task_budget_tokens == 900
    assert config.routing.fail_on_budget_exhausted is True
    assert config.routing.ledger_backend == "sqlite"
    assert config.routing.ledger_path == ".logos_adk/runtime_spend.db"
    assert config.routing.workspace_soft_limit_usd == 0.05
    assert config.routing.session_soft_limit_usd == 0.01
    assert config.routing.threshold_ratio == 0.75
    assert config.routing.simple_task_types == ["support"]
    assert config.routing.critical_task_types == ["analysis"]
    assert config.routing.medium_length_threshold == 120
    assert config.routing.high_length_threshold == 500


def test_parse_agent_config_builds_postgresql_routing_ledger():
    config = parse_agent_config_data(
        {
            "name": "router_bot",
            "model": "mock",
            "routing": {
                "enabled": True,
                "ledger_backend": "postgres",
                "ledger_dsn": "postgresql://ledger-db/logos",
            },
        }
    )

    assert config.routing.ledger_backend == "postgresql"
    assert config.routing.ledger_dsn == "postgresql://ledger-db/logos"


def test_parse_agent_config_rejects_invalid_routing_policy():
    with pytest.raises(ConfigError, match="routing.budget_preference"):
        parse_agent_config_data(
            {
                "name": "router_bot",
                "model": "mock",
                "routing": {
                    "enabled": True,
                    "budget_preference": "free",
                },
            }
        )

    with pytest.raises(ConfigError, match="routing.task_budget_tokens"):
        parse_agent_config_data(
            {
                "name": "router_bot",
                "model": "mock",
                "routing": {
                    "task_budget_tokens": -1,
                },
            }
        )

    with pytest.raises(ConfigError, match="routing.fail_on_budget_exhausted"):
        parse_agent_config_data(
            {
                "name": "router_bot",
                "model": "mock",
                "routing": {
                    "fail_on_budget_exhausted": "yes",
                },
            }
        )
