"""
Unit tests for exchange_validation module.

Tests cover:
- Exchange extraction from strategy data
- Mode-specific validation (backtest, paper, live)
- Exchange name canonicalization
- Error code generation
- Edge cases (empty data, EDM unavailable, etc.)
"""

import pytest
import sys
import os

# Add src to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))

from exchange_validation import (
    canonicalize_exchange,
    extract_required_exchanges,
    validate_for_backtest,
    validate_for_paper,
    validate_for_live,
    validate_exchange_requirements,
    ValidationErrorCode,
    ExchangeValidationResult,
)


class TestCanonicalizeExchange:
    """Tests for exchange name canonicalization."""

    def test_lowercase_normalization(self):
        """Test that exchange names are normalized to lowercase."""
        assert canonicalize_exchange('BINANCE') == 'binance'
        assert canonicalize_exchange('Binance') == 'binance'
        assert canonicalize_exchange('binance') == 'binance'

    def test_alias_mapping(self):
        """Test that known aliases are mapped correctly."""
        assert canonicalize_exchange('okex') == 'okx'
        assert canonicalize_exchange('OKEX') == 'okx'
        assert canonicalize_exchange('huobi') == 'htx'
        assert canonicalize_exchange('gate') == 'gateio'

    def test_passthrough_unknown(self):
        """Test that unknown exchange names pass through lowercased."""
        assert canonicalize_exchange('someexchange') == 'someexchange'
        assert canonicalize_exchange('NEWEXCHANGE') == 'newexchange'

    def test_empty_string(self):
        """Test handling of empty string."""
        assert canonicalize_exchange('') == ''
        assert canonicalize_exchange('  ') == ''

    def test_whitespace_stripping(self):
        """Test that whitespace is stripped."""
        assert canonicalize_exchange('  binance  ') == 'binance'
        assert canonicalize_exchange('\tkucoin\n') == 'kucoin'


class TestExtractRequiredExchanges:
    """Tests for extracting required exchanges from strategy data."""

    def test_empty_strategy(self):
        """Test handling of empty/None strategy."""
        assert extract_required_exchanges(None) == set()
        assert extract_required_exchanges({}) == set()

    def test_extract_from_data_sources_tuple(self):
        """Test extraction from data_sources as tuples."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ('binance', 'BTC/USDT', '1h'),
                    ('kucoin', 'ETH/USDT', '5m'),
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance', 'kucoin'}

    def test_extract_from_data_sources_list(self):
        """Test extraction from data_sources as lists."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ['binance', 'BTC/USDT', '1h'],
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}

    def test_extract_from_data_sources_dict(self):
        """Test extraction from data_sources as dicts."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    {'exchange': 'binance', 'symbol': 'BTC/USDT', 'timeframe': '1h'},
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}

    def test_extract_from_default_source(self):
        """Test extraction from default_source."""
        strategy = {
            'default_source': {
                'exchange': 'kucoin',
                'symbol': 'BTC/USDT',
                'timeframe': '15m'
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'kucoin'}

    def test_extract_combined_sources(self):
        """Test extraction from both data_sources and default_source."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ('binance', 'BTC/USDT', '1h'),
                ]
            },
            'default_source': {
                'exchange': 'kucoin',
                'symbol': 'ETH/USDT',
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance', 'kucoin'}

    def test_extract_with_canonicalization(self):
        """Test that extracted exchanges are canonicalized."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ('BINANCE', 'BTC/USDT', '1h'),
                    ('okex', 'ETH/USDT', '5m'),  # Should become 'okx'
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance', 'okx'}

    def test_extract_deduplication(self):
        """Test that duplicate exchanges are deduplicated."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ('binance', 'BTC/USDT', '1h'),
                    ('Binance', 'ETH/USDT', '5m'),
                    ('BINANCE', 'LTC/USDT', '15m'),
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}

    def test_extract_json_string_components(self):
        """Test extraction when strategy_components is JSON string."""
        import json
        strategy = {
            'strategy_components': json.dumps({
                'data_sources': [
                    ['binance', 'BTC/USDT', '1h'],
                ]
            })
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}


