"""
tests/test_mcp_router.py
─────────────────────────────────────────────────────────────────
Operation Ollama-First v5.0 / Phase 10.5 — MCP Router unit tests

驗證面：
  T1. flag OFF → 直接回 success=False（不打 HTTP）
  T2. flag ON + tool 不在白名單 → 回 success=False
  T3. flag ON + 正常 → HTTP call + cache 命中第二次
  T4. flag ON + HTTP timeout → success=False + error 含 timeout
  T5. flag ON + HTTP 500 → success=False + error 含 HTTP 500
  T6. cache key 排序穩定（相同 args 不同 dict 順序 → 同 key）
  T7. health_check 全失敗時所有 server 回 False

紀律：
  - 不打真實 MCP server（全 mock requests.post / requests.get）
  - 不寫 mcp_calls 表（fire-and-forget thread 不影響 test）
"""

import json
from unittest.mock import patch, MagicMock

import pytest


@pytest.fixture(autouse=True)
def _reset_state(monkeypatch):
    """每 test 清 env + memory cache"""
    monkeypatch.delenv('MCP_ROUTER_ENABLED', raising=False)
    import services.mcp_router as mr
    mr._memory_cache.clear()
    yield
    mr._memory_cache.clear()


# ═══════════════════════════════════════════════════════════════════════════
# T1: flag OFF
# ═══════════════════════════════════════════════════════════════════════════

def test_flag_off_returns_failure_without_http(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'false')
    from services.mcp_router import mcp_router

    with patch('services.mcp_router.requests.post') as mock_post:
        result = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': 'test'}, caller='mcp_collector',
        )

    assert result.success is False
    assert result.status == 'error'
    assert 'MCP_ROUTER_ENABLED=false' in (result.error or '')
    mock_post.assert_not_called()


# ═══════════════════════════════════════════════════════════════════════════
# T2: flag ON + tool 不在白名單
# ═══════════════════════════════════════════════════════════════════════════

