"""EvoMap SDK 单元测试"""
import json
import hashlib
import pytest
from unittest.mock import patch, MagicMock

from evomap_sdk.core.config import EvoMapConfig
from evomap_sdk.core.serialize import compute_asset_id, canonical_json
from evomap_sdk.core.validation import validate_bundle, lint_bundle
from evomap_sdk.errors import (
    EvoMapError, AuthError, RateLimitError,
    ValidationError, DuplicateError
)
from evomap_sdk.builder import BundleBuilder
from evomap_sdk.protocol.client import EvoMapClient


# ── Config Tests ──────────────────────────────────────────────────

class TestEvoMapConfig:
    def test_from_params(self):
        cfg = EvoMapConfig(node_id='test_id', node_secret='test_secret')
        assert cfg.node_id == 'test_id'
        assert cfg.node_secret == 'test_secret'
        assert cfg.hub_url == 'https://evomap.ai'

    def test_custom_hub_url(self):
        cfg = EvoMapConfig(
            node_id='id', node_secret='s',
            hub_url='https://custom.hub.com/'
        )
        assert cfg.hub_url == 'https://custom.hub.com'  # trailing slash stripped

    def test_explicit_params_override_files(self):
        """Explicit params should override env/file config"""
        cfg = EvoMapConfig(node_id='explicit_id', node_secret='explicit_secret')
        assert cfg.node_id == 'explicit_id'
        assert cfg.node_secret == 'explicit_secret'

    @patch.dict('os.environ', {'A2A_NODE_ID': 'env_id', 'A2A_NODE_SECRET': 'env_secret'})
    def test_from_env(self):
        cfg = EvoMapConfig()
        assert cfg.node_id == 'env_id'
        assert cfg.node_secret == 'env_secret'


# ── Serialize Tests ───────────────────────────────────────────────

class TestSerialize:
    def test_canonical_json(self):
        data = {'b': 2, 'a': 1}
        result = canonical_json(data)
        assert result == '{"a":1,"b":2}'

    def test_canonical_json_utf8(self):
        data = {'text': '你好世界'}
        result = canonical_json(data)
        assert '你好世界' in result  # ensure_ascii=False

    def test_compute_asset_id(self):
        asset = {'type': 'Gene', 'category': 'optimize'}
        asset_id = compute_asset_id(asset)
        assert asset_id.startswith('sha256:')
        assert len(asset_id) == 71  # sha256: + 64 hex chars

    def test_compute_asset_id_excludes_existing_id(self):
        asset = {'type': 'Gene', 'asset_id': 'sha256:old_id'}
        new_id = compute_asset_id(asset)
        assert new_id != 'sha256:old_id'

    def test_compute_asset_id_deterministic(self):
        asset = {'type': 'Capsule', 'content': 'test'}
        id1 = compute_asset_id(asset)
        id2 = compute_asset_id(asset)
        assert id1 == id2


# ── Validation Tests ──────────────────────────────────────────────

class TestValidation:
    def _valid_bundle(self) -> dict:
        return {
            'gene': {
                'signals_match': ['sig1', 'sig2', 'sig3'],
                'strategy': ['step one with details', 'step two with details']
            },
            'capsule': {
                'content': '测试内容' * 60,  # >200 chars
                'confidence': 0.90
            },
            'EvolutionEvent': 'test_event'
        }

    def test_valid_bundle(self):
        is_valid, issues = validate_bundle(self._valid_bundle())
        assert is_valid, f"Unexpected issues: {issues}"

    def test_missing_signals(self):
        bundle = self._valid_bundle()
        bundle['gene']['signals_match'] = []
        is_valid, issues = validate_bundle(bundle)
        assert not is_valid
        assert any('signals_match' in i for i in issues)

    def test_short_signal(self):
        bundle = self._valid_bundle()
        bundle['gene']['signals_match'] = ['ab']  # <3 chars
        is_valid, issues = validate_bundle(bundle)
        assert not is_valid

    def test_short_strategy(self):
        bundle = self._valid_bundle()
        bundle['gene']['strategy'] = ['short']  # <15 chars
        is_valid, issues = validate_bundle(bundle)
        assert not is_valid

    def test_short_content(self):
        bundle = self._valid_bundle()
        bundle['capsule']['content'] = '短'
        is_valid, issues = validate_bundle(bundle)
        assert not is_valid

    def test_code_snippet_quarantine(self):
        bundle = self._valid_bundle()
        bundle['capsule']['code_snippet'] = 'def foo():\n    return 42\n' * 3  # >50 chars
        is_valid, issues = validate_bundle(bundle)
        assert not is_valid
        assert any('quarantine' in i for i in issues)

    def test_low_confidence_warning(self):
        bundle = self._valid_bundle()
        bundle['capsule']['confidence'] = 0.50
        is_valid, issues = validate_bundle(bundle)
        # confidence outside range is warning, not error
        assert any('confidence' in i for i in issues)

    def test_missing_evolution_event_warning(self):
        bundle = self._valid_bundle()
        del bundle['EvolutionEvent']
        is_valid, issues = validate_bundle(bundle)
        # EvolutionEvent is recommended, not required
        assert any('EvolutionEvent' in i for i in issues)

    def test_lint_returns_structured(self):
        bundle = self._valid_bundle()
        bundle['capsule']['confidence'] = 0.50
        issues = lint_bundle(bundle)
        assert isinstance(issues, list)
        assert all('severity' in i for i in issues)


# ── Builder Tests ─────────────────────────────────────────────────

