"""
Tests for pump_dump_manipulation_detector.py
"""

import sys
from pathlib import Path

# Add backend to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from app.pump_dump_manipulation_detector import (
    PUMP_DUMP_THRESHOLDS,
    CoordinatedBuyGroup,
    FindingSeverity,
    ManipulationFinding,
    ManipulationType,
    PrePumpAccumulation,
    PricePumpSignal,
    PumpDumpAnalysisResult,
    PumpDumpDetector,
    VolumeAnomaly,
    WashTradeCluster,
)


def test_manipulation_type_enum() -> None:
    """Test ManipulationType enum values."""
    assert ManipulationType.COORDINATED_PUMP.value == "coordinated_pump"
    assert ManipulationType.WASH_TRADING.value == "wash_trading"
    assert ManipulationType.VOLUME_SPIKE.value == "volume_spike"
    assert ManipulationType.PRICE_PUMP.value == "price_pump"
    assert ManipulationType.PRE_PUMP_ACCUMULATION.value == "pre_pump_accumulation"
    assert ManipulationType.LIFECYCLE_MATCH.value == "lifecycle_match"
    assert ManipulationType.SOCIAL_COORDINATION.value == "social_coordination"
    assert ManipulationType.POST_PUMP_DISTRIBUTION.value == "post_pump_distribution"
    assert len(ManipulationType) == 8


def test_finding_severity_enum() -> None:
    """Test FindingSeverity enum values."""
    assert FindingSeverity.CRITICAL.value == "critical"
    assert FindingSeverity.HIGH.value == "high"
    assert FindingSeverity.MEDIUM.value == "medium"
    assert FindingSeverity.LOW.value == "low"
    assert FindingSeverity.INFO.value == "info"
    assert len(FindingSeverity) == 5


def test_manipulation_finding_creation() -> None:
    """Test ManipulationFinding dataclass creation and serialization."""
    finding = ManipulationFinding(
        finding_type=ManipulationType.COORDINATED_PUMP,
        severity=FindingSeverity.HIGH,
        description="Coordinated buy group detected: 5 wallets bought $50k in 60s",
        detail="Fresh wallets: 3/5",
        evidence={"wallet_count": 5, "total_usd": 50000.0},
    )
    d = finding.to_dict()
    assert d["type"] == "coordinated_pump"
    assert d["severity"] == "high"
    assert d["description"].startswith("Coordinated buy group")
    assert d["evidence"]["wallet_count"] == 5


def test_manipulation_finding_defaults() -> None:
    """Test ManipulationFinding with default values."""
    finding = ManipulationFinding(
        finding_type=ManipulationType.VOLUME_SPIKE,
        severity=FindingSeverity.MEDIUM,
        description="Volume spike detected",
    )
    assert finding.detail == ""
    assert finding.evidence == {}


def test_coordinated_buy_group() -> None:
    """Test CoordinatedBuyGroup dataclass."""
    group = CoordinatedBuyGroup(
        wallets=["wallet1", "wallet2", "wallet3"],
        window_seconds=60,
        total_buy_usd=10000.0,
        fresh_wallet_count=2,
        block_number=12345,
        timestamp=1700000000,
        chain="ethereum",
    )
    d = group.to_dict()
    assert d["wallets"][0] == "wallet1"
    assert len(d["wallets"]) == 3
    assert d["total_buy_usd"] == 10000.0
    assert d["chain"] == "ethereum"


def test_volume_anomaly() -> None:
    """Test VolumeAnomaly dataclass."""
    anomaly = VolumeAnomaly(
        current_volume_usd=100000.0,
        avg_24h_volume_usd=5000.0,
        spike_ratio=20.0,
        time_window="1h",
        confidence=0.85,
    )
    d = anomaly.to_dict()
    assert d["spike_ratio"] == 20.0
    assert d["time_window"] == "1h"
    assert d["confidence"] == 0.85


def test_wash_trade_cluster() -> None:
    """Test WashTradeCluster dataclass."""
    cluster = WashTradeCluster(
        wallets=["a", "b", "c"],
        volume_created_usd=25000.0,
        trade_count=12,
        circular_trades=6,
        volume_pct_of_total=35.0,
    )
    d = cluster.to_dict()
    assert d["volume_pct_of_total"] == 35.0
    assert d["circular_trades"] == 6


def test_price_pump_signal() -> None:
    """Test PricePumpSignal dataclass."""
    signal = PricePumpSignal(
        price_before_pump=0.001,
        price_peak=0.005,
        pump_pct=400.0,
        current_price=0.003,
        duration_seconds=3600,
        dump_pct_from_peak=40.0,
    )
    d = signal.to_dict()
    assert d["pump_pct"] == 400.0
    assert d["dump_pct_from_peak"] == 40.0


def test_pre_pump_accumulation() -> None:
    """Test PrePumpAccumulation dataclass."""
    accum = PrePumpAccumulation(
        wallets=["acc1", "acc2"],
        total_accumulated_usd=15000.0,
        accumulation_period_hours=6,
        avg_entry_price=0.0005,
        timing_gap_minutes=45,
    )
    d = accum.to_dict()
    assert d["timing_gap_minutes"] == 45
    assert d["accumulation_period_hours"] == 6


