"""
Tests for resolve_model_provider() model routing logic.
Verifies that model IDs are correctly resolved to (model, provider, base_url)
tuples for different provider configurations.
"""
import api.config as config
import sys
import types


def _resolve_with_config(model_id, provider=None, base_url=None, default=None):
    """Helper: temporarily set config.cfg model section, call resolve, restore."""
    old_cfg = dict(config.cfg)
    model_cfg = {}
    if provider:
        model_cfg['provider'] = provider
    if base_url:
        model_cfg['base_url'] = base_url
    if default:
        model_cfg['default'] = default
    config.cfg['model'] = model_cfg if model_cfg else {}
    try:
        return config.resolve_model_provider(model_id)
    finally:
        config.cfg.clear()
        config.cfg.update(old_cfg)


# ── OpenRouter prefix handling ────────────────────────────────────────────

def test_openrouter_free_keeps_full_path():
    """openrouter/free must NOT be stripped to 'free' when provider is openrouter."""
    model, provider, base_url = _resolve_with_config(
        'openrouter/free', provider='openrouter',
        base_url='https://openrouter.ai/api/v1',
    )
    assert model == 'openrouter/free', f"Expected 'openrouter/free', got '{model}'"
    assert provider == 'openrouter'


def test_openrouter_model_with_provider_prefix():
    """anthropic/claude-sonnet-4.6 via openrouter keeps full path."""
    model, provider, base_url = _resolve_with_config(
        'anthropic/claude-sonnet-4.6', provider='openrouter',
        base_url='https://openrouter.ai/api/v1',
    )
    assert model == 'anthropic/claude-sonnet-4.6'
    assert provider == 'openrouter'


# ── Direct provider prefix stripping ─────────────────────────────────────

def test_anthropic_prefix_stripped_for_direct_api():
    """anthropic/claude-sonnet-4.6 strips prefix when provider is anthropic."""
    model, provider, base_url = _resolve_with_config(
        'anthropic/claude-sonnet-4.6', provider='anthropic',
    )
    assert model == 'claude-sonnet-4.6'
    assert provider == 'anthropic'


def test_openai_prefix_stripped_for_direct_api():
    """openai/gpt-5.4-mini strips prefix when provider is openai."""
    model, provider, base_url = _resolve_with_config(
        'openai/gpt-5.4-mini', provider='openai',
    )
    assert model == 'gpt-5.4-mini'
    assert provider == 'openai'


# ── Cross-provider routing ───────────────────────────────────────────────

def test_cross_provider_routes_through_openrouter():
    """Picking openai model when config is anthropic routes via openrouter."""
    model, provider, base_url = _resolve_with_config(
        'openai/gpt-5.4-mini', provider='anthropic',
    )
    assert model == 'openai/gpt-5.4-mini'
    assert provider == 'openrouter'
    assert base_url is None  # openrouter uses its own endpoint


# ── Bare model names ─────────────────────────────────────────────────────

def test_bare_model_uses_config_provider():
    """A model name without / uses the config provider and base_url."""
    model, provider, base_url = _resolve_with_config(
        'gemma-4-26B', provider='custom',
        base_url='http://192.168.1.160:4000',
    )
    assert model == 'gemma-4-26B'
    assert provider == 'custom'
    assert base_url == 'http://192.168.1.160:4000'


def test_empty_model_returns_config_defaults():
    """Empty model string returns config provider and base_url."""
    model, provider, base_url = _resolve_with_config(
        '', provider='anthropic',
    )
    assert model == ''
    assert provider == 'anthropic'


# ── @provider:model hint routing (Issue #138 v2) ────────────────────────

def test_provider_hint_routes_to_specific_provider():
    """@minimax:MiniMax-M2.7 routes to minimax provider directly."""
    model, provider, base_url = _resolve_with_config(
        '@minimax:MiniMax-M2.7', provider='anthropic',
    )
    assert model == 'MiniMax-M2.7'
    assert provider == 'minimax'
    assert base_url is None  # resolve_runtime_provider will fill this


def test_provider_hint_zai():
    """@zai:GLM-5 routes to zai provider directly."""
    model, provider, base_url = _resolve_with_config(
        '@zai:GLM-5', provider='openai',
    )
    assert model == 'GLM-5'
    assert provider == 'zai'


