"""
Tests for CHRONOS HNDL Attack Detector
======================================

Tests baseline profiling, anomaly detection, and HNDL attack detection.
"""

import pytest
import math
from datetime import datetime, timedelta
from chronos.core.detect.hndl_detector import (
    HNDLDetector,
    BaselineProfiler,
    AnomalyDetector,
    StatisticsTracker,
    TimeSeriesAnalyzer,
    BaselineProfile,
    AnomalyScore,
    TrafficSample,
    HNDLAlert,
    HNDLIndicator,
    RiskLevel,
    DEFAULT_THRESHOLDS,
    create_detector,
    quick_analyze,
)
from chronos.core.detect.models import PacketInfo, Protocol


# ============================================================================
# Test Fixtures
# ============================================================================

def create_test_packet(
    timestamp: datetime = None,
    src_ip: str = "192.168.1.100",
    dst_ip: str = "10.0.0.1",
    src_port: int = 50000,
    dst_port: int = 443,
    protocol: Protocol = Protocol.TCP,
    length: int = 1000,
) -> PacketInfo:
    """Create a test packet."""
    return PacketInfo(
        timestamp=timestamp or datetime.now(),
        src_ip=src_ip,
        dst_ip=dst_ip,
        src_port=src_port,
        dst_port=dst_port,
        protocol=protocol,
        length=length,
        payload_length=length // 2,
    )


def create_sample(
    bytes_transferred: int = 10000,
    packet_count: int = 100,
    connection_count: int = 10,
    unique_destinations: int = 5,
    encrypted_ratio: float = 0.5,
    avg_packet_size: float = 100.0,
) -> TrafficSample:
    """Create a test traffic sample."""
    return TrafficSample(
        timestamp=datetime.now(),
        bytes_transferred=bytes_transferred,
        packet_count=packet_count,
        connection_count=connection_count,
        unique_destinations=unique_destinations,
        encrypted_ratio=encrypted_ratio,
        avg_packet_size=avg_packet_size,
    )


def create_baseline_profile(
    mean_bytes: float = 10000,
    std_bytes: float = 2000,
    sample_count: int = 100,
) -> BaselineProfile:
    """Create a test baseline profile."""
    profile = BaselineProfile(
        id="test-profile",
        name="Test Profile",
        sample_count=sample_count,
    )
    profile.mean_bytes_per_window = mean_bytes
    profile.std_bytes_per_window = std_bytes
    profile.mean_packets_per_window = 100
    profile.std_packets_per_window = 20
    profile.mean_connections = 10
    profile.std_connections = 3
    profile.mean_destinations = 5
    profile.std_destinations = 2
    profile.mean_packet_size = 100
    profile.std_packet_size = 30
    profile.mean_encrypted_ratio = 0.5
    profile.std_encrypted_ratio = 0.1
    profile.known_destinations = {"10.0.0.1", "10.0.0.2", "10.0.0.3"}
    profile.hourly_pattern = {h: mean_bytes for h in range(24)}
    profile.protocol_baseline = {"tcp": 0.8, "udp": 0.2}
    return profile


# ============================================================================
# StatisticsTracker Tests
# ============================================================================