class TestBundleBuilder:
    def test_basic_build(self):
        bundle = (BundleBuilder()
                  .topic("test_topic")
                  .signals("sig1", "sig2", "sig3")
                  .strategy("step one description", "step two description")
                  .capsule("测试内容" * 60, confidence=0.91)
                  .event("test_event")
                  .build())

        assert bundle['topic'] == 'test_topic'
        assert len(bundle['gene']['signals_match']) == 3
        assert len(bundle['gene']['strategy']) == 2
        assert bundle['capsule']['confidence'] == 0.91

    def test_missing_topic_raises(self):
        with pytest.raises(ValueError, match="topic"):
            (BundleBuilder()
             .signals("a", "b")
             .strategy("s1", "s2")
             .capsule("x" * 300)
             .build())

    def test_short_signals_auto_fixed(self):
        bundle = (BundleBuilder()
                  .topic("t")
                  .signals("ab", "cd", "ef")  # all <3 chars
                  .strategy("step one detail", "step two detail")
                  .capsule("x" * 300)
                  .build())
        # auto_fix pads short signals
        for sig in bundle['gene']['signals_match']:
            assert len(sig) >= 3

    def test_short_strategy_auto_fixed(self):
        bundle = (BundleBuilder()
                  .topic("t")
                  .signals("a1", "b2", "c3")
                  .strategy("short", "tiny")
                  .capsule("x" * 300)
                  .build())
        # auto_fix pads short strategy steps
        for step in bundle['gene']['strategy']:
            assert len(step) >= 15

    def test_default_event_generated(self):
        bundle = (BundleBuilder()
                  .topic("t")
                  .signals("a", "b", "c")
                  .strategy("s1", "s2")
                  .capsule("x" * 300)
                  .build())
        assert 'EvolutionEvent' in bundle

    def test_blast_radius(self):
        bundle = (BundleBuilder()
                  .topic("t")
                  .signals("a", "b", "c")
                  .strategy("s1", "s2")
                  .capsule("x" * 300)
                  .blast_radius(5)
                  .build())
        assert bundle['capsule']['blast_radius'] == 5


# ── Client Tests ──────────────────────────────────────────────────

class TestEvoMapClient:
    @patch('evomap_sdk.protocol.client.EvoMapConfig')
    def test_init_with_default_config(self, mock_config):
        mock_config.return_value = EvoMapConfig(
            node_id='test', node_secret='secret'
        )
        client = EvoMapClient()
        assert client.config.node_id == 'test'

    def test_bundle_to_assets(self):
        cfg = EvoMapConfig(node_id='test', node_secret='secret')
        client = EvoMapClient(config=cfg)

        bundle = {
            'topic': 'test',
            'gene': {
                'signals_match': ['sig1', 'sig2'],
                'strategy': ['step one detail', 'step two detail'],
                'category': 'repair'
            },
            'capsule': {
                'content': 'x' * 300,
                'confidence': 0.89,
                'summary': 'test summary',
                'outcome': {'status': 'success', 'score': 0.89}
            },
            'evolution_event': {
                'intent': 'repair',
                'description': 'test event',
                'outcome': {'status': 'success'}
            }
        }

        assets = client._bundle_to_assets(bundle)
        assert len(assets) == 3
        assert assets[0]['type'] == 'Gene'
        assert assets[1]['type'] == 'Capsule'
        assert assets[2]['type'] == 'EvolutionEvent'
        # Each asset should have asset_id
        for a in assets:
            assert 'asset_id' in a
            assert a['asset_id'].startswith('sha256:')

    def test_handle_response_200(self):
        cfg = EvoMapConfig(node_id='test', node_secret='secret')
        client = EvoMapClient(config=cfg)

        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_resp.json.return_value = {'status': 'ok'}

        result = client._handle_response(mock_resp, 'test')
        assert result == {'status': 'ok'}

    def test_handle_response_401(self):
        cfg = EvoMapConfig(node_id='test', node_secret='secret')
        client = EvoMapClient(config=cfg)

        mock_resp = MagicMock()
        mock_resp.status_code = 401

        with pytest.raises(AuthError):
            client._handle_response(mock_resp, 'test')

    def test_handle_response_429(self):
        cfg = EvoMapConfig(node_id='test', node_secret='secret')
        client = EvoMapClient(config=cfg)

        mock_resp = MagicMock()
        mock_resp.status_code = 429
        mock_resp.headers = {'Retry-After': '65', 'content-type': 'application/json'}
        mock_resp.json.return_value = {'retry_after_ms': 65000}

        with pytest.raises(RateLimitError) as exc_info:
            client._handle_response(mock_resp, 'test')
        assert exc_info.value.retry_after == 65

    def test_handle_response_409(self):
        cfg = EvoMapConfig(node_id='test', node_secret='secret')
        client = EvoMapClient(config=cfg)

        mock_resp = MagicMock()
        mock_resp.status_code = 409

        with pytest.raises(DuplicateError):
            client._handle_response(mock_resp, 'test')


# ── Error Tests ───────────────────────────────────────────────────

class TestErrors:
    def test_base_error(self):
        err = EvoMapError("test error", status_code=500)
        assert str(err) == "test error"
        assert err.status_code == 500

    def test_rate_limit_error(self):
        err = RateLimitError("limited", retry_after=120)
        assert err.retry_after == 120

    def test_error_hierarchy(self):
        assert issubclass(AuthError, EvoMapError)
        assert issubclass(RateLimitError, EvoMapError)
        assert issubclass(ValidationError, EvoMapError)
        assert issubclass(DuplicateError, EvoMapError)