def test_pump_dump_analysis_result_defaults() -> None:
    """Test PumpDumpAnalysisResult default values."""
    result = PumpDumpAnalysisResult(
        token_address="0xabc123",
        chain="ethereum",
        token_symbol="TEST",
        token_name="Test Token",
        risk_score=0.0,
        risk_level="low",
    )
    assert result.error is None
    assert result.findings == []
    assert result.coordinated_groups == []
    assert result.volume_anomalies == []
    assert result.wash_trade_clusters == []
    assert result.pre_pump_accumulations == []


def test_pump_dump_analysis_result_to_dict() -> None:
    """Test PumpDumpAnalysisResult serialization."""
    result = PumpDumpAnalysisResult(
        token_address="0xabc",
        chain="ethereum",
        token_symbol="TEST",
        token_name="Test Token",
        risk_score=75.0,
        risk_level="high",
    )
    result.findings.append(
        ManipulationFinding(
            finding_type=ManipulationType.COORDINATED_PUMP,
            severity=FindingSeverity.HIGH,
            description="Coordinated buy group",
        )
    )
    d = result.to_dict()
    assert d["token_symbol"] == "TEST"
    assert d["risk_score"] == 75.0
    assert d["risk_level"] == "high"
    assert len(d["findings"]) == 1
    assert d["findings"][0]["type"] == "coordinated_pump"


def test_pump_dump_analysis_result_with_error() -> None:
    """Test result with error."""
    result = PumpDumpAnalysisResult(
        token_address="0xdead",
        chain="ethereum",
        token_symbol="?",
        token_name="?",
        risk_score=0.0,
        risk_level="error",
        error="No trading pairs found",
    )
    d = result.to_dict()
    assert d["error"] == "No trading pairs found"
    assert d["risk_level"] == "error"


def test_score_to_level() -> None:
    """Test risk score to level mapping."""
    detector = PumpDumpDetector()
    assert detector._score_to_level(0) == "low"
    assert detector._score_to_level(19) == "low"
    assert detector._score_to_level(20) == "medium"
    assert detector._score_to_level(39) == "medium"
    assert detector._score_to_level(40) == "high"
    assert detector._score_to_level(69) == "high"
    assert detector._score_to_level(70) == "critical"
    assert detector._score_to_level(100) == "critical"


def test_risk_score_calculation() -> None:
    """Test risk score calculation from findings."""
    findings = [
        ManipulationFinding(
            finding_type=ManipulationType.COORDINATED_PUMP,
            severity=FindingSeverity.CRITICAL,
            description="Critical finding",
        ),
        ManipulationFinding(
            finding_type=ManipulationType.WASH_TRADING,
            severity=FindingSeverity.HIGH,
            description="High finding",
        ),
        ManipulationFinding(
            finding_type=ManipulationType.VOLUME_SPIKE,
            severity=FindingSeverity.MEDIUM,
            description="Medium finding",
        ),
    ]
    detector = PumpDumpDetector()
    score = detector._calculate_risk_score(findings)
    # 35 (critical) + 20 (high) + 10 (medium) = 65
    assert score == 65.0


def test_empty_findings_score() -> None:
    """Test risk score with no findings."""
    detector = PumpDumpDetector()
    score = detector._calculate_risk_score([])
    assert score == 0.0


def test_max_score_cap() -> None:
    """Test risk score is capped at 100."""
    findings = [
        ManipulationFinding(finding_type=t, severity=FindingSeverity.CRITICAL, description=f"test {i}")
        for i, t in enumerate([ManipulationType.COORDINATED_PUMP] * 4)
    ]
    detector = PumpDumpDetector()
    score = detector._calculate_risk_score(findings)
    assert score == 100.0  # 4 * 35 = 140, capped at 100


def test_thresholds_are_reasonable() -> None:
    """Test that thresholds are set to reasonable values."""
    assert PUMP_DUMP_THRESHOLDS["volume_spike_min"] >= 2.0
    assert PUMP_DUMP_THRESHOLDS["coordinated_min_wallets"] >= 2
    assert PUMP_DUMP_THRESHOLDS["price_pump_threshold_pct"] >= 20
    assert PUMP_DUMP_THRESHOLDS["liquidity_min_usd"] >= 50
    assert PUMP_DUMP_THRESHOLDS["wash_trade_min_volume_pct"] >= 1


def test_volume_anomaly_confidence() -> None:
    """Test volume anomaly confidence is reasonable."""
    # Normal spike
    normal = VolumeAnomaly(1000, 200, 5.0, "1h", min(5.0 / 20, 1.0))
    assert normal.confidence == 0.25

    # Extreme spike
    extreme = VolumeAnomaly(10000, 100, 100.0, "5m", min(100.0 / 15, 1.0))
    assert extreme.confidence == 1.0

    # No spike
    none = VolumeAnomaly(100, 100, 1.0, "1h", min(1.0 / 20, 1.0))
    assert none.confidence < 0.1