class TestStatisticsTracker:
    """Tests for StatisticsTracker class."""
    
    def test_initialization(self):
        """Test tracker initialization."""
        tracker = StatisticsTracker()
        assert tracker.count == 0
        assert tracker.mean == 0.0
        assert tracker.std == 0.0
    
    def test_add_single_value(self):
        """Test adding a single value."""
        tracker = StatisticsTracker()
        tracker.add(100)
        assert tracker.count == 1
        assert tracker.mean == 100.0
        assert tracker.variance == 0.0
    
    def test_mean_calculation(self):
        """Test mean calculation."""
        tracker = StatisticsTracker()
        for v in [10, 20, 30, 40, 50]:
            tracker.add(v)
        assert tracker.mean == 30.0
        assert tracker.count == 5
    
    def test_std_calculation(self):
        """Test standard deviation calculation."""
        tracker = StatisticsTracker()
        # Sample std of [2, 4, 6, 8, 10] = sqrt(10) ≈ 3.16 (Welford's algorithm)
        for v in [2, 4, 6, 8, 10]:
            tracker.add(v)
        assert abs(tracker.std - 3.16) < 0.1
    
    def test_min_max_tracking(self):
        """Test min/max value tracking."""
        tracker = StatisticsTracker()
        for v in [50, 10, 90, 30, 70]:
            tracker.add(v)
        assert tracker.min_value == 10
        assert tracker.max_value == 90
    
    def test_z_score_calculation(self):
        """Test z-score calculation."""
        tracker = StatisticsTracker()
        for v in [100] * 100:  # All same values
            tracker.add(v)
        # Zero std, z-score should be 0
        assert tracker.z_score(150) == 0.0
        
        # With variance
        tracker2 = StatisticsTracker()
        for v in range(100):
            tracker2.add(v)
        z = tracker2.z_score(tracker2.mean + 2 * tracker2.std)
        assert abs(z - 2.0) < 0.1
    
    def test_is_anomaly(self):
        """Test anomaly detection."""
        tracker = StatisticsTracker()
        for v in range(100):
            tracker.add(v)
        
        # Value within 3 std should not be anomaly
        assert not tracker.is_anomaly(tracker.mean, threshold=3.0)
        
        # Value far from mean should be anomaly
        far_value = tracker.mean + 10 * tracker.std
        assert tracker.is_anomaly(far_value, threshold=3.0)
    
    def test_percentile(self):
        """Test percentile calculation."""
        tracker = StatisticsTracker()
        for v in range(1, 101):
            tracker.add(v)
        
        p50 = tracker.percentile(50)
        assert 45 <= p50 <= 55
        
        p99 = tracker.percentile(99)
        assert p99 >= 95


# ============================================================================
# TimeSeriesAnalyzer Tests
# ============================================================================

class TestTimeSeriesAnalyzer:
    """Tests for TimeSeriesAnalyzer class."""
    
    def test_initialization(self):
        """Test analyzer initialization."""
        analyzer = TimeSeriesAnalyzer()
        assert analyzer.window_size == 24
    
    def test_add_values(self):
        """Test adding time series values."""
        analyzer = TimeSeriesAnalyzer()
        now = datetime.now()
        # Add multiple values at the same hour
        for i in range(10):
            analyzer.add(now, 100 + i * 10)
        
        mean, std = analyzer.get_hourly_baseline(now.hour)
        assert mean > 0
    
    def test_time_anomaly_detection(self):
        """Test time-based anomaly detection."""
        analyzer = TimeSeriesAnalyzer()
        now = datetime.now()
        
        # Add normal values with some variance for this hour
        for i in range(20):
            analyzer.add(now, 100 + (i % 3) * 5)  # Values 100-110
        
        # Check normal value - should not be anomaly
        is_anomaly, score = analyzer.is_time_anomaly(now, 105)
        assert not is_anomaly
        
        # Check extreme value - 100x the normal should be flagged
        is_anomaly, score = analyzer.is_time_anomaly(now, 10000)
        # Score should be high even if detection threshold not met
        assert score > 3.0  # Very high deviation
    
    def test_trend_detection_insufficient_data(self):
        """Test trend detection with insufficient data."""
        analyzer = TimeSeriesAnalyzer()
        trend = analyzer.detect_trend()
        assert trend == "insufficient_data"
    
    def test_trend_detection_stable(self):
        """Test trend detection for stable values."""
        analyzer = TimeSeriesAnalyzer()
        now = datetime.now()
        for i in range(20):
            analyzer.add(now + timedelta(seconds=i), 100)
        
        trend = analyzer.detect_trend()
        assert trend == "stable"
    
    def test_trend_detection_increasing(self):
        """Test trend detection for increasing values."""
        analyzer = TimeSeriesAnalyzer()
        now = datetime.now()
        # Use exponential growth for clearer trend detection
        for i in range(20):
            analyzer.add(now + timedelta(seconds=i), 100 * (1.5 ** i))
        
        trend = analyzer.detect_trend()
        assert trend == "increasing"