class TestValidateForBacktest:
    """Tests for backtest mode validation."""

    def test_empty_requirements(self):
        """Test that empty requirements are valid."""
        result = validate_for_backtest(set(), ['binance', 'kucoin'])
        assert result.valid is True

    def test_all_available(self):
        """Test validation when all exchanges are available."""
        result = validate_for_backtest(
            {'binance', 'kucoin'},
            ['binance', 'kucoin', 'kraken']
        )
        assert result.valid is True

    def test_missing_exchanges(self):
        """Test validation when exchanges are missing."""
        result = validate_for_backtest(
            {'binance', 'kucoin', 'unknown_exchange'},
            ['binance', 'kucoin']
        )
        assert result.valid is False
        assert result.error_code == ValidationErrorCode.MISSING_EDM_DATA
        assert result.missing_exchanges == {'unknown_exchange'}
        assert 'unknown_exchange' in result.message

    def test_empty_edm_list(self):
        """Test validation when EDM list is empty."""
        result = validate_for_backtest({'binance'}, [])
        assert result.valid is False
        assert result.error_code == ValidationErrorCode.MISSING_EDM_DATA

    def test_none_edm_list(self):
        """Test validation when EDM list is None."""
        result = validate_for_backtest({'binance'}, None)
        assert result.valid is False
        assert result.error_code == ValidationErrorCode.MISSING_EDM_DATA

    def test_canonicalization_in_comparison(self):
        """Test that EDM exchanges are canonicalized during comparison."""
        result = validate_for_backtest(
            {'binance'},
            ['BINANCE']  # Should match after canonicalization
        )
        assert result.valid is True

    def test_edm_returns_dicts(self):
        """Test that EDM response with dicts (containing 'name' key) is handled."""
        result = validate_for_backtest(
            {'binance', 'kucoin'},
            [{'name': 'binance', 'timeframes': ['1m', '1h']},
             {'name': 'kucoin', 'timeframes': ['5m', '15m']}]
        )
        assert result.valid is True

    def test_edm_returns_dicts_missing(self):
        """Test that missing exchanges are detected with dict format."""
        result = validate_for_backtest(
            {'binance', 'kraken'},
            [{'name': 'binance'}]
        )
        assert result.valid is False
        assert result.missing_exchanges == {'kraken'}


class TestValidateForPaper:
    """Tests for paper mode validation."""

    def test_empty_requirements(self):
        """Test that empty requirements are valid."""
        result = validate_for_paper(set(), [])
        assert result.valid is True

    def test_valid_ccxt_exchange(self):
        """Test validation with valid ccxt exchange."""
        result = validate_for_paper({'binance'}, [])
        assert result.valid is True

    def test_invalid_exchange(self):
        """Test validation with invalid exchange name."""
        result = validate_for_paper({'totally_fake_exchange_xyz'}, [])
        assert result.valid is False
        assert result.error_code == ValidationErrorCode.INVALID_EXCHANGE
        assert 'totally_fake_exchange_xyz' in result.missing_exchanges

    def test_mixed_valid_invalid(self):
        """Test validation with mix of valid and invalid exchanges."""
        result = validate_for_paper({'binance', 'fake_exchange'}, [])
        assert result.valid is False
        assert result.missing_exchanges == {'fake_exchange'}


