from __future__ import annotations

import builtins
import json
from types import SimpleNamespace

import httpx
import pytest

from nanobot.config.loader import load_config, save_config
from nanobot.config.schema import Config, ModelPresetConfig
from nanobot.providers.registry import find_by_name
from nanobot.webui.settings_api import (
    WebUISettingsError,
    _docs_version,
    _model_catalog_kind,
    _oauth_provider_status,
    _reasoning_effort_values_for,
    create_model_configuration,
    login_oauth_provider,
    provider_models_payload,
    settings_payload,
    settings_usage_payload,
    update_agent_settings,
    update_api_settings,
    update_model_configuration,
    update_network_safety_settings,
    update_provider_settings,
    update_transcription_settings,
    update_web_search_settings,
)

DYNAMIC_PROVIDER_NAME = "my-company-api"
DYNAMIC_PROVIDER_API_BASE = "https://example.test/v1"


def test_settings_payload_propagates_preset_resolution_failure(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config = Config()
    monkeypatch.setattr("nanobot.webui.settings_api.load_config", lambda: config)
    monkeypatch.setattr(
        Config,
        "resolve_preset",
        lambda _self: (_ for _ in ()).throw(RuntimeError("invalid preset")),
    )

    with pytest.raises(RuntimeError, match="invalid preset"):
        settings_payload()


def test_docs_version_uses_released_versions_and_falls_back_for_dev() -> None:
    assert _docs_version("0.2.3") == "0.2.3"
    assert _docs_version("0.2.3.post1") == "0.2.3.post1"
    assert _docs_version("0.2.3.dev0") == "latest"
    assert _docs_version("0.2.3+editable") == "latest"


def test_kimi_k3_only_offers_supported_reasoning_effort_values() -> None:
    assert _reasoning_effort_values_for("moonshot", "kimi-k3") == ["", "max"]


def test_settings_payload_includes_versioned_docs(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.settings_api.__version__", "0.2.3")

    payload = settings_payload()

    assert payload["docs"] == {
        "version": "0.2.3",
        "base_url": "https://nanobot.wiki/docs/0.2.3",
        "chat_apps_url": "https://nanobot.wiki/docs/0.2.3/getting-started/chat-apps",
        "latest_url": "https://nanobot.wiki/docs/latest",
    }


def test_settings_payload_includes_relocated_capabilities(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.api.port = 9910
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
    monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")

    payload = settings_payload()

    assert payload["api"]["port"] == 9910
    assert payload["api"]["api_key_hint"] is None
    assert payload["observability"]["provider"] == "langfuse"
    assert payload["observability"]["configured"] is True


def test_update_api_settings_requires_key_for_network_access(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="API key"):
        update_api_settings({"host": ["0.0.0.0"], "port": ["8900"]})

    payload = update_api_settings({
        "host": ["0.0.0.0"],
        "port": ["9900"],
        "api_key": ["secret-token"],
    })
    saved = load_config(config_path)
    assert saved.api.host == "0.0.0.0"
    assert saved.api.port == 9900
    assert saved.api.api_key == "secret-token"
    assert payload["api"]["api_key_hint"]


def test_update_api_settings_requires_key_for_specific_network_interface(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="API key"):
        update_api_settings({"host": ["192.168.1.10"], "port": ["8900"]})


def test_update_api_settings_allows_alternate_loopback_without_key(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    update_api_settings({"host": ["127.0.0.2"], "port": ["8900"]})

    assert load_config(config_path).api.host == "127.0.0.2"


def _dynamic_provider_config(
    *,
    api_base: str = DYNAMIC_PROVIDER_API_BASE,
    defaults: bool = False,
) -> Config:
    raw_config = {
        "providers": {
            DYNAMIC_PROVIDER_NAME: {
                "apiBase": api_base,
            }
        }
    }
    if defaults:
        raw_config["agents"] = {
            "defaults": {
                "provider": DYNAMIC_PROVIDER_NAME,
                "model": "gpt-4o-mini",
            }
        }
    return Config.model_validate(raw_config)


def test_create_model_configuration_writes_label_and_selects(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.agents.defaults.model = "openai/gpt-4o"
    config.agents.defaults.provider = "openai"
    config.providers.openai.api_key = "sk-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = create_model_configuration(
        {
            "label": ["Fast writing"],
            "provider": ["openai"],
            "model": ["openai/gpt-4.1-mini"],
        }
    )

    assert payload["agent"]["model_preset"] == "fast-writing"
    assert payload["agent"]["model"] == "openai/gpt-4.1-mini"
    rows = {row["name"]: row for row in payload["model_presets"]}
    assert rows["fast-writing"]["label"] == "Fast writing"

    saved = load_config(config_path)
    assert saved.agents.defaults.model_preset == "fast-writing"
    assert saved.model_presets["fast-writing"].label == "Fast writing"
    assert saved.model_presets["fast-writing"].model == "openai/gpt-4.1-mini"
    assert saved.model_presets["fast-writing"].provider == "openai"

    with pytest.raises(WebUISettingsError) as duplicate:
        create_model_configuration(
            {
                "label": ["Fast writing"],
                "provider": ["openai"],
                "model": ["openai/gpt-4.1-mini"],
            }
        )
    assert duplicate.value.status == 409


def test_create_model_configuration_accepts_dynamic_custom_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(_dynamic_provider_config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = create_model_configuration(
        {
            "label": ["Tenant model"],
            "provider": [DYNAMIC_PROVIDER_NAME],
            "model": ["gpt-4o-mini"],
        }
    )

    assert payload["agent"]["model_preset"] == "tenant-model"
    assert payload["agent"]["provider"] == DYNAMIC_PROVIDER_NAME
    saved = load_config(config_path)
    assert saved.model_presets["tenant-model"].provider == DYNAMIC_PROVIDER_NAME
    assert saved.model_presets["tenant-model"].model == "gpt-4o-mini"


def test_create_model_configuration_rejects_dynamic_custom_provider_without_api_base(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config.model_validate({
        "providers": {
            DYNAMIC_PROVIDER_NAME: {
                "apiKey": "sk-test",
            }
        }
    })
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="provider is not configured"):
        create_model_configuration(
            {
                "label": ["Tenant model"],
                "provider": [DYNAMIC_PROVIDER_NAME],
                "model": ["gpt-4o-mini"],
            }
        )


def test_create_model_configuration_rejects_unconfigured_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="provider is not configured"):
        create_model_configuration(
            {
                "label": ["Deep"],
                "provider": ["openai"],
                "model": ["openai/gpt-4.1"],
            }
        )


def test_update_model_configuration_edits_named_preset_and_selects(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.openai.api_key = "sk-test"
    config.model_presets["codex"] = ModelPresetConfig(
        label="Old Codex",
        provider="openai",
        model="openai/gpt-4.1",
    )
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr(
        "nanobot.webui.settings_api._oauth_provider_status",
        lambda spec: {
            "configured": spec.name == "openai_codex",
            "account": "acct-test",
            "expires_at": 123,
            "login_supported": True,
        },
    )

    payload = update_model_configuration(
        {
            "name": ["codex"],
            "label": ["Codex"],
            "provider": ["openai_codex"],
            "model": ["openai-codex/gpt-5.5"],
        }
    )

    assert payload["agent"]["model_preset"] == "codex"
    assert payload["agent"]["model"] == "openai-codex/gpt-5.5"
    saved = load_config(config_path)
    assert saved.agents.defaults.model_preset == "codex"
    assert saved.model_presets["codex"].label == "Codex"
    assert saved.model_presets["codex"].provider == "openai_codex"
    assert saved.model_presets["codex"].model == "openai-codex/gpt-5.5"


def test_update_provider_settings_updates_dynamic_custom_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(_dynamic_provider_config(api_base="https://old.example/v1"), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_provider_settings(
        {
            "provider": [DYNAMIC_PROVIDER_NAME],
            "apiBase": ["https://new.example/v1"],
            "apiKey": ["sk-test"],
        }
    )

    providers = {row["name"]: row for row in payload["providers"]}
    assert providers[DYNAMIC_PROVIDER_NAME]["api_base"] == "https://new.example/v1"
    assert providers[DYNAMIC_PROVIDER_NAME]["api_key_hint"] == "••••"
    saved = load_config(config_path)
    dynamic_provider = saved.providers.model_extra[DYNAMIC_PROVIDER_NAME]
    assert dynamic_provider.api_base == "https://new.example/v1"
    assert dynamic_provider.api_key == "sk-test"


def test_update_agent_settings_accepts_context_window_options(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_agent_settings({"context_window_tokens": ["200000"]})

    assert payload["agent"]["context_window_tokens"] == 200000
    saved = load_config(config_path)
    assert saved.agents.defaults.context_window_tokens == 200000


def test_update_model_configuration_accepts_context_window_options(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.model_presets["codex"] = ModelPresetConfig(
        label="Codex",
        provider="openai",
        model="openai/gpt-4.1",
    )
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_model_configuration(
        {
            "name": ["codex"],
            "context_window_tokens": ["1048576"],
        }
    )

    assert payload["agent"]["context_window_tokens"] == 1048576
    saved = load_config(config_path)
    assert saved.model_presets["codex"].context_window_tokens == 1048576


def test_update_context_window_rejects_unknown_values(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(
        WebUISettingsError,
        match="context_window_tokens must be 65536, 200000, 262144, or 1048576",
    ):
        update_agent_settings({"context_window_tokens": ["128000"]})


def test_update_model_configuration_rejects_default_preset(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="model configuration is required"):
        update_model_configuration({"name": ["default"], "model": ["openai/gpt-4.1"]})


def test_settings_payload_includes_oauth_provider_status(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    def fake_oauth_status(spec):
        if spec.name == "openai_codex":
            return {
                "configured": True,
                "account": "acct-test",
                "expires_at": 123,
                "login_supported": True,
            }
        return {
            "configured": False,
            "account": None,
            "expires_at": None,
            "login_supported": True,
        }

    monkeypatch.setattr("nanobot.webui.settings_api._oauth_provider_status", fake_oauth_status)

    payload = settings_payload()
    providers = {row["name"]: row for row in payload["providers"]}

    assert providers["openai_codex"]["auth_type"] == "oauth"
    assert providers["openai_codex"]["configured"] is True
    assert providers["openai_codex"]["oauth_account"] == "acct-test"


def test_settings_payload_includes_dynamic_custom_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(_dynamic_provider_config(defaults=True), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    providers = {row["name"]: row for row in payload["providers"]}

    assert payload["agent"]["provider"] == DYNAMIC_PROVIDER_NAME
    assert payload["agent"]["resolved_provider"] == DYNAMIC_PROVIDER_NAME
    assert providers[DYNAMIC_PROVIDER_NAME]["configured"] is True
    assert providers[DYNAMIC_PROVIDER_NAME]["api_key_required"] is False
    assert providers[DYNAMIC_PROVIDER_NAME]["api_base"] == DYNAMIC_PROVIDER_API_BASE


def test_settings_payload_groups_opencode_compatibility_alias(tmp_path, monkeypatch) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    opencode_rows = [row for row in payload["providers"] if row["label"].startswith("OpenCode")]

    assert [(row["name"], row["label"]) for row in opencode_rows] == [
        ("opencode", "OpenCode Zen"),
        ("opencode_go", "OpenCode Go"),
    ]


def test_settings_payload_keeps_configured_opencode_legacy_alias(tmp_path, monkeypatch) -> None:
    config_path = tmp_path / "config.json"
    config = Config.model_validate({
        "providers": {"opencodeZen": {"apiKey": "legacy-key"}},
        "agents": {
            "defaults": {
                "provider": "opencode_zen",
                "model": "opencode/deepseek-v4-pro",
            }
        },
    })
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    zen_rows = [row for row in payload["providers"] if row["label"] == "OpenCode Zen"]

    assert len(zen_rows) == 1
    assert zen_rows[0]["name"] == "opencode_zen"
    assert zen_rows[0]["configured"] is True


def test_settings_payload_marks_dynamic_custom_provider_without_api_base_unconfigured(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config.model_validate({
        "providers": {
            DYNAMIC_PROVIDER_NAME: {
                "apiKey": "sk-test",
            }
        }
    })
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    providers = {row["name"]: row for row in payload["providers"]}

    assert providers[DYNAMIC_PROVIDER_NAME]["configured"] is False
    assert providers[DYNAMIC_PROVIDER_NAME]["api_key_hint"] == "••••"
    assert providers[DYNAMIC_PROVIDER_NAME]["api_base"] is None


def test_settings_payload_includes_network_safety_fields(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.tools.webui_allow_local_service_access = False
    config.tools.ssrf_whitelist = ["100.64.0.0/10"]
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.workspaces.get_webui_dir", lambda: tmp_path / "webui")

    payload = settings_payload()

    assert payload["advanced"]["webui_allow_local_service_access"] is False
    assert payload["advanced"]["allow_local_preview_access"] is False
    assert payload["advanced"]["webui_default_access_mode"] == "default"
    assert payload["advanced"]["private_service_protection_enabled"] is True
    assert payload["advanced"]["ssrf_whitelist_count"] == 1


def test_settings_payload_includes_exec_path_flags(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.tools.exec.path_prepend = "/venv/bin"
    config.tools.exec.path_append = "/usr/sbin"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.workspaces.get_webui_dir", lambda: tmp_path / "webui")

    payload = settings_payload()

    assert payload["advanced"]["exec_path_prepend_set"] is True
    assert payload["advanced"]["exec_path_append_set"] is True


def test_update_web_search_settings_accepts_keenable_without_api_key(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.tools.web.search.provider = "brave"
    config.tools.web.search.api_key = "brave-key"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_web_search_settings({"provider": ["keenable"]})

    saved = load_config(config_path)
    assert saved.tools.web.search.provider == "keenable"
    assert saved.tools.web.search.api_key == ""
    option = next(item for item in payload["web_search"]["providers"] if item["name"] == "keenable")
    assert option["credential"] == "optional_api_key"


def test_update_web_search_settings_can_clear_optional_api_key(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.tools.web.search.provider = "keenable"
    config.tools.web.search.api_key = "keen-key"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    update_web_search_settings({"provider": ["keenable"], "api_key": [""]})

    saved = load_config(config_path)
    assert saved.tools.web.search.provider == "keenable"
    assert saved.tools.web.search.api_key == ""


def test_settings_payload_includes_effective_transcription_config(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.channels.transcription_provider = "openai"
    config.channels.transcription_language = "en"
    config.providers.openai.api_key = "sk-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()

    assert payload["transcription"]["enabled"] is True
    assert payload["transcription"]["provider"] == "openai"
    assert payload["transcription"]["provider_configured"] is True
    assert payload["transcription"]["model"] == "whisper-1"
    assert payload["transcription"]["language"] == "en"


def test_settings_payload_exposes_openrouter_transcription_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.openrouter.api_key = "sk-or-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()

    providers = {provider["name"]: provider for provider in payload["transcription"]["providers"]}
    assert providers["openrouter"]["label"] == "OpenRouter"
    assert providers["openrouter"]["configured"] is True


def test_settings_payload_exposes_siliconflow_transcription_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.siliconflow.api_key = "sf-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()

    providers = {provider["name"]: provider for provider in payload["transcription"]["providers"]}
    assert providers["siliconflow"]["label"] == "SiliconFlow"
    assert providers["siliconflow"]["configured"] is True
    assert providers["siliconflow"]["default_api_base"] == "https://api.siliconflow.cn/v1"


def test_settings_payload_exposes_xiaomi_mimo_transcription_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.xiaomi_mimo.api_key = "mimo-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()

    providers = {provider["name"]: provider for provider in payload["transcription"]["providers"]}
    assert providers["xiaomi_mimo"]["label"] == "Xiaomi MIMO"
    assert providers["xiaomi_mimo"]["configured"] is True


def test_settings_payload_exposes_assemblyai_transcription_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.transcription.provider = "assemblyai"
    config.providers.assemblyai.api_key = "aai-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()

    assert payload["transcription"]["provider"] == "assemblyai"
    assert payload["transcription"]["provider_configured"] is True
    providers = {provider["name"]: provider for provider in payload["transcription"]["providers"]}
    assert providers["assemblyai"]["label"] == "AssemblyAI"
    assert providers["assemblyai"]["configured"] is True
    assert providers["assemblyai"]["default_api_base"] == "https://api.assemblyai.com/v2"
    provider_rows = {provider["name"]: provider for provider in payload["providers"]}
    assert provider_rows["assemblyai"]["configured"] is True
    assert provider_rows["assemblyai"]["model_selectable"] is False


def test_model_configuration_rejects_transcription_only_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.assemblyai.api_key = "aai-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="does not support chat models"):
        create_model_configuration(
            {
                "label": ["Voice only"],
                "provider": ["assemblyai"],
                "model": ["universal-3-pro"],
            }
        )


def test_update_transcription_settings_writes_top_level_only(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.channels.transcription_provider = "openai"
    config.channels.transcription_language = "en"
    config.providers.groq.api_key = "gsk-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_transcription_settings(
        {
            "enabled": ["true"],
            "provider": ["groq"],
            "model": ["whisper-large-v3-turbo"],
            "language": ["ko"],
            "maxDurationSec": ["90"],
            "maxUploadMb": ["20"],
        }
    )

    saved = load_config(config_path)
    assert saved.channels.transcription_provider == "openai"
    assert saved.channels.transcription_language == "en"
    assert saved.transcription.enabled is True
    assert saved.transcription.provider == "groq"
    assert saved.transcription.model == "whisper-large-v3-turbo"
    assert saved.transcription.language == "ko"
    assert saved.transcription.max_duration_sec == 90
    assert saved.transcription.max_upload_mb == 20
    assert payload["transcription"]["provider"] == "groq"
    assert payload["transcription"]["provider_configured"] is True


def test_update_transcription_settings_accepts_openrouter(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.openrouter.api_key = "sk-or-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_transcription_settings(
        {
            "provider": ["openrouter"],
            "model": ["nvidia/parakeet-tdt-0.6b-v3"],
        }
    )

    saved = load_config(config_path)
    assert saved.transcription.provider == "openrouter"
    assert saved.transcription.model == "nvidia/parakeet-tdt-0.6b-v3"
    assert payload["transcription"]["provider"] == "openrouter"
    assert payload["transcription"]["provider_configured"] is True


def test_update_transcription_settings_accepts_xiaomi_mimo(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.xiaomi_mimo.api_key = "mimo-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_transcription_settings(
        {
            "provider": ["xiaomi_mimo"],
            "model": ["mimo-v2.5-asr"],
            "language": ["zh"],
        }
    )

    saved = load_config(config_path)
    assert saved.transcription.provider == "xiaomi_mimo"
    assert saved.transcription.model == "mimo-v2.5-asr"
    assert saved.transcription.language == "zh"
    assert payload["transcription"]["provider"] == "xiaomi_mimo"
    assert payload["transcription"]["provider_configured"] is True


def test_update_transcription_settings_accepts_assemblyai(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.assemblyai.api_key = "aai-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = update_transcription_settings(
        {
            "provider": ["assemblyai"],
            "model": ["universal-3-pro"],
        }
    )

    saved = load_config(config_path)
    assert saved.transcription.provider == "assemblyai"
    assert saved.transcription.model == "universal-3-pro"
    assert payload["transcription"]["provider"] == "assemblyai"
    assert payload["transcription"]["provider_configured"] is True


def test_update_transcription_settings_validates_language(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="transcription language"):
        update_transcription_settings({"language": ["en-US"]})


def test_settings_payload_includes_token_usage_summary(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.token_usage.get_webui_dir", lambda: tmp_path / "webui")

    from nanobot.webui.token_usage import record_token_usage

    record_token_usage({"prompt_tokens": 10, "completion_tokens": 5})

    payload = settings_payload()

    assert payload["usage"]["total_tokens_30d"] == 15
    assert payload["usage"]["total_tokens"] == 15
    assert payload["usage"]["peak_day_tokens"] == 15
    assert payload["usage"]["current_streak_days"] == 1
    assert payload["usage"]["longest_streak_days"] == 1
    assert payload["usage"]["active_days_30d"] == 1
    assert payload["usage"]["requests_30d"] == 1


def test_settings_usage_payload_returns_lightweight_token_usage(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.token_usage.get_webui_dir", lambda: tmp_path / "webui")

    from nanobot.webui.token_usage import record_token_usage

    record_token_usage({"prompt_tokens": 20, "completion_tokens": 2})

    payload = settings_usage_payload()

    assert payload["total_tokens"] == 22
    assert payload["requests_30d"] == 1
    assert "agent" not in payload


def test_update_network_safety_settings_writes_local_service_flag(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.workspaces.get_webui_dir", lambda: tmp_path / "webui")

    payload = update_network_safety_settings(
        {
            "webui_allow_local_service_access": ["false"],
            "webui_default_access_mode": ["full"],
        }
    )

    saved = load_config(config_path)
    saved_raw = json.loads(config_path.read_text(encoding="utf-8"))
    assert saved.tools.webui_allow_local_service_access is False
    assert saved_raw["tools"]["webuiAllowLocalServiceAccess"] is False
    assert "allowLocalPreviewAccess" not in saved_raw["tools"]
    assert payload["advanced"]["webui_allow_local_service_access"] is False
    assert payload["advanced"]["webui_default_access_mode"] == "full"
    assert payload["requires_restart"] is True


def test_update_network_safety_settings_accepts_legacy_restricted_default_access(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.workspaces.get_webui_dir", lambda: tmp_path / "webui")

    payload = update_network_safety_settings({"webui_default_access_mode": ["restricted"]})

    assert payload["advanced"]["webui_default_access_mode"] == "default"


def test_update_network_safety_settings_default_access_is_webui_only(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    before = config_path.read_text(encoding="utf-8")
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr("nanobot.webui.workspaces.get_webui_dir", lambda: tmp_path / "webui")

    payload = update_network_safety_settings({"webui_default_access_mode": ["full"]})

    saved = load_config(config_path)
    assert config_path.read_text(encoding="utf-8") == before
    assert saved.tools.restrict_to_workspace is False
    assert payload["advanced"]["webui_default_access_mode"] == "full"
    assert payload["requires_restart"] is False


def test_openai_codex_oauth_status_uses_available_token(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    token = type(
        "Token",
        (),
        {
            "access": "access-token",
            "refresh": "refresh-token",
            "expires": 2_000_000_000_000,
            "account_id": "acct-codex",
        },
    )()
    monkeypatch.setattr("oauth_cli_kit.storage.FileTokenStorage.load", lambda _self: token)

    status = _oauth_provider_status(find_by_name("openai_codex"))

    assert status["configured"] is True
    assert status["account"] == "acct-codex"


def test_openai_codex_oauth_status_uses_refreshable_expired_token(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    token = type(
        "Token",
        (),
        {
            "access": "access-token",
            "refresh": "refresh-token",
            "expires": 1,
            "account_id": "acct-codex",
        },
    )()
    monkeypatch.setattr("oauth_cli_kit.storage.FileTokenStorage.load", lambda _self: token)

    status = _oauth_provider_status(find_by_name("openai_codex"))

    assert status["configured"] is True
    assert status["expires_at"] == 1


def test_openai_codex_oauth_status_rejects_unavailable_token(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def fake_load(_self):
        raise RuntimeError("refresh failed")

    monkeypatch.setattr("oauth_cli_kit.storage.FileTokenStorage.load", fake_load)

    status = _oauth_provider_status(find_by_name("openai_codex"))

    assert status["configured"] is False
    assert status["account"] is None


def test_openai_codex_oauth_login_passes_configured_proxy(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    proxy = "http://127.0.0.1:23458"
    config_path = tmp_path / "config.json"
    save_config(
        Config.model_validate({"providers": {"openaiCodex": {"proxy": "${CODEX_PROXY_TEST}"}}}),
        config_path,
    )
    monkeypatch.setenv("CODEX_PROXY_TEST", proxy)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    import oauth_cli_kit

    captured: dict[str, str | None] = {}

    def fake_get_token(*, proxy=None):
        captured["get_proxy"] = proxy
        raise RuntimeError("no-token")

    def fake_login(*, print_fn, prompt_fn, proxy=None):
        captured["login_proxy"] = proxy
        return SimpleNamespace(access="access-token", account_id="acct-test")

    monkeypatch.setattr(oauth_cli_kit, "get_token", fake_get_token)
    monkeypatch.setattr(oauth_cli_kit, "login_oauth_interactive", fake_login)

    login_oauth_provider({"provider": ["openai-codex"]})

    assert captured == {"get_proxy": proxy, "login_proxy": proxy}


def test_openai_codex_oauth_login_reports_missing_oauth_cli_kit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    real_import = builtins.__import__

    def fake_import(name, *args, **kwargs):
        if name == "oauth_cli_kit":
            raise ImportError("missing")
        return real_import(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", fake_import)

    with pytest.raises(WebUISettingsError) as exc:
        login_oauth_provider({"provider": ["openai-codex"]})

    assert "oauth_cli_kit not installed. Run: pip install oauth-cli-kit" in str(exc.value)


def test_github_copilot_oauth_login_reports_missing_oauth_cli_kit(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    real_import = builtins.__import__

    def fake_import(name, *args, **kwargs):
        if name == "nanobot.providers.github_copilot_provider":
            raise ImportError("missing")
        return real_import(name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", fake_import)

    with pytest.raises(WebUISettingsError) as exc:
        login_oauth_provider({"provider": ["github-copilot"]})

    assert "oauth_cli_kit not installed. Run: pip install oauth-cli-kit" in str(exc.value)


def test_provider_models_payload_fetches_openai_compatible_models(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.deepseek.api_key = "sk-test"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    def fake_get(url: str, **kwargs):
        assert url == "https://api.deepseek.com/models"
        assert kwargs["headers"]["Authorization"] == "Bearer sk-test"
        return httpx.Response(
            200,
            json={
                "data": [
                    {"id": "deepseek-chat", "owned_by": "deepseek"},
                    {"id": "deepseek-reasoner", "context_window": 65536},
                ]
            },
            request=httpx.Request("GET", url),
        )

    monkeypatch.setattr("nanobot.webui.settings_api.httpx.get", fake_get)

    payload = provider_models_payload({"provider": ["deepseek"]})

    assert payload["status"] == "available"
    assert payload["catalog_kind"] == "official"
    assert payload["model_count"] == 2
    assert payload["models"][0]["id"] == "deepseek-chat"
    assert payload["models"][1]["context_window"] == 65536


def test_provider_models_payload_returns_curated_openai_codex_models() -> None:
    payload = provider_models_payload({"provider": ["openai_codex"]})

    assert payload["status"] == "available"
    assert payload["catalog_kind"] == "builtin"
    assert payload["model_count"] == 7
    assert payload["models"][0] == {
        "id": "openai-codex/gpt-5.6-sol",
        "label": "GPT-5.6-Sol",
        "description": "Latest frontier agentic coding model.",
        "owned_by": "OpenAI Codex",
        "context_window": 372000,
    }
    assert [model["id"] for model in payload["models"][:3]] == [
        "openai-codex/gpt-5.6-sol",
        "openai-codex/gpt-5.6-terra",
        "openai-codex/gpt-5.6-luna",
    ]


def test_provider_models_payload_fetches_dynamic_custom_provider_models(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(_dynamic_provider_config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    def fake_get(url: str, **kwargs):
        assert url == f"{DYNAMIC_PROVIDER_API_BASE}/models"
        assert "Authorization" not in kwargs["headers"]
        return httpx.Response(
            200,
            json={"data": [{"id": "custom-gpt", "owned_by": "example"}]},
            request=httpx.Request("GET", url),
        )

    monkeypatch.setattr("nanobot.webui.settings_api.httpx.get", fake_get)

    payload = provider_models_payload({"provider": [DYNAMIC_PROVIDER_NAME]})

    assert payload["provider"] == DYNAMIC_PROVIDER_NAME
    assert payload["status"] == "available"
    assert payload["catalog_kind"] == "custom"
    assert payload["models"][0]["id"] == "custom-gpt"


@pytest.mark.parametrize(
    ("api_base", "expected_url"),
    [
        ("https://api.minimaxi.com/anthropic", "https://api.minimaxi.com/anthropic/v1/models"),
        ("https://api.minimaxi.com/anthropic/v1", "https://api.minimaxi.com/anthropic/v1/models"),
    ],
)
def test_provider_models_payload_fetches_minimax_anthropic_models(
    api_base: str,
    expected_url: str,
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.minimax_anthropic.api_key = "sk-test"
    config.providers.minimax_anthropic.api_base = api_base
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    def fake_get(url: str, **kwargs):
        assert url == expected_url
        assert kwargs["headers"]["X-Api-Key"] == "sk-test"
        assert "Authorization" not in kwargs["headers"]
        return httpx.Response(
            200,
            json={"data": [{"id": "MiniMax-M2.7-highspeed"}]},
            request=httpx.Request("GET", url),
        )

    monkeypatch.setattr("nanobot.webui.settings_api.httpx.get", fake_get)

    payload = provider_models_payload({"provider": ["minimax_anthropic"]})

    assert payload["status"] == "available"
    assert payload["catalog_kind"] == "official"
    assert payload["models"] == [
        {
            "id": "MiniMax-M2.7-highspeed",
            "label": None,
            "owned_by": None,
            "context_window": None,
        }
    ]


def test_provider_models_payload_requires_gateway_key(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = provider_models_payload({"provider": ["openrouter"]})

    assert payload["status"] == "not_configured"
    assert payload["catalog_kind"] == "catalog"
    assert payload["models"] == []


def test_model_catalog_kind_uses_provider_spec_metadata() -> None:
    assert _model_catalog_kind(find_by_name("skywork")) == "official"
    assert _model_catalog_kind(find_by_name("anthropic")) == "unsupported"
    assert _model_catalog_kind(find_by_name("openrouter")) == "catalog"
    assert _model_catalog_kind(find_by_name("openai_codex")) == "builtin"


def test_create_model_configuration_accepts_configured_oauth_provider(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
    monkeypatch.setattr(
        "nanobot.webui.settings_api._oauth_provider_status",
        lambda spec: {
            "configured": spec.name == "openai_codex",
            "account": "acct-test",
            "expires_at": 123,
            "login_supported": True,
        },
    )

    payload = create_model_configuration(
        {
            "label": ["Codex"],
            "provider": ["openai_codex"],
            "model": ["openai-codex/gpt-5.6-sol"],
        }
    )

    assert payload["agent"]["model_preset"] == "codex"
    saved = load_config(config_path)
    assert saved.model_presets["codex"].provider == "openai_codex"


# ---------------------------------------------------------------------------
# Azure OpenAI: settings contract for static-key vs AAD (DefaultAzureCredential)
# ---------------------------------------------------------------------------


def test_settings_payload_azure_openai_with_api_key_is_configured(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Static-key mode: api_key + api_base both set -> configured."""
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.azure_openai.api_key = "k"
    config.providers.azure_openai.api_base = "https://r.openai.azure.com"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    azure = next(row for row in payload["providers"] if row["name"] == "azure_openai")

    assert azure["configured"] is True
    assert azure["api_key_required"] is False
    assert azure["auth_type"] == "api_key"
    assert azure["api_base"] == "https://r.openai.azure.com"


def test_settings_payload_azure_openai_aad_mode_is_configured(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """AAD mode: only api_base set (no api_key) -> still configured."""
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.azure_openai.api_base = "https://r.openai.azure.com"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    azure = next(row for row in payload["providers"] if row["name"] == "azure_openai")

    assert azure["configured"] is True
    assert azure["api_key_required"] is False
    assert azure["api_base"] == "https://r.openai.azure.com"
    assert azure["api_key_hint"] is None


def test_settings_payload_azure_openai_missing_base_not_configured(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """api_key alone (no api_base) is NOT a working config -> not configured."""
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.azure_openai.api_key = "k"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = settings_payload()
    azure = next(row for row in payload["providers"] if row["name"] == "azure_openai")

    assert azure["configured"] is False


def test_create_model_configuration_accepts_azure_openai_aad_mode(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Provider-validation accepts azure_openai with only api_base (AAD mode)."""
    config_path = tmp_path / "config.json"
    config = Config()
    config.providers.azure_openai.api_base = "https://r.openai.azure.com"
    save_config(config, config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    payload = create_model_configuration(
        {
            "label": ["Azure AAD"],
            "provider": ["azure_openai"],
            "model": ["my-deployment"],
        }
    )

    assert payload["agent"]["model_preset"] == "azure-aad"
    saved = load_config(config_path)
    assert saved.model_presets["azure-aad"].provider == "azure_openai"
    assert saved.model_presets["azure-aad"].model == "my-deployment"


def test_create_model_configuration_rejects_azure_openai_without_base(
    tmp_path,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """azure_openai without api_base must still be rejected as not configured."""
    config_path = tmp_path / "config.json"
    save_config(Config(), config_path)
    monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)

    with pytest.raises(WebUISettingsError, match="provider is not configured"):
        create_model_configuration(
            {
                "label": ["Azure"],
                "provider": ["azure_openai"],
                "model": ["my-deployment"],
            }
        )


def test_azure_openai_spec_no_longer_requires_api_key() -> None:
    """Contract guard: api_key is optional for azure_openai (AAD fallback)."""
    from nanobot.webui.settings_api import _provider_requires_api_key

    spec = find_by_name("azure_openai")
    assert spec is not None
    assert _provider_requires_api_key(spec) is False