def test_provider_hint_deepseek():
    """@deepseek:deepseek-chat routes to deepseek provider."""
    model, provider, base_url = _resolve_with_config(
        '@deepseek:deepseek-chat', provider='anthropic',
    )
    assert model == 'deepseek-chat'
    assert provider == 'deepseek'


def test_slash_prefix_non_default_still_routes_openrouter():
    """minimax/MiniMax-M2.7 (old format) still routes through openrouter."""
    model, provider, base_url = _resolve_with_config(
        'minimax/MiniMax-M2.7', provider='anthropic',
    )
    assert model == 'minimax/MiniMax-M2.7'
    assert provider == 'openrouter'


# ── get_available_models() @provider: hint behaviour ──────────────────────

def _available_models_with_provider(provider):
    """Helper: temporarily set active_provider in config."""
    old_cfg = dict(config.cfg)
    old_repo_env = config._load_repo_env
    config.cfg['model'] = {'provider': provider}
    config._load_repo_env = lambda: {}
    try:
        return config.get_available_models()
    finally:
        config._load_repo_env = old_repo_env
        config.cfg.clear()
        config.cfg.update(old_cfg)


def _available_models_with_detection(
    monkeypatch,
    model_cfg,
    env=None,
    auth_store=None,
    openrouter_models=None,
    codex_models=None,
    custom_providers=None,
    custom_catalogs=None,
):
    """Helper: isolate provider discovery from the real machine state."""
    old_cfg = dict(config.cfg)
    config.cfg['model'] = dict(model_cfg)
    config.cfg['custom_providers'] = list(custom_providers or [])

    fake_pkg = types.ModuleType('hermes_cli')
    fake_pkg.__path__ = []
    fake_models = types.ModuleType('hermes_cli.models')
    fake_models.list_available_providers = lambda: []
    fake_auth = types.ModuleType('hermes_cli.auth')
    fake_auth.get_auth_status = lambda _pid: {}

    monkeypatch.setitem(sys.modules, 'hermes_cli', fake_pkg)
    monkeypatch.setitem(sys.modules, 'hermes_cli.models', fake_models)
    monkeypatch.setitem(sys.modules, 'hermes_cli.auth', fake_auth)
    monkeypatch.setattr(config, '_load_profile_env', lambda: dict(env or {}))
    monkeypatch.setattr(config, '_load_auth_store', lambda: dict(auth_store or {}))
    monkeypatch.setattr(config, '_load_repo_env', lambda: {})
    if openrouter_models is None:
        openrouter_models = [{'id': m['id'], 'label': m['label']} for m in config._OPENROUTER_FREE_MODELS]
    if codex_models is None:
        codex_models = list(config._PROVIDER_MODELS.get('openai-codex', []))
    if custom_catalogs is None:
        custom_catalogs = {}
    monkeypatch.setattr(config, '_discover_openrouter_free_models', lambda all_env=None: list(openrouter_models))
    monkeypatch.setattr(config, '_discover_codex_model_entries', lambda: list(codex_models))
    monkeypatch.setattr(config, '_discover_named_custom_provider_catalogs', lambda all_env=None: dict(custom_catalogs))

    try:
        return config.get_available_models()
    finally:
        config.cfg.clear()
        config.cfg.update(old_cfg)


def test_non_default_provider_models_use_hint_prefix():
    """With anthropic as default, minimax model IDs should use @minimax: prefix."""
    result = _available_models_with_provider('anthropic')
    groups = {g['provider']: g['models'] for g in result['groups']}
    if 'MiniMax' in groups:
        for m in groups['MiniMax']:
            assert m['id'].startswith('@minimax:'), (
                f"Expected @minimax: prefix, got: {m['id']!r}"
            )


