# -*- coding: utf-8 -*-
"""
离线模式单元测试（PRD §3 离线部署方案）

覆盖：
  T1 utils/offline_config.py 的 is_offline_mode / reset_for_test
  T2 services/ai_service.build_offline_response
  T3 routes/troubleshoot.py 三个路由 + health 的离线分支
  回归：OFFLINE_MODE 未设置时行为不变（不调 AI）

隔离：本测试只动问题排查助手相关文件，不触碰 service_manage / service_monitor。
"""

import json

import pytest

import utils.offline_config as offline_config


# ============================================================
# Fixture：离线开关切换
# ============================================================

@pytest.fixture
def offline_on(monkeypatch):
    """开启离线模式"""
    monkeypatch.setenv('OFFLINE_MODE', 'true')
    offline_config.reset_for_test()
    yield
    offline_config.reset_for_test()


@pytest.fixture
def offline_off(monkeypatch):
    """关闭离线模式（在线）"""
    monkeypatch.delenv('OFFLINE_MODE', raising=False)
    offline_config.reset_for_test()
    yield
    offline_config.reset_for_test()


# ============================================================
# T1：offline_config
# ============================================================

class TestOfflineConfig:

    def test_offline_true(self, offline_on):
        assert offline_config.is_offline_mode() is True

    def test_offline_not_set_defaults_online(self, offline_off):
        assert offline_config.is_offline_mode() is False

    @pytest.mark.parametrize('val,expected', [
        ('true', True), ('TRUE', True), ('1', True), ('yes', True), ('YES', True),
        ('false', False), ('0', False), ('', False), ('random', False),
    ])
    def test_value_parsing(self, monkeypatch, val, expected):
        if val == '':
            monkeypatch.delenv('OFFLINE_MODE', raising=False)
        else:
            monkeypatch.setenv('OFFLINE_MODE', val)
        offline_config.reset_for_test()
        assert offline_config.is_offline_mode() is expected
        offline_config.reset_for_test()

    def test_cache_persists(self, monkeypatch):
        """首次读取后缓存，中途改环境变量不生效"""
        monkeypatch.setenv('OFFLINE_MODE', 'true')
        offline_config.reset_for_test()
        assert offline_config.is_offline_mode() is True
        # 中途改环境变量，缓存不变
        monkeypatch.setenv('OFFLINE_MODE', 'false')
        assert offline_config.is_offline_mode() is True
        offline_config.reset_for_test()


# ============================================================
# T2：build_offline_response
# ============================================================

class TestBuildOfflineResponse:

    def test_empty_cases(self):
        from services.ai_service import build_offline_response
        result = build_offline_response([])
        assert result['offline'] is True
        assert '未匹配到相关案例' in result['response']
        assert '离线模式' in result['response']

    def test_with_cases_raw_format(self):
        """原始格式：search engine 返回的结构（含 record）"""
        from services.ai_service import build_offline_response
        cases = [
            {
                'rank': 1, 'score': 0.92,
                'record': {
                    'project': '厦门银行',
                    'title': '门口屏 MQTT 绑定失败',
                    'phenomenon': '绑定时报 MQTT 连接错误',
                    'solution': '升级门口屏到 5.0+ 版本',
                    'file': 'record_001.md',
                },
            },
            {
                'rank': 2, 'score': 0.75,
                'record': {
                    'project': '展厅',
                    'title': '门口屏不显示会议信息',
                    'phenomenon': '会议开始后不显示主题',
                    'solution': '检查 EMQX 容器状态',
                    'file': 'record_002.md',
                },
            },
        ]
        result = build_offline_response(cases)
        assert result['offline'] is True
        text = result['response']
        assert '厦门银行' in text
        assert '门口屏 MQTT 绑定失败' in text
        assert '92%' in text  # 相似度格式化
        assert '升级门口屏到 5.0+ 版本' in text  # solution
        assert 'record_001.md' in text  # 参考文件
        assert '共匹配 2 个' in text

    def test_with_cases_formatted_format(self):
        """已格式化格式：前端 analyze 入参（无 solution，直接字段）"""
        from services.ai_service import build_offline_response
        cases = [
            {
                'rank': 1, 'score': 0.80,
                'project': '展厅', 'title': '测试问题', 'phenomenon': '现象A', 'file': 'r.md',
            },
        ]
        result = build_offline_response(cases)
        assert '展厅' in result['response']
        assert '80%' in result['response']
        # 无 solution 时不报错

    def test_max_five_cases(self):
        from services.ai_service import build_offline_response
        cases = [{'rank': i, 'score': 0.5, 'record': {'title': f'c{i}'}} for i in range(10)]
        result = build_offline_response(cases)
        # 最多展示 5 个案例标题（### 案例N），但总数标注为 10
        assert result['response'].count('### 案例') == 5
        assert '共匹配 10 个' in result['response']