# ============================================================================
# BaselineProfiler Tests
# ============================================================================

class TestBaselineProfiler:
    """Tests for BaselineProfiler class."""
    
    def test_initialization(self):
        """Test profiler initialization."""
        profiler = BaselineProfiler(profile_name="test")
        assert profiler.profile.name == "test"
        assert profiler.profile.sample_count == 0
    
    def test_process_single_packet(self):
        """Test processing a single packet."""
        profiler = BaselineProfiler()
        packet = create_test_packet()
        profiler.process_packet(packet)
        assert profiler._window_packets == 1
        assert profiler._window_bytes == packet.length
    
    def test_window_accumulation(self):
        """Test packet accumulation within window."""
        profiler = BaselineProfiler(window_seconds=10)
        base_time = datetime.now()
        
        for i in range(5):
            packet = create_test_packet(
                timestamp=base_time + timedelta(seconds=i),
                length=100,
            )
            profiler.process_packet(packet)
        
        assert profiler._window_packets == 5
        assert profiler._window_bytes == 500
    
    def test_window_finalization(self):
        """Test window finalization updates profile."""
        profiler = BaselineProfiler(window_seconds=1)
        base_time = datetime.now()
        
        # Add packets
        for i in range(10):
            packet = create_test_packet(
                timestamp=base_time + timedelta(milliseconds=i * 10),
                length=100,
            )
            profiler.process_packet(packet)
        
        # Force finalization
        profiler._finalize_window()
        
        assert profiler.profile.sample_count >= 1
        assert profiler._window_packets == 0  # Reset
    
    def test_destination_tracking(self):
        """Test unique destination tracking."""
        profiler = BaselineProfiler()
        
        destinations = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
        for dst in destinations:
            packet = create_test_packet(dst_ip=dst)
            profiler.process_packet(packet)
        
        assert len(profiler._window_destinations) == 3
    
    def test_encrypted_traffic_detection(self):
        """Test encrypted traffic ratio tracking."""
        profiler = BaselineProfiler()
        
        # Add HTTPS packets
        for i in range(5):
            packet = create_test_packet(dst_port=443, length=100)
            profiler.process_packet(packet)
        
        # Add HTTP packets
        for i in range(5):
            packet = create_test_packet(dst_port=80, length=100)
            profiler.process_packet(packet)
        
        # 50% should be encrypted
        ratio = profiler._window_encrypted_bytes / profiler._window_bytes
        assert ratio == 0.5
    
    def test_get_current_sample(self):
        """Test getting current sample without finalizing."""
        profiler = BaselineProfiler()
        
        for i in range(5):
            packet = create_test_packet(length=100)
            profiler.process_packet(packet)
        
        sample = profiler.get_current_sample()
        assert sample is not None
        assert sample.bytes_transferred == 500
        assert sample.packet_count == 5
    
    def test_is_baseline_valid(self):
        """Test baseline validity check."""
        profiler = BaselineProfiler()
        assert not profiler.is_baseline_valid()
        
        # Not enough samples
        profiler.profile.sample_count = 50
        assert not profiler.is_baseline_valid()
        
        # Enough samples
        profiler.profile.sample_count = 100
        assert profiler.is_baseline_valid()


# ============================================================================
# AnomalyDetector Tests
# ============================================================================