def test_no_duplicate_when_default_model_is_prefixed(monkeypatch):
    """Issue #147 Bug 2: 'anthropic/claude-opus-4.6' as default_model must not
    inject a duplicate alongside the existing bare 'claude-opus-4.6' entry in
    the same provider group."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'anthropic',
            'default': 'anthropic/claude-opus-4.6',
        },
    )
    groups = {g['provider_id']: g['models'] for g in result['groups']}
    norm = lambda mid: mid.split('/', 1)[-1] if '/' in mid else mid
    bare_ids = [norm(m['id']) for m in groups['anthropic']]
    duplicates = [mid for mid in set(bare_ids) if bare_ids.count(mid) > 1]
    assert not duplicates, (
        "Anthropic group has duplicate models after normalization: "
        f"{duplicates}\nFull group: {[m['id'] for m in groups['anthropic']]}"
    )


def test_default_provider_models_not_prefixed():
    """The active provider's models remain bare (no @prefix added)."""
    import api.config as _cfg
    raw_anthropic_ids = {m['id'] for m in _cfg._PROVIDER_MODELS.get('anthropic', [])}
    result = _available_models_with_provider('anthropic')
    groups = {g['provider']: g['models'] for g in result['groups']}
    if 'Anthropic' in groups:
        returned_ids = {m['id'] for m in groups['Anthropic']}
        for bare_id in raw_anthropic_ids:
            assert bare_id in returned_ids, (
                f"_PROVIDER_MODELS entry '{bare_id}' is missing from the Anthropic group"
            )


def test_detects_codex_oauth_and_custom_env_aliases(monkeypatch):
    """OpenAI Codex OAuth and the local custom env aliases should still surface."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'qwen/qwen3.6-plus:free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={
            'OPENROUTER_API_KEY': 'or-key',
            'CUSTOM_API_MOONSHOT_AI_API_KEY': 'moonshot-key',
            'CUSTOM_API_DEEPSEEK_COM_API_KEY': 'deepseek-key',
        },
        auth_store={
            'providers': {
                'openai-codex': {'tokens': {'access_token': 'codex-token'}},
            }
        },
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    assert {'openai-codex', 'kimi-coding', 'deepseek', 'openrouter'}.issubset(groups.keys())
    assert any(m['id'].startswith('@openai-codex:') for m in groups['openai-codex'])
    assert any(m['id'].startswith('@kimi-coding:') for m in groups['kimi-coding'])
    assert any(m['id'].startswith('@deepseek:') for m in groups['deepseek'])


def test_openrouter_group_stays_free_and_no_duplicate_custom_catalog(monkeypatch):
    """Standard OpenRouter config should not produce a second giant 'custom' group."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'qwen/qwen3.6-plus:free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={'OPENROUTER_API_KEY': 'or-key'},
    )

    groups = {g['provider_id']: g for g in result['groups']}
    assert 'custom' not in groups
    openrouter_ids = {m['id'] for m in groups['openrouter']['models']}
    assert 'deepseek-chat' not in openrouter_ids
    assert 'deepseek-reasoner' not in openrouter_ids
    assert 'openrouter/free' in openrouter_ids


def test_allowed_providers_filters_groups(monkeypatch):
    """Repo/env allowlist should hide providers outside the requested set."""
    monkeypatch.setenv('HERMES_WEBUI_ALLOWED_PROVIDERS', 'openai-codex,kimi-coding,deepseek,openrouter')
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'qwen/qwen3.6-plus:free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={
            'OPENROUTER_API_KEY': 'or-key',
            'CUSTOM_API_MOONSHOT_AI_API_KEY': 'moonshot-key',
            'CUSTOM_API_DEEPSEEK_COM_API_KEY': 'deepseek-key',
            'ANTHROPIC_API_KEY': 'anthropic-key',
        },
        auth_store={
            'providers': {
                'openai-codex': {'tokens': {'access_token': 'codex-token'}},
            }
        },
    )
    provider_ids = {g['provider_id'] for g in result['groups']}
    assert provider_ids == {'openrouter', 'openai-codex', 'kimi-coding', 'deepseek'}