class TestValidateForLive:
    """Tests for live mode validation."""

    def test_empty_requirements(self):
        """Test that empty requirements are valid."""
        result = validate_for_live(set(), [])
        assert result.valid is True

    def test_configured_exchange(self):
        """Test validation when exchange is configured."""
        result = validate_for_live({'binance'}, ['binance', 'kucoin'])
        assert result.valid is True

    def test_missing_config(self):
        """Test validation when exchange is not configured."""
        result = validate_for_live({'kraken'}, ['binance', 'kucoin'])
        assert result.valid is False
        assert result.error_code == ValidationErrorCode.MISSING_CONFIG
        assert result.missing_exchanges == {'kraken'}
        assert 'API keys' in result.message

    def test_multiple_missing(self):
        """Test validation with multiple missing exchanges."""
        result = validate_for_live(
            {'binance', 'kraken', 'bybit'},
            ['binance']
        )
        assert result.valid is False
        assert result.missing_exchanges == {'kraken', 'bybit'}

    def test_canonicalization_in_comparison(self):
        """Test that configured exchanges are canonicalized."""
        result = validate_for_live({'binance'}, ['BINANCE'])
        assert result.valid is True


class TestValidateExchangeRequirements:
    """Tests for main validation entrypoint."""

    def test_routes_to_backtest(self):
        """Test that backtest mode routes correctly."""
        result = validate_exchange_requirements(
            required_exchanges={'binance'},
            user_configured_exchanges=[],
            edm_available_exchanges=['binance'],
            mode='backtest'
        )
        assert result.valid is True

    def test_routes_to_paper(self):
        """Test that paper mode routes correctly."""
        result = validate_exchange_requirements(
            required_exchanges={'binance'},
            user_configured_exchanges=[],
            edm_available_exchanges=[],
            mode='paper'
        )
        assert result.valid is True  # binance is valid ccxt exchange

    def test_routes_to_live(self):
        """Test that live mode routes correctly."""
        result = validate_exchange_requirements(
            required_exchanges={'binance'},
            user_configured_exchanges=['binance'],
            edm_available_exchanges=[],
            mode='live'
        )
        assert result.valid is True

    def test_empty_requirements_all_modes(self):
        """Test that empty requirements are valid for all modes."""
        for mode in ['backtest', 'paper', 'live']:
            result = validate_exchange_requirements(
                required_exchanges=set(),
                user_configured_exchanges=[],
                edm_available_exchanges=[],
                mode=mode
            )
            assert result.valid is True, f"Failed for mode: {mode}"

    def test_unknown_mode_rejected(self):
        """Test that unknown mode is rejected."""
        result = validate_exchange_requirements(
            required_exchanges={'binance'},
            user_configured_exchanges=[],
            edm_available_exchanges=[],
            mode='invalid_mode'
        )
        assert result.valid is False
        assert 'Invalid trading mode' in result.message


class TestExchangeValidationResult:
    """Tests for ExchangeValidationResult class."""

    def test_valid_result_to_dict(self):
        """Test serialization of valid result."""
        result = ExchangeValidationResult(valid=True)
        d = result.to_dict()
        assert d == {"valid": True}

    def test_invalid_result_to_dict(self):
        """Test serialization of invalid result."""
        result = ExchangeValidationResult(
            valid=False,
            error_code=ValidationErrorCode.MISSING_CONFIG,
            missing_exchanges={'binance', 'kucoin'},
            message="API keys required"
        )
        d = result.to_dict()
        assert d["valid"] is False
        assert d["error_code"] == "missing_config"
        assert set(d["missing_exchanges"]) == {'binance', 'kucoin'}
        assert d["message"] == "API keys required"


class TestEdgeCases:
    """Tests for edge cases and error handling."""

    def test_none_values_in_data_sources(self):
        """Test handling of None values in data sources."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    (None, 'BTC/USDT', '1h'),
                    ('binance', 'BTC/USDT', '1h'),
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}

    def test_empty_tuple_in_data_sources(self):
        """Test handling of empty tuples in data sources."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    (),
                    ('binance', 'BTC/USDT', '1h'),
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}

    def test_single_element_tuple(self):
        """Test handling of single element tuple (just exchange)."""
        strategy = {
            'strategy_components': {
                'data_sources': [
                    ('binance',),
                ]
            }
        }
        result = extract_required_exchanges(strategy)
        assert result == {'binance'}