class TestAnomalyDetector:
    """Tests for AnomalyDetector class."""
    
    def test_initialization(self):
        """Test detector initialization."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        assert detector.baseline == baseline
    
    def test_analyze_normal_sample(self):
        """Test analyzing normal traffic sample."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        
        sample = create_sample(
            bytes_transferred=baseline.mean_bytes_per_window,
            packet_count=int(baseline.mean_packets_per_window),
        )
        
        result = detector.analyze(sample)
        assert result.total_score < 0.5
        assert result.risk_level in (RiskLevel.NONE, RiskLevel.LOW)
    
    def test_analyze_high_volume_anomaly(self):
        """Test detecting high volume anomaly."""
        baseline = create_baseline_profile(mean_bytes=10000, std_bytes=1000)
        detector = AnomalyDetector(baseline)
        
        # Create sample with 10x normal traffic
        sample = create_sample(bytes_transferred=100000)
        
        result = detector.analyze(sample)
        assert HNDLIndicator.HIGH_VOLUME_TRANSFER in result.indicators
        assert result.total_score > 0.3
    
    def test_analyze_unusual_destination(self):
        """Test detecting unusual destination anomaly."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        
        # Create sample with all unknown destinations
        sample = create_sample(unique_destinations=2)
        sample.destination_distribution = {
            "unknown.ip.1": 100,
            "unknown.ip.2": 100,
        }
        
        result = detector.analyze(sample)
        # Verify destination analysis ran and found unknowns
        assert result.details.get("destination_score", 0) > 0 or \
               HNDLIndicator.UNUSUAL_DESTINATION in result.indicators or \
               HNDLIndicator.PATTERN_ANOMALY in result.indicators
    
    def test_analyze_encryption_anomaly(self):
        """Test detecting encryption ratio anomaly."""
        baseline = create_baseline_profile()
        baseline.mean_encrypted_ratio = 0.3
        baseline.std_encrypted_ratio = 0.05
        detector = AnomalyDetector(baseline)
        
        # Sample with very high encryption
        sample = create_sample(encrypted_ratio=0.95)
        
        result = detector.analyze(sample)
        assert HNDLIndicator.BULK_ENCRYPTION in result.indicators
    
    def test_risk_level_calculation(self):
        """Test risk level calculation from score."""
        assert AnomalyScore.calculate_risk_level(0.0) == RiskLevel.NONE
        assert AnomalyScore.calculate_risk_level(0.3) == RiskLevel.LOW
        assert AnomalyScore.calculate_risk_level(0.5) == RiskLevel.MEDIUM
        assert AnomalyScore.calculate_risk_level(0.7) == RiskLevel.HIGH
        assert AnomalyScore.calculate_risk_level(0.9) == RiskLevel.CRITICAL
    
    def test_get_trend(self):
        """Test traffic trend detection."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        
        # Add some samples
        for i in range(15):
            sample = create_sample(bytes_transferred=1000 + i * 100)
            detector.analyze(sample)
        
        trend = detector.get_trend()
        assert trend in ("increasing", "stable", "decreasing", "insufficient_data")
    
    def test_recent_anomalies_tracking(self):
        """Test tracking of recent anomalies."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        
        # Generate some anomalies
        for i in range(10):
            sample = create_sample(bytes_transferred=100000 * (i + 1))
            detector.analyze(sample)
        
        recent = detector.get_recent_anomalies(min_score=0.0)
        assert len(recent) > 0


# ============================================================================
# HNDLDetector Tests
# ============================================================================

class TestHNDLDetector:
    """Tests for HNDLDetector class."""
    
    def test_initialization(self):
        """Test detector initialization."""
        detector = HNDLDetector(profile_name="test")
        assert detector.mode == "learning"
        assert not detector.is_baseline_ready
    
    def test_learn_packets(self):
        """Test learning from packets."""
        detector = HNDLDetector()
        
        for i in range(10):
            packet = create_test_packet()
            detector.learn(packet)
        
        stats = detector.get_stats()
        assert stats["packets_processed"] == 10
    
    def test_start_detection_fails_without_baseline(self):
        """Test that detection fails without sufficient baseline."""
        detector = HNDLDetector()
        
        result = detector.start_detection()
        assert result is False
        assert detector.mode == "learning"
    
    def test_start_detection_with_baseline(self):
        """Test starting detection with valid baseline."""
        detector = HNDLDetector()
        
        # Manually set baseline as valid
        detector._profiler.profile.sample_count = 150
        
        result = detector.start_detection()
        assert result is True
        assert detector.mode == "detection"
    
    def test_analyze_in_learning_mode(self):
        """Test analyze returns None in learning mode."""
        detector = HNDLDetector()
        packet = create_test_packet()
        
        result = detector.analyze(packet)
        assert result is None
    
    def test_alert_callback_registration(self):
        """Test alert callback registration."""
        detector = HNDLDetector()
        alerts_received = []
        
        def callback(alert):
            alerts_received.append(alert)
        
        detector.on_alert(callback)
        assert len(detector._alert_callbacks) == 1
    
    def test_get_alerts_with_filter(self):
        """Test getting alerts with risk filter."""
        detector = HNDLDetector()
        
        # Add test alerts
        for risk in [RiskLevel.LOW, RiskLevel.MEDIUM, RiskLevel.HIGH]:
            alert = HNDLAlert(
                id=f"test-{risk.value}",
                timestamp=datetime.now(),
                risk_level=risk,
                anomaly_score=AnomalyScore(
                    total_score=0.5,
                    risk_level=risk,
                    indicators=[],
                ),
                indicators=[],
                description="Test alert",
                affected_destinations=[],
                bytes_transferred=1000,
                duration_seconds=60,
            )
            detector._alerts.append(alert)
        
        # Filter by risk
        high_alerts = detector.get_alerts(min_risk=RiskLevel.HIGH)
        assert len(high_alerts) == 1
        assert high_alerts[0].risk_level == RiskLevel.HIGH
    
    def test_get_stats(self):
        """Test getting detector statistics."""
        detector = HNDLDetector()
        
        for i in range(5):
            detector.learn(create_test_packet())
        
        stats = detector.get_stats()
        assert "packets_processed" in stats
        assert "mode" in stats
        assert "baseline_valid" in stats
        assert stats["packets_processed"] == 5


# ============================================================================
# Data Model Tests
# ============================================================================

class TestTrafficSample:
    """Tests for TrafficSample dataclass."""
    
    def test_creation(self):
        """Test sample creation."""
        sample = create_sample()
        assert sample.bytes_transferred == 10000
        assert sample.packet_count == 100
    
    def test_to_dict(self):
        """Test conversion to dictionary."""
        sample = create_sample()
        d = sample.to_dict()
        assert "bytes_transferred" in d
        assert "timestamp" in d


class TestBaselineProfile:
    """Tests for BaselineProfile dataclass."""
    
    def test_is_valid(self):
        """Test validity check."""
        profile = BaselineProfile(id="test", name="Test")
        assert not profile.is_valid
        
        profile.sample_count = 100
        assert profile.is_valid
    
    def test_to_dict(self):
        """Test conversion to dictionary."""
        profile = create_baseline_profile()
        d = profile.to_dict()
        assert "id" in d
        assert "is_valid" in d


class TestAnomalyScore:
    """Tests for AnomalyScore dataclass."""
    
    def test_creation(self):
        """Test score creation."""
        score = AnomalyScore(
            total_score=0.75,
            risk_level=RiskLevel.HIGH,
            indicators=[HNDLIndicator.HIGH_VOLUME_TRANSFER],
        )
        assert score.total_score == 0.75
        assert score.risk_level == RiskLevel.HIGH
    
    def test_to_dict(self):
        """Test conversion to dictionary."""
        score = AnomalyScore(
            total_score=0.5,
            risk_level=RiskLevel.MEDIUM,
            indicators=[HNDLIndicator.PATTERN_ANOMALY],
        )
        d = score.to_dict()
        assert d["total_score"] == 0.5
        assert "indicators" in d


class TestHNDLAlert:
    """Tests for HNDLAlert dataclass."""
    
    def test_creation(self):
        """Test alert creation."""
        alert = HNDLAlert(
            id="test-alert",
            timestamp=datetime.now(),
            risk_level=RiskLevel.HIGH,
            anomaly_score=AnomalyScore(
                total_score=0.8,
                risk_level=RiskLevel.HIGH,
                indicators=[],
            ),
            indicators=[HNDLIndicator.HIGH_VOLUME_TRANSFER],
            description="Test alert",
            affected_destinations=["10.0.0.1"],
            bytes_transferred=100000,
            duration_seconds=300,
        )
        assert alert.id == "test-alert"
        assert alert.risk_level == RiskLevel.HIGH
    
    def test_to_dict(self):
        """Test conversion to dictionary."""
        alert = HNDLAlert(
            id="test",
            timestamp=datetime.now(),
            risk_level=RiskLevel.MEDIUM,
            anomaly_score=AnomalyScore(
                total_score=0.5,
                risk_level=RiskLevel.MEDIUM,
                indicators=[],
            ),
            indicators=[],
            description="Test",
            affected_destinations=[],
            bytes_transferred=0,
            duration_seconds=0,
        )
        d = alert.to_dict()
        assert "id" in d
        assert "risk_level" in d


# ============================================================================
# Enum Tests
# ============================================================================

class TestEnums:
    """Tests for enum types."""
    
    def test_hndl_indicator_values(self):
        """Test HNDL indicator enum values."""
        assert HNDLIndicator.HIGH_VOLUME_TRANSFER.value == "high_volume_transfer"
        assert HNDLIndicator.BULK_ENCRYPTION.value == "bulk_encryption"
    
    def test_risk_level_values(self):
        """Test risk level enum values."""
        assert RiskLevel.NONE.value == "none"
        assert RiskLevel.CRITICAL.value == "critical"


# ============================================================================
# Helper Function Tests
# ============================================================================

class TestHelperFunctions:
    """Tests for module helper functions."""
    
    def test_create_detector(self):
        """Test create_detector factory function."""
        detector = create_detector(profile_name="test", window_seconds=60)
        assert isinstance(detector, HNDLDetector)
        assert detector._profiler.profile.name == "test"
    
    def test_quick_analyze_insufficient_data(self):
        """Test quick_analyze with insufficient data."""
        packets = [create_test_packet() for _ in range(10)]
        alerts = quick_analyze(packets)
        assert alerts == []  # Not enough for baseline


# ============================================================================
# Integration Tests
# ============================================================================

class TestIntegration:
    """Integration tests for HNDL detection system."""
    
    def test_full_detection_workflow(self):
        """Test complete detection workflow."""
        detector = HNDLDetector(window_seconds=1)
        alerts_received = []
        
        def on_alert(alert):
            alerts_received.append(alert)
        
        detector.on_alert(on_alert)
        
        # Learning phase - simulate normal traffic
        base_time = datetime.now()
        for i in range(150):
            packet = create_test_packet(
                timestamp=base_time + timedelta(seconds=i),
                length=1000 + (i % 100),
            )
            detector.learn(packet)
        
        # Check baseline
        profile = detector.get_baseline()
        assert profile.sample_count > 0
    
    def test_anomaly_score_normalization(self):
        """Test that anomaly scores are properly normalized."""
        baseline = create_baseline_profile()
        detector = AnomalyDetector(baseline)
        
        # Various traffic levels
        for multiplier in [0.5, 1.0, 2.0, 5.0, 10.0]:
            sample = create_sample(
                bytes_transferred=int(baseline.mean_bytes_per_window * multiplier)
            )
            result = detector.analyze(sample)
            
            # Score should always be between 0 and 1
            assert 0.0 <= result.total_score <= 1.0
    
    def test_multiple_indicator_detection(self):
        """Test detection of multiple indicators."""
        baseline = create_baseline_profile()
        baseline.known_destinations = {"10.0.0.1"}
        detector = AnomalyDetector(baseline)
        
        # Create sample with multiple anomalies
        sample = create_sample(
            bytes_transferred=100000,  # High volume
            encrypted_ratio=0.99,       # High encryption
            unique_destinations=20,     # Many destinations
        )
        sample.destination_distribution = {
            "unknown.1": 100,
            "unknown.2": 100,
        }
        
        result = detector.analyze(sample)
        
        # Should detect multiple indicators
        assert len(result.indicators) >= 2


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