def test_hidden_providers_filters_groups(monkeypatch):
    """Hidden-provider config should remove only the requested providers."""
    monkeypatch.delenv('HERMES_WEBUI_ALLOWED_PROVIDERS', raising=False)
    monkeypatch.setenv('HERMES_WEBUI_HIDDEN_PROVIDERS', 'anthropic')
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'qwen/qwen3.6-plus:free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={
            'OPENROUTER_API_KEY': 'or-key',
            'CUSTOM_API_MOONSHOT_AI_API_KEY': 'moonshot-key',
            'CUSTOM_API_DEEPSEEK_COM_API_KEY': 'deepseek-key',
            'ANTHROPIC_API_KEY': 'anthropic-key',
        },
        auth_store={
            'providers': {
                'openai-codex': {'tokens': {'access_token': 'codex-token'}},
            }
        },
    )
    provider_ids = {g['provider_id'] for g in result['groups']}
    assert 'anthropic' not in provider_ids
    assert {'openrouter', 'openai-codex', 'kimi-coding', 'deepseek'}.issubset(provider_ids)


def test_codex_group_uses_dynamic_catalog(monkeypatch):
    """OpenAI Codex should expose the live/discovered catalog instead of only the old static list."""
    result = _available_models_with_detection(
        monkeypatch,
        {'provider': 'openrouter', 'default': 'qwen/qwen3.6-plus:free'},
        auth_store={'providers': {'openai-codex': {'tokens': {'access_token': 'codex-token'}}}},
        codex_models=[
            {'id': 'gpt-5.4', 'label': 'GPT-5.4'},
            {'id': 'gpt-5.4-mini', 'label': 'GPT-5.4 Mini'},
        ],
    )
    groups = {g['provider_id']: g['models'] for g in result['groups']}
    codex_ids = {m['id'] for m in groups['openai-codex']}
    assert '@openai-codex:gpt-5.4' in codex_ids
    assert '@openai-codex:gpt-5.4-mini' in codex_ids


def test_openrouter_group_uses_dynamic_free_catalog(monkeypatch):
    """OpenRouter group should come from the live/discovered free-text catalog."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'openrouter/free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={'OPENROUTER_API_KEY': 'or-key'},
        openrouter_models=[
            {'id': 'openrouter/free', 'label': 'Auto (Free Router)'},
            {'id': 'openai/gpt-oss-120b:free', 'label': 'OpenAI: gpt-oss-120b (free)'},
            {'id': 'qwen/qwen3-coder:free', 'label': 'Qwen: Qwen3 Coder 480B A35B (free)'},
        ],
    )
    groups = {g['provider_id']: g['models'] for g in result['groups']}
    openrouter_ids = [m['id'] for m in groups['openrouter']]
    assert openrouter_ids == [
        'openrouter/free',
        'openai/gpt-oss-120b:free',
        'qwen/qwen3-coder:free',
    ]


def test_named_custom_provider_group_is_exposed_and_prefixed(monkeypatch):
    """Named custom providers from config.yaml should appear with provider hints when inactive."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'openrouter',
            'default': 'openrouter/free',
            'base_url': 'https://openrouter.ai/api/v1',
        },
        env={'OPENROUTER_API_KEY': 'or-key'},
        custom_providers=[
            {'name': 'blockrun', 'base_url': 'http://127.0.0.1:8402/v1', 'api_mode': 'chat_completions'},
        ],
        custom_catalogs={
            'blockrun': [
                {'id': 'auto', 'label': 'auto'},
                {'id': 'openai/gpt-5.4', 'label': 'openai/gpt-5.4'},
            ],
        },
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    assert 'blockrun' in groups
    blockrun_ids = [m['id'] for m in groups['blockrun']]
    assert blockrun_ids == ['@blockrun:auto', '@blockrun:openai/gpt-5.4']