# ============================================================
# T3：路由离线分支
# ============================================================

class TestRoutesOffline:

    def test_health_has_offline_field(self, client, offline_on):
        resp = client.get('/api/health')
        assert resp.status_code == 200
        data = resp.get_json()
        assert 'offline_mode' in data
        assert data['offline_mode'] is True

    def test_health_online_mode(self, client, offline_off):
        resp = client.get('/api/health')
        data = resp.get_json()
        assert data['offline_mode'] is False

    def test_troubleshoot_offline_skips_api(self, auth_client, offline_on, monkeypatch):
        """离线模式不调 Claude API（mock 抛异常验证不被调用）"""
        import services.ai_service as ai_service
        def _explode(*a, **kw):
            raise AssertionError("离线模式不应调用 Claude API")
        monkeypatch.setattr(ai_service, 'call_claude_api', _explode)

        resp = auth_client.post('/api/troubleshoot', json={
            'query': '门口屏MQTT绑定失败',
            'project_name': '',
        })
        assert resp.status_code == 200
        data = resp.get_json()
        assert data['success'] is True
        assert data.get('offline') is True
        assert data['api_time'] == 0.0
        assert len(data['matched_cases']) > 0
        assert '离线模式' in data['response']

    def test_troubleshoot_online_calls_api(self, auth_client, offline_off):
        """在线模式回归：调 AI（已被 conftest mock），返回正常分析"""
        resp = auth_client.post('/api/troubleshoot', json={
            'query': '门口屏MQTT绑定失败',
        })
        assert resp.status_code == 200
        data = resp.get_json()
        assert data['success'] is True
        # 在线模式无 offline 字段（或为 False），有 AI 响应
        assert data.get('offline') is not True
        assert 'response' in data

    def test_analyze_offline(self, auth_client, offline_on, monkeypatch):
        import services.ai_service as ai_service
        def _explode(*a, **kw):
            raise AssertionError("离线模式不应调用 Claude API")
        monkeypatch.setattr(ai_service, 'call_claude_api', _explode)

        resp = auth_client.post('/api/analyze', json={
            'query': '门口屏问题',
            'matched_cases': [
                {'rank': 1, 'score': 0.9, 'project': '厦门银行',
                 'title': 'MQTT失败', 'phenomenon': '绑定报错', 'file': 'r.md'},
            ],
        })
        assert resp.status_code == 200
        data = resp.get_json()
        assert data['success'] is True
        assert data.get('offline') is True
        assert '厦门银行' in data['response']

    def test_analyze_stream_offline_sse(self, auth_client, offline_on, monkeypatch):
        """离线流式仍走 SSE，单帧推送，含 offline 标记"""
        import services.ai_service as ai_service
        def _explode(*a, **kw):
            raise AssertionError("离线模式不应调用 Claude API")
        monkeypatch.setattr(ai_service, 'call_claude_api_stream', _explode)

        resp = auth_client.get('/api/analyze/stream?query=门口屏MQTT')
        assert resp.status_code == 200
        assert 'text/event-stream' in resp.content_type

        body = resp.get_data(as_text=True)
        # 至少有 start / matched_cases / chunk / done 四类事件
        assert '"type": "start"' in body
        assert '"type": "matched_cases"' in body
        assert '"type": "chunk"' in body
        assert '"type": "done"' in body
        # 离线标记
        assert '"offline": true' in body
        # 不应出现 error
        assert '"type": "error"' not in body
