"""Tests for config.yaml structure validation (validate_config_structure)."""


import pytest

from hermes_cli.config import (
    validate_config_structure,
)


class TestCustomProvidersValidation:
    """custom_providers must be a YAML list, not a dict."""

    def test_dict_instead_of_list(self):
        """The exact Discord user scenario — custom_providers as flat dict."""
        issues = validate_config_structure({
            "custom_providers": {
                "name": "Generativelanguage.googleapis.com",
                "base_url": "https://generativelanguage.googleapis.com/v1beta",
                "api_key": "xxx",
                "model": "models/gemini-2.5-flash",
                "rate_limit_delay": 2.0,
                "fallback_model": {
                    "provider": "openrouter",
                    "model": "qwen/qwen3.6-plus:free",
                },
            },
            "fallback_providers": [],
        })
        errors = [i for i in issues if i.severity == "error"]
        assert any("dict" in i.message and "list" in i.message for i in errors), (
            "Should detect custom_providers as dict instead of list"
        )

    def test_scalar_is_an_error_naming_key_and_type(self):
        """A non-list scalar (a bad `config set`) makes every endpoint vanish — name the key and the type."""
        issues = validate_config_structure({"custom_providers": "oops", "model": {"provider": "openrouter"}})
        errors = [i.message for i in issues if i.severity == "error"]
        assert any(m.startswith("custom_providers is a str") and "list" in m for m in errors), errors
        assert not [i for i in validate_config_structure(
            {"custom_providers": [{"name": "x", "base_url": "http://h/v1"}], "model": {"provider": "custom"}})
            if i.severity == "error"]

    def test_dict_detects_misplaced_fields(self):
        """When custom_providers is a dict, detect fields that look misplaced."""
        issues = validate_config_structure({
            "custom_providers": {
                "name": "test",
                "base_url": "https://example.com",
                "api_key": "xxx",
            },
        })
        warnings = [i for i in issues if i.severity == "warning"]
        # Should flag base_url, api_key as looking like custom_providers entry fields
        misplaced = [i for i in warnings if "custom_providers entry fields" in i.message]
        assert len(misplaced) == 1


    def test_list_entry_not_dict(self):
        """Non-dict list entries should warn."""
        issues = validate_config_structure({
            "custom_providers": ["not-a-dict"],
            "model": {"provider": "custom"},
        })
        assert any("not a dict" in i.message for i in issues)




class TestMissingModelSection:
    """Warn when custom_providers exists but model section is missing."""


    def test_custom_providers_with_model(self):
        issues = validate_config_structure({
            "custom_providers": [
                {"name": "test", "base_url": "https://example.com/v1"},
            ],
            "model": {"provider": "custom", "default": "test-model"},
        })
        # Should not warn about missing model section
        assert not any("no 'model' section" in i.message for i in issues)




class TestVoiceSubmitModeValidation:

    def test_direct_and_draft_are_valid(self):
        for mode in ("direct", "draft"):
            issues = validate_config_structure({"voice": {"submit_mode": mode}})
            assert not any("voice.submit_mode" in issue.message for issue in issues)

    def test_invalid_mode_is_reported(self):
        issues = validate_config_structure({"voice": {"submit_mode": "refine"}})

        assert any(
            issue.severity == "error"
            and "voice.submit_mode" in issue.message
            and "direct" in issue.hint
            and "draft" in issue.hint
            for issue in issues
        )


def _has_tz_database() -> bool:
    try:
        import zoneinfo
        zoneinfo.ZoneInfo("UTC")
        return True
    except Exception:
        return False


def _tz_issues(config):
    return [i for i in validate_config_structure(config) if "timezone" in i.message]


class TestTimezoneValidation:
    """An invalid ``timezone`` silently puts the agent clock and every cron
    schedule on server-local time (hermes_time._get_zoneinfo falls back with
    one log warning). validate_config_structure must report it (#111725)."""

    @pytest.mark.skipif(not _has_tz_database(), reason="no tz database in this interpreter")
    def test_invalid_or_non_string_zone_is_an_error(self):
        [issue] = _tz_issues({"timezone": "Asia/Tokio", "model": {"provider": "nous"}})
        assert issue.severity == "error"
        assert "Asia/Tokio" in issue.message
        assert "IANA" in issue.hint and "HERMES_TIMEZONE" in issue.hint
        [issue] = _tz_issues({"timezone": 9, "model": {"provider": "nous"}})
        assert issue.severity == "error" and "string" in issue.message

    def test_valid_blank_missing_or_unverifiable_zone_is_silent(self, monkeypatch):
        for cfg in ({"timezone": "Asia/Tokyo"}, {}, {"timezone": ""}, {"timezone": "   "}, {"timezone": None}):
            assert _tz_issues({**cfg, "model": {"provider": "nous"}}) == []
        # Bare Windows without tzdata: ZoneInfo cannot load anything, including UTC.
        # A name that cannot be checked must not be flagged.
        import zoneinfo

        def no_db(_key):
            raise zoneinfo.ZoneInfoNotFoundError("no tz database")

        monkeypatch.setattr(zoneinfo, "ZoneInfo", no_db)
        assert _tz_issues({"timezone": "Asia/Tokio", "model": {"provider": "nous"}}) == []


class TestUnknownTopLevelKeys:
    """Arbitrary top-level keys must NOT warn — they are bridged to os.environ.

    Top-level scalars in config.yaml are forwarded into the environment
    (gateway/run.py, hermes send) so users can feed skills and external apps
    env-style keys like DISCORD_HOME_CHANNEL or MY_APP_TOKEN. A closed-world
    allowlist can never enumerate those, so no "Unknown top-level config key"
    warning may exist.
    """



    def test_provider_like_unknown_root_keeps_misplaced_message(self):
        """Preserve existing base_url/api_key root-level guidance."""
        issues = validate_config_structure({
            "base_url": "https://example.com/v1",
            "api_key": "secret",
        })
        misplaced = [
            i for i in issues
            if i.severity == "warning" and "looks misplaced" in i.message
        ]
        assert any("base_url" in i.message for i in misplaced)
        assert any("api_key" in i.message for i in misplaced)



class TestQuotedContainerValues:
    """A list/mapping slot holding one quoted string is ignored by every reader (#83308, #105706)."""

    def test_quoted_list_in_container_slot_is_flagged_with_remedy(self):
        issues = validate_config_structure({
            "plugins": {"enabled": '["a","b"]'},
            "model_catalog": {"excluded_providers": '["openai-api"]'},
        })
        flagged = {i.message.split(" ", 1)[0]: i for i in issues if "quoted string" in i.message}
        assert set(flagged) == {"plugins.enabled", "model_catalog.excluded_providers"}
        assert "hermes config set plugins.enabled '[\"a\",\"b\"]'" in flagged["plugins.enabled"].hint

    def test_string_typed_and_tolerant_slots_are_not_flagged(self):
        """`approvals.mode` is a string in the schema; `model: name` is the documented shorthand;
        `agent.disabled_toolsets` readers parse the quoted form themselves."""
        issues = validate_config_structure({
            "approvals": {"mode": "[off]"},
            "model": "gpt-4o",
            "agent": {"disabled_toolsets": '["web"]'},
            "plugins": {"enabled": ["a"]},
        })
        assert not [i for i in issues if "quoted string" in i.message]