def test_named_custom_provider_active_keeps_raw_model_ids(monkeypatch):
    """When a named custom provider is active, its catalog should remain bare."""
    result = _available_models_with_detection(
        monkeypatch,
        {
            'provider': 'blockrun',
            'default': 'auto',
            'base_url': 'http://127.0.0.1:8402/v1',
        },
        custom_providers=[
            {'name': 'blockrun', 'base_url': 'http://127.0.0.1:8402/v1', 'api_mode': 'chat_completions'},
        ],
        custom_catalogs={
            'blockrun': [
                {'id': 'auto', 'label': 'auto'},
                {'id': 'openai/gpt-5.4', 'label': 'openai/gpt-5.4'},
            ],
        },
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    assert [m['id'] for m in groups['blockrun']] == ['auto', 'openai/gpt-5.4']


def test_named_custom_provider_keeps_slash_model_on_active_provider():
    """Slash-form models on a named custom provider should not be rerouted to OpenRouter."""
    old_cfg = dict(config.cfg)
    config.cfg['model'] = {
        'provider': 'blockrun',
        'base_url': 'http://127.0.0.1:8402/v1',
    }
    config.cfg['custom_providers'] = [
        {'name': 'blockrun', 'base_url': 'http://127.0.0.1:8402/v1', 'api_mode': 'chat_completions'},
    ]
    try:
        model, provider, base_url = config.resolve_model_provider('openai/gpt-5.4')
        assert model == 'openai/gpt-5.4'
        assert provider == 'blockrun'
        assert base_url == 'http://127.0.0.1:8402/v1'
    finally:
        config.cfg.clear()
        config.cfg.update(old_cfg)


def test_codex_reasoning_metadata_tracks_supported_efforts(monkeypatch):
    """GPT-5 Codex models should expose only the effort values Hermes supports."""
    result = _available_models_with_detection(
        monkeypatch,
        {'provider': 'openrouter', 'default': 'openrouter/free'},
        auth_store={'providers': {'openai-codex': {'tokens': {'access_token': 'codex-token'}}}},
        codex_models=[
            {'id': 'gpt-5.4', 'label': 'GPT-5.4'},
            {'id': 'gpt-4o', 'label': 'GPT-4o'},
        ],
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    gpt54 = next(m for m in groups['openai-codex'] if m['id'] == '@openai-codex:gpt-5.4')
    gpt4o = next(m for m in groups['openai-codex'] if m['id'] == '@openai-codex:gpt-4o')

    assert gpt54['reasoning']['mode'] == 'effort'
    assert gpt54['reasoning']['options'] == ['', 'none', 'minimal', 'low', 'medium', 'high']
    assert gpt54['reasoning']['disabled'] is False

    assert gpt4o['reasoning']['mode'] == 'unsupported'
    assert gpt4o['reasoning']['disabled'] is True


def test_deepseek_reasoning_is_model_controlled(monkeypatch):
    """DeepSeek should disable the effort dropdown and point users to Chat vs Thinking models."""
    result = _available_models_with_detection(
        monkeypatch,
        {'provider': 'deepseek', 'default': 'deepseek-chat'},
        env={'CUSTOM_API_DEEPSEEK_COM_API_KEY': 'deepseek-key'},
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    reasoner = next(m for m in groups['deepseek'] if m['id'] == 'deepseek-reasoner')

    assert reasoner['reasoning']['mode'] == 'model'
    assert reasoner['reasoning']['options'] == ['']
    assert reasoner['reasoning']['disabled'] is True
    assert 'Chat and Thinking models' in reasoner['reasoning']['note']


def test_named_custom_provider_reasoning_metadata_uses_capabilities_boolean(monkeypatch):
    """Custom providers like Mistral should surface toggle-only reasoning when the catalog says so."""
    result = _available_models_with_detection(
        monkeypatch,
        {'provider': 'mistral', 'default': 'mistral-small-latest'},
        custom_providers=[
            {'name': 'mistral', 'base_url': 'https://api.mistral.ai/v1', 'api_mode': 'chat_completions'},
        ],
        custom_catalogs={
            'mistral': [
                {
                    'id': 'mistral-small-latest',
                    'label': 'mistral-small-latest',
                    'capabilities': {'completion_chat': True, 'reasoning': True},
                },
                {
                    'id': 'mistral-medium-latest',
                    'label': 'mistral-medium-latest',
                    'capabilities': {'completion_chat': True, 'reasoning': False},
                },
            ],
        },
    )

    groups = {g['provider_id']: g['models'] for g in result['groups']}
    small = next(m for m in groups['mistral'] if m['id'] == 'mistral-small-latest')
    medium = next(m for m in groups['mistral'] if m['id'] == 'mistral-medium-latest')

    assert small['reasoning']['mode'] == 'toggle'
    assert small['reasoning']['options'] == ['', 'none']
    assert small['reasoning']['disabled'] is False

    assert medium['reasoning']['mode'] == 'unsupported'
    assert medium['reasoning']['disabled'] is True