def test_unauthorized_caller_tool_combo_rejected(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    with patch('services.mcp_router.requests.post') as mock_post:
        # mcp_collector 不允許 postgres
        result = mcp_router.call(
            server='postgres', tool='query',
            args={'sql': 'SELECT 1'}, caller='mcp_collector',
        )

    assert result.success is False
    assert result.status == 'error'
    assert 'tool not in registry' in (result.error or '')
    mock_post.assert_not_called()


def test_filesystem_registry_is_read_only(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    fake_resp = MagicMock(status_code=200)
    fake_resp.json.return_value = {'content': 'ok'}
    fake_resp.text = '{"content": "ok"}'

    with patch('services.mcp_router.requests.post', return_value=fake_resp) as mock_post:
        allowed = mcp_router.call(
            server='filesystem', tool='read_file',
            args={'path': '/logs/system.log'}, caller='ops_diagnostics',
        )
        denied = mcp_router.call(
            server='filesystem', tool='write_file',
            args={'path': '/data/x', 'content': 'nope'}, caller='ops_diagnostics',
        )

    assert allowed.success is True
    assert denied.success is False
    assert 'tool not in registry' in (denied.error or '')
    mock_post.assert_called_once()
    assert mock_post.call_args.args[0].endswith('/tools/read_file')


# ═══════════════════════════════════════════════════════════════════════════
# T3: flag ON + 正常 + cache
# ═══════════════════════════════════════════════════════════════════════════

def test_successful_call_and_cache_hit(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    import services.mcp_router as mr
    from services.mcp_router import mcp_router

    written_statuses = []
    monkeypatch.setattr(
        mr,
        '_async_write_mcp_call',
        lambda caller, server, tool, args, result, request_id=None: written_statuses.append(result.status),
    )

    fake_resp = MagicMock(status_code=200)
    fake_resp.json.return_value = {'results': [{'title': '商品 A', 'content': '熱銷'}]}
    fake_resp.text = '{"ok": true}'

    with patch('services.mcp_router.requests.post', return_value=fake_resp) as mock_post:
        # 第一次：HTTP call
        r1 = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': '母親節'}, caller='mcp_collector',
        )
        assert r1.success is True
        assert r1.cache_hit is False
        assert r1.status == 'ok'
        assert r1.data['results'][0]['title'] == '商品 A'
        assert mock_post.call_count == 1

        # 第二次：cache 命中
        r2 = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': '母親節'}, caller='mcp_collector',
        )
        assert r2.success is True
        assert r2.cache_hit is True
        assert r2.status == 'cache_only'
        assert mock_post.call_count == 1  # 沒再打 HTTP
        assert written_statuses == ['ok', 'cache_only']


# ═══════════════════════════════════════════════════════════════════════════
# T4: HTTP timeout
# ═══════════════════════════════════════════════════════════════════════════

def test_http_timeout_returns_failure(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    import requests as _r
    from services.mcp_router import mcp_router

    with patch('services.mcp_router.requests.post', side_effect=_r.Timeout('30s')):
        result = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': 'x'}, caller='mcp_collector',
        )

    assert result.success is False
    assert result.status == 'timeout'
    assert 'timeout' in (result.error or '').lower()


# ═══════════════════════════════════════════════════════════════════════════
# T5: HTTP 500
# ═══════════════════════════════════════════════════════════════════════════

def test_http_500_returns_failure(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    fake_resp = MagicMock(status_code=500)
    fake_resp.text = 'Internal Server Error'

    with patch('services.mcp_router.requests.post', return_value=fake_resp):
        result = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': 'x'}, caller='mcp_collector',
        )

    assert result.success is False
    assert result.status == 'error'
    assert 'HTTP 500' in (result.error or '')


def test_http_429_records_rate_limited_status(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    fake_resp = MagicMock(status_code=429)
    fake_resp.text = 'Too Many Requests'

    with patch('services.mcp_router.requests.post', return_value=fake_resp):
        result = mcp_router.call(
            server='omnisearch', tool='tavily_search',
            args={'query': 'x'}, caller='mcp_collector',
        )

    assert result.success is False
    assert result.status == 'rate_limited'
    assert 'HTTP 429' in (result.error or '')


# ═══════════════════════════════════════════════════════════════════════════
# T6: cache key 穩定性
# ═══════════════════════════════════════════════════════════════════════════

def test_cache_key_sort_stable():
    from services.mcp_router import _cache_key
    k1 = _cache_key('omnisearch', 'tavily_search', {'a': 1, 'b': 2, 'c': 3})
    k2 = _cache_key('omnisearch', 'tavily_search', {'c': 3, 'a': 1, 'b': 2})
    assert k1 == k2  # dict 順序不影響 cache key


# ═══════════════════════════════════════════════════════════════════════════
# T7: health_check 全失敗
# ═══════════════════════════════════════════════════════════════════════════

def test_health_check_all_fail_returns_all_false(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    with patch('services.mcp_router.requests.get', side_effect=Exception('connection refused')):
        results = mcp_router.health_check()

    assert results == {'postgres': False, 'firecrawl': False,
                       'omnisearch': False, 'filesystem': False}


# ═══════════════════════════════════════════════════════════════════════════
# T8: 未知 server 直接拒絕
# ═══════════════════════════════════════════════════════════════════════════

def test_unknown_server_rejected(monkeypatch):
    monkeypatch.setenv('MCP_ROUTER_ENABLED', 'true')
    from services.mcp_router import mcp_router

    with patch('services.mcp_router.requests.post') as mock_post:
        # 統帥的 caller 在 registry，但 server 名拼錯
        result = mcp_router.call(
            server='omnisearch_wrong', tool='tavily_search',
            args={'query': 'x'}, caller='mcp_collector',
        )

    # 注意：白名單檢查在前，會先回 'tool not in registry'（因為 omnisearch_wrong 不在 registry）
    assert result.success is False
    assert result.status == 'error'
    mock_post.assert_not_called()
