import json

import pytest

from nanobot.config.errors import ConfigLoadError
from nanobot.config.loader import load_config, resolve_config_env_vars
from nanobot.config.schema import ApiConfig


def test_load_config_missing_file_uses_defaults(tmp_path) -> None:
    config_path = tmp_path / "instance" / "missing.json"
    config = load_config(config_path)

    assert config.agents.defaults.model
    assert config.runtime_data_dir == config_path.parent


def test_env_resolution_preserves_config_runtime_data_dir(
    tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
    config_path = tmp_path / "instance" / "config.json"
    config_path.parent.mkdir()
    config_path.write_text('{"providers": {"openai": {"apiKey": "${TEST_API_KEY}"}}}')
    monkeypatch.setenv("TEST_API_KEY", "resolved")

    config = resolve_config_env_vars(load_config(config_path), config_path=config_path)

    assert config.runtime_data_dir == config_path.parent


def test_load_config_reports_malformed_environment_safely(
    tmp_path,
    monkeypatch,
) -> None:
    config_path = tmp_path / "missing.json"
    invalid_value = "sensitive-not-json"
    monkeypatch.setenv("NANOBOT_PROVIDERS", invalid_value)

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    error = exc_info.value
    assert error.kind == "invalid_schema"
    assert error.path == config_path
    assert "complex NANOBOT_* values use valid JSON" in str(error)
    assert invalid_value not in str(error)


def test_load_config_invalid_json_fails_fast(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    config_path.write_text("{broken json", encoding="utf-8")

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    error = exc_info.value
    assert error.kind == "invalid_json"
    assert "line 1, column 2" in str(error)


def test_load_config_invalid_schema_fails_fast(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    config_path.write_text(
        json.dumps({"tools": {"exec": {"timeout": -1}}}),
        encoding="utf-8",
    )

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    error = exc_info.value
    message = str(error)
    assert error.kind == "invalid_schema"
    assert "tools.exec.timeout" in message
    assert "Must be greater than or equal to 0." in message
    assert "input_value" not in message
    assert "errors.pydantic.dev" not in message


@pytest.mark.parametrize(
    ("content", "root_type"),
    [("[]", "list"), ("null", "NoneType"), ('"value"', "str")],
)
def test_load_config_rejects_non_object_root(tmp_path, content: str, root_type: str) -> None:
    config_path = tmp_path / "config.json"
    config_path.write_text(content, encoding="utf-8")

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    error = exc_info.value
    assert error.kind == "invalid_root"
    assert f"Expected an object, but found {root_type}." in str(error)


def test_load_config_error_does_not_expose_invalid_secret_value(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    secret = "should-never-appear"
    config_path.write_text(
        json.dumps({"providers": {"openrouter": {"apiKey": [secret]}}}),
        encoding="utf-8",
    )

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    assert secret not in str(exc_info.value)


def test_load_config_error_redacts_untrusted_location_parts(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    secret = "should-never-appear-in-location"
    server_name = f"https://user:{secret}@example.test"
    config_path.write_text(
        json.dumps(
            {
                "tools": {
                    "mcpServers": {
                        server_name: {"toolTimeout": "not-a-number"},
                    }
                }
            }
        ),
        encoding="utf-8",
    )

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    message = str(exc_info.value)
    assert "tools.mcpServers.<redacted>.toolTimeout" in message
    assert server_name not in message
    assert secret not in message


def test_load_config_error_does_not_trust_custom_validator_message(tmp_path) -> None:
    config_path = tmp_path / "config.json"
    secret = "diagnostic-secret-should-not-print"
    config_path.write_text(
        json.dumps({"providers": {"openrouter": {"thinkingStyle": secret}}}),
        encoding="utf-8",
    )

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    message = str(exc_info.value)
    assert "providers.openrouter.thinkingStyle" in message
    assert "Value does not satisfy this setting's requirements." in message
    assert secret not in message


@pytest.mark.parametrize(
    "tools",
    [
        [],
        {"exec": []},
        {"my": 1, "myEnabled": True},
    ],
)
def test_load_config_malformed_legacy_sections_use_structured_error(
    tmp_path,
    tools: object,
) -> None:
    config_path = tmp_path / "config.json"
    config_path.write_text(json.dumps({"tools": tools}), encoding="utf-8")

    with pytest.raises(ConfigLoadError) as exc_info:
        load_config(config_path)

    error = exc_info.value
    assert error.kind == "invalid_schema"
    assert "tools" in str(error)


@pytest.mark.parametrize("host", ["0.0.0.0", "::"])
def test_api_config_requires_key_for_wildcard_hosts(host: str) -> None:
    with pytest.raises(ValueError, match="api_key is not set"):
        ApiConfig(host=host)


def test_api_config_allows_wildcard_host_with_key() -> None:
    config = ApiConfig(host="0.0.0.0", api_key="secret")

    assert config.host == "0.0.0.0"
    assert config.api_key == "secret"