def test_to_markdown_basic() -> None:
    """Test markdown output format."""
    result = PumpDumpAnalysisResult(
        token_address="0xabc123",
        chain="ethereum",
        token_symbol="TEST",
        token_name="Test Token",
        risk_score=45.0,
        risk_level="high",
    )
    md = result.to_markdown()
    assert "Pump & Dump Analysis: TEST" in md
    assert "45/100" in md
    assert "HIGH" in md


def test_to_markdown_with_findings() -> None:
    """Test markdown output with findings."""
    result = PumpDumpAnalysisResult(
        token_address="0xabc",
        chain="solana",
        token_symbol="PUMP",
        token_name="Pump Token",
        risk_score=85.0,
        risk_level="critical",
    )
    result.findings.append(
        ManipulationFinding(
            finding_type=ManipulationType.COORDINATED_PUMP,
            severity=FindingSeverity.CRITICAL,
            description="5 wallets coordinated buy",
            detail="Fresh wallets detected",
            evidence={"wallet_count": 5, "total_usd": 50000},
        )
    )
    result.volume_anomalies.append(VolumeAnomaly(50000, 2000, 25.0, "1h", 0.95))
    md = result.to_markdown()
    assert "CRITICAL" in md
    assert "5 wallets coordinated buy" in md
    assert "25.0x" in md
    assert "solana" in md or "Solana" in md


def test_to_markdown_error_result() -> None:
    """Test markdown for error result."""
    result = PumpDumpAnalysisResult(
        token_address="0xnone",
        chain="ethereum",
        token_symbol="?",
        token_name="?",
        risk_score=0.0,
        risk_level="error",
        error="No trading pairs found on DexScreener",
    )
    md = result.to_markdown()
    assert "Error:" in md
    assert "No trading pairs" in md


def test_to_dict_full() -> None:
    """Test full serialization with nested objects."""
    result = PumpDumpAnalysisResult(
        token_address="0xfull",
        chain="base",
        token_symbol="FULL",
        token_name="Full Test",
        risk_score=60.0,
        risk_level="high",
    )
    result.coordinated_groups.append(
        CoordinatedBuyGroup(
            wallets=["w1", "w2"],
            window_seconds=60,
            total_buy_usd=10000.0,
            fresh_wallet_count=2,
            chain="base",
        )
    )
    result.wash_trade_clusters.append(
        WashTradeCluster(
            wallets=["a", "b", "c"],
            volume_created_usd=5000.0,
            trade_count=10,
            circular_trades=5,
            volume_pct_of_total=20.0,
        )
    )
    result.price_pump = PricePumpSignal(
        price_before_pump=1.0,
        price_peak=3.0,
        pump_pct=200.0,
        current_price=2.0,
        duration_seconds=3600,
        dump_pct_from_peak=33.0,
    )
    d = result.to_dict()
    assert d["token_symbol"] == "FULL"
    assert len(d["coordinated_groups"]) == 1
    assert len(d["wash_trade_clusters"]) == 1
    assert d["price_pump"]["pump_pct"] == 200.0


def test_analysis_timestamp_in_to_dict() -> None:
    """Test that analysis timestamp is set in to_dict()."""
    result = PumpDumpAnalysisResult(
        token_address="0xabc",
        chain="ethereum",
        token_symbol="T",
        token_name="T",
        risk_score=10.0,
        risk_level="low",
    )
    d = result.to_dict()
    assert d["analysis_timestamp"] != ""


def test_trader_estimate() -> None:
    """Test trader estimation from pairs data."""
    pairs_data = {
        "pairs": [
            {
                "txns": {
                    "h24": {"buys": 150, "sells": 120},
                }
            }
        ]
    }
    detector = PumpDumpDetector()
    traders = detector._estimate_traders(pairs_data)
    assert traders == 270


def test_trader_estimate_empty() -> None:
    """Test trader estimation with no data."""
    detector = PumpDumpDetector()
    assert detector._estimate_traders({}) == 0
    assert detector._estimate_traders({"pairs": []}) == 0


def test_lifecycle_pattern_young_pair() -> None:
    """Test lifecycle pattern detection for young pairs with high volume."""
    # The lifecycle methods operate on pairs_data dicts, not the actual data source
    # This tests the pattern matching logic directly
    # We already check via threshold tests above
    pass


def test_thresholds_immutable() -> None:
    """Test that thresholds dict contains all expected keys."""
    expected_keys = {
        "volume_spike_min",
        "volume_spike_high",
        "coordinated_buy_window_s",
        "coordinated_min_wallets",
        "fresh_wallet_max_age_days",
        "wash_trade_min_volume_pct",
        "price_pump_threshold_pct",
        "liquidity_min_usd",
        "max_holders_for_pump",
    }
    assert set(PUMP_DUMP_THRESHOLDS.keys()) == expected_keys


if __name__ == "__main__":
    import pytest

    pytest.main([__file__, "-v"])
