"""
Comprehensive tests for src/behavior_profiler.py - Behavior profiling functionality.
"""

import json
import queue
import sys
import threading
import time
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch

import pytest

# Add src to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from behavior_profiler import (
    BehaviorProfile,
    BehaviorProfiler,
    RegisterAccess,
    TimingPattern,
)


class TestDataClasses:
    """Test data class functionality."""

    def test_register_access_creation(self):
        """Test RegisterAccess dataclass creation."""
        access = RegisterAccess(
            timestamp=1234567890.0,
            register="REG_CTRL",
            offset=0x400,
            operation="write",
            value=0x12345678,
            duration_us=5.5,
        )

        assert access.timestamp == 1234567890.0
        assert access.register == "REG_CTRL"
        assert access.offset == 0x400
        assert access.operation == "write"
        assert access.value == 0x12345678
        assert access.duration_us == 5.5

    def test_register_access_optional_fields(self):
        """Test RegisterAccess with optional fields."""
        access = RegisterAccess(
            timestamp=1234567890.0,
            register="REG_STATUS",
            offset=0x404,
            operation="read",
        )

        assert access.value is None
        assert access.duration_us is None

    def test_timing_pattern_creation(self):
        """Test TimingPattern dataclass creation."""
        pattern = TimingPattern(
            pattern_type="periodic",
            registers=["REG_CTRL", "REG_STATUS"],
            avg_interval_us=100.0,
            std_deviation_us=5.0,
            frequency_hz=10000.0,
            confidence=0.95,
        )

        assert pattern.pattern_type == "periodic"
        assert len(pattern.registers) == 2
        assert pattern.avg_interval_us == 100.0
        assert pattern.confidence == 0.95

    def test_behavior_profile_creation(self, mock_behavior_profile):
        """Test BehaviorProfile dataclass creation."""
        profile = mock_behavior_profile

        assert profile.device_bdf == "0000:03:00.0"
        assert profile.capture_duration == 10.0
        assert profile.total_accesses == 100
        assert len(profile.register_accesses) == 1
        assert len(profile.timing_patterns) == 1
        assert "init" in profile.state_transitions
        assert "D0" in profile.power_states


class TestBehaviorProfilerInitialization:
    """Test BehaviorProfiler initialization."""

    def test_valid_bdf_initialization(self):
        """Test initialization with valid BDF."""
        profiler = BehaviorProfiler("0000:03:00.0", debug=True)

        assert profiler.bdf == "0000:03:00.0"
        assert profiler.debug is True
        assert profiler.monitoring is False
        assert isinstance(profiler.access_queue, queue.Queue)
        assert profiler.monitor_thread is None

    def test_invalid_bdf_initialization(self):
        """Test initialization with invalid BDF."""
        invalid_bdfs = [
            "invalid-bdf",
            "000:03:00.0",
            "0000:3:00.0",
            "0000:03:0.0",
            "0000:03:00.8",
            "",
        ]

        for bdf in invalid_bdfs:
            with pytest.raises(ValueError, match="Invalid BDF format"):
                BehaviorProfiler(bdf)

    def test_default_parameters(self):
        """Test initialization with default parameters."""
        profiler = BehaviorProfiler("0000:03:00.0")

        assert profiler.debug is False
        assert profiler.monitoring is False


class TestLogging:
    """Test logging functionality."""

    def test_debug_logging_enabled(self, capsys):
        """Test debug logging when enabled."""
        profiler = BehaviorProfiler("0000:03:00.0", debug=True)
        profiler._log("Test message")

        captured = capsys.readouterr()
        assert "[BehaviorProfiler] Test message" in captured.out

    def test_debug_logging_disabled(self, capsys):
        """Test debug logging when disabled."""
        profiler = BehaviorProfiler("0000:03:00.0", debug=False)
        profiler._log("Test message")

        captured = capsys.readouterr()
        assert captured.out == ""


class TestMonitoringSetup:
    """Test monitoring infrastructure setup."""

    @patch("os.path.exists")
    @patch("subprocess.run")
    def test_setup_monitoring_success(self, mock_run, mock_exists):
        """Test successful monitoring setup."""
        mock_exists.return_value = True
        mock_run.return_value = Mock(returncode=0)

        profiler = BehaviorProfiler("0000:03:00.0", debug=True)
        result = profiler._setup_monitoring()

        assert result is True

    @patch("os.path.exists")
    def test_setup_monitoring_device_not_found(self, mock_exists):
        """Test monitoring setup when device is not found."""
        mock_exists.return_value = False

        profiler = BehaviorProfiler("0000:03:00.0", debug=True)
        result = profiler._setup_monitoring()

        assert result is False

    @patch("os.path.exists")
    @patch("subprocess.run")
    def test_setup_monitoring_command_failure(self, mock_run, mock_exists):
        """Test monitoring setup when commands fail."""
        mock_exists.return_value = True
        mock_run.side_effect = Exception("Command failed")

        profiler = BehaviorProfiler("0000:03:00.0", debug=True)
        result = profiler._setup_monitoring()

        assert result is False


class TestBehaviorCapture:
    """Test behavior capture functionality."""

    @patch.object(BehaviorProfiler, "_setup_monitoring")
    @patch.object(BehaviorProfiler, "_start_monitoring")
    @patch.object(BehaviorProfiler, "_stop_monitoring")
    @patch("time.sleep")
    def test_capture_behavior_profile_success(
        self, mock_sleep, mock_stop, mock_start, mock_setup
    ):
        """Test successful behavior profile capture."""
        mock_setup.return_value = True

        # Mock some register accesses
        mock_accesses = [
            RegisterAccess(
                timestamp=time.time(),
                register="REG_CTRL",
                offset=0x400,
                operation="write",
                value=0x1,
            ),
            RegisterAccess(
                timestamp=time.time() + 0.1,
                register="REG_STATUS",
                offset=0x404,
                operation="read",
            ),
        ]

        profiler = BehaviorProfiler("0000:03:00.0", debug=True)

        # Mock the access queue to return test data
        profiler.access_queue.put(mock_accesses[0])
        profiler.access_queue.put(mock_accesses[1])
        profiler.access_queue.put(None)  # End marker

        profile = profiler.capture_behavior_profile(1.0)

        assert isinstance(profile, BehaviorProfile)
        assert profile.device_bdf == "0000:03:00.0"
        assert profile.capture_duration == 1.0

        mock_setup.assert_called_once()
        mock_start.assert_called_once()
        mock_stop.assert_called_once()

    @patch.object(BehaviorProfiler, "_setup_monitoring")
    def test_capture_behavior_profile_setup_failure(self, mock_setup):
        """Test behavior capture when monitoring setup fails."""
        mock_setup.return_value = False

        profiler = BehaviorProfiler("0000:03:00.0", debug=True)

        with pytest.raises(RuntimeError, match="Failed to setup monitoring"):
            profiler.capture_behavior_profile(1.0)

    @patch.object(BehaviorProfiler, "_setup_monitoring")
    @patch.object(BehaviorProfiler, "_start_monitoring")
    @patch.object(BehaviorProfiler, "_stop_monitoring")
    @patch("time.sleep")
    def test_capture_behavior_profile_with_duration(
        self, mock_sleep, mock_stop, mock_start, mock_setup
    ):
        """Test behavior capture with specific duration."""
        mock_setup.return_value = True

        profiler = BehaviorProfiler("0000:03:00.0")
        profiler.access_queue.put(None)  # End marker

        profile = profiler.capture_behavior_profile(5.0)

        assert profile.capture_duration == 5.0
        mock_sleep.assert_called_with(5.0)


class TestPatternAnalysis:
    """Test pattern analysis functionality."""

    def test_analyze_patterns_basic(self):
        """Test basic pattern analysis."""
        # Create test profile with some patterns
        register_accesses = [
            RegisterAccess(1000.0, "REG_CTRL", 0x400, "write", 0x1, 5.0),
            RegisterAccess(1000.1, "REG_CTRL", 0x400, "write", 0x2, 4.5),
            RegisterAccess(1000.2, "REG_STATUS", 0x404, "read", None, 3.0),
            RegisterAccess(1000.3, "REG_CTRL", 0x400, "write", 0x3, 5.5),
        ]

        timing_patterns = [
            TimingPattern("periodic", ["REG_CTRL"], 100.0, 5.0, 10000.0, 0.95)
        ]

        profile = BehaviorProfile(
            device_bdf="0000:03:00.0",
            capture_duration=10.0,
            total_accesses=4,
            register_accesses=register_accesses,
            timing_patterns=timing_patterns,
            state_transitions={"init": ["ready"]},
            power_states=["D0"],
            interrupt_patterns={},
        )

        profiler = BehaviorProfiler("0000:03:00.0")
        analysis = profiler.analyze_patterns(profile)

        assert "device_characteristics" in analysis
        assert "behavioral_signatures" in analysis
        assert "performance_metrics" in analysis
        assert "register_usage" in analysis

        # Check device characteristics
        device_chars = analysis["device_characteristics"]
        assert "access_frequency_hz" in device_chars
        assert "avg_access_duration_us" in device_chars
        assert "register_diversity" in device_chars

        # Check behavioral signatures
        behavioral_sigs = analysis["behavioral_signatures"]
        assert "timing_regularity" in behavioral_sigs
        assert "access_pattern_consistency" in behavioral_sigs
        assert "state_complexity" in behavioral_sigs

    def test_analyze_patterns_empty_profile(self):
        """Test pattern analysis with empty profile."""
        empty_profile = BehaviorProfile(
            device_bdf="0000:03:00.0",
            capture_duration=10.0,
            total_accesses=0,
            register_accesses=[],
            timing_patterns=[],
            state_transitions={},
            power_states=[],
            interrupt_patterns={},
        )

        profiler = BehaviorProfiler("0000:03:00.0")
        analysis = profiler.analyze_patterns(empty_profile)

        # Should handle empty data gracefully
        assert analysis["device_characteristics"]["access_frequency_hz"] == 0.0
        assert analysis["device_characteristics"]["register_diversity"] == 0

    def test_analyze_patterns_single_register(self):
        """Test pattern analysis with single register access."""
        single_access = [RegisterAccess(1000.0, "REG_CTRL", 0x400, "write", 0x1, 5.0)]

        profile = BehaviorProfile(
            device_bdf="0000:03:00.0",
            capture_duration=1.0,
            total_accesses=1,
            register_accesses=single_access,
            timing_patterns=[],
            state_transitions={},
            power_states=["D0"],
            interrupt_patterns={},
        )

        profiler = BehaviorProfiler("0000:03:00.0")
        analysis = profiler.analyze_patterns(profile)

        assert analysis["device_characteristics"]["access_frequency_hz"] == 1.0
        assert analysis["device_characteristics"]["register_diversity"] == 1


class TestTimingPatternDetection:
    """Test timing pattern detection."""

    def test_detect_periodic_patterns(self):
        """Test detection of periodic access patterns."""
        # Create regular periodic accesses
        accesses = []
        base_time = 1000.0
        interval = 0.1  # 100ms intervals

        for i in range(10):
            accesses.append(
                RegisterAccess(
                    timestamp=base_time + (i * interval),
                    register="REG_PERIODIC",
                    offset=0x400,
                    operation="read",
                )
            )

        profiler = BehaviorProfiler("0000:03:00.0")
        patterns = profiler._detect_timing_patterns(accesses)

        assert len(patterns) > 0

        # Should detect the periodic pattern
        periodic_pattern = next(
            (p for p in patterns if p.pattern_type == "periodic"), None
        )
        assert periodic_pattern is not None
        assert "REG_PERIODIC" in periodic_pattern.registers
        assert abs(periodic_pattern.avg_interval_us - 100000.0) < 1000.0  # 100ms ± 1ms

    def test_detect_burst_patterns(self):
        """Test detection of burst access patterns."""
        # Create burst pattern: quick succession followed by gap
        accesses = []
        base_time = 1000.0

        # First burst
        for i in range(5):
            accesses.append(
                RegisterAccess(
                    timestamp=base_time + (i * 0.001),  # 1ms apart
                    register="REG_BURST",
                    offset=0x400,
                    operation="write",
                )
            )

        # Gap
        base_time += 1.0  # 1 second gap

        # Second burst
        for i in range(5):
            accesses.append(
                RegisterAccess(
                    timestamp=base_time + (i * 0.001),
                    register="REG_BURST",
                    offset=0x400,
                    operation="write",
                )
            )

        profiler = BehaviorProfiler("0000:03:00.0")
        patterns = profiler._detect_timing_patterns(accesses)

        # Should detect burst pattern
        burst_pattern = next((p for p in patterns if p.pattern_type == "burst"), None)
        assert burst_pattern is not None

    def test_detect_irregular_patterns(self):
        """Test detection of irregular access patterns."""
        # Create completely random access times
        import random

        accesses = []
        base_time = 1000.0

        for i in range(20):
            accesses.append(
                RegisterAccess(
                    timestamp=base_time + random.uniform(0, 10),
                    register="REG_RANDOM",
                    offset=0x400,
                    operation="read",
                )
            )

        profiler = BehaviorProfiler("0000:03:00.0")
        patterns = profiler._detect_timing_patterns(accesses)

        # Should detect irregular pattern or no strong patterns
        if patterns:
            irregular_pattern = next(
                (p for p in patterns if p.pattern_type == "irregular"), None
            )
            if irregular_pattern:
                assert (
                    irregular_pattern.confidence < 0.5
                )  # Low confidence for irregular


class TestStateTransitionAnalysis:
    """Test state transition analysis."""

    def test_analyze_state_transitions_simple(self):
        """Test simple state transition analysis."""
        accesses = [
            RegisterAccess(1000.0, "REG_INIT", 0x400, "write", 0x1),
            RegisterAccess(1001.0, "REG_STATUS", 0x404, "read"),
            RegisterAccess(1002.0, "REG_CTRL", 0x408, "write", 0x2),
            RegisterAccess(1003.0, "REG_STATUS", 0x404, "read"),
        ]

        profiler = BehaviorProfiler("0000:03:00.0")
        transitions = profiler._analyze_state_transitions(accesses)

        assert isinstance(transitions, dict)
        # Should identify some state transitions based on register access patterns

    def test_analyze_state_transitions_empty(self):
        """Test state transition analysis with empty access list."""
        profiler = BehaviorProfiler("0000:03:00.0")
        transitions = profiler._analyze_state_transitions([])

        assert transitions == {}


class TestInterruptPatternAnalysis:
    """Test interrupt pattern analysis."""

    @patch("subprocess.check_output")
    def test_analyze_interrupt_patterns_success(self, mock_output):
        """Test successful interrupt pattern analysis."""
        # Mock /proc/interrupts output
        mock_interrupts = """           CPU0       CPU1       
  24:      12345      23456   PCI-MSI 1048576-edge      eth0
  25:       5678       6789   PCI-MSI 2097152-edge      wifi0
"""
        mock_output.return_value = mock_interrupts

        profiler = BehaviorProfiler("0000:03:00.0")
        patterns = profiler._analyze_interrupt_patterns()

        assert isinstance(patterns, dict)
        # Should contain interrupt information

    @patch("subprocess.check_output")
    def test_analyze_interrupt_patterns_failure(self, mock_output):
        """Test interrupt pattern analysis when command fails."""
        mock_output.side_effect = Exception("Command failed")

        profiler = BehaviorProfiler("0000:03:00.0")
        patterns = profiler._analyze_interrupt_patterns()

        assert patterns == {}


class TestMonitoringThreads:
    """Test monitoring thread functionality."""

    def test_start_stop_monitoring(self):
        """Test starting and stopping monitoring threads."""
        profiler = BehaviorProfiler("0000:03:00.0", debug=True)

        # Mock the monitoring method
        with patch.object(profiler, "_monitor_device_access") as mock_monitor:
            profiler._start_monitoring()

            assert profiler.monitoring is True
            assert profiler.monitor_thread is not None
            assert profiler.monitor_thread.is_alive()

            profiler._stop_monitoring()

            assert profiler.monitoring is False
            # Thread should finish
            profiler.monitor_thread.join(timeout=1.0)
            assert not profiler.monitor_thread.is_alive()

    def test_monitor_device_access_mock(self):
        """Test device access monitoring with mocked data."""
        profiler = BehaviorProfiler("0000:03:00.0", debug=True)

        # Mock the actual monitoring to avoid hardware dependencies
        with patch("time.sleep"), patch("subprocess.check_output") as mock_output:

            # Mock some register access data
            mock_output.return_value = "REG_CTRL 0x400 write 0x12345678"

            # Start monitoring briefly
            profiler.monitoring = True

            # Run one iteration of monitoring
            try:
                profiler._monitor_device_access()
            except Exception:
                # Expected in test environment without real hardware
                pass

            profiler.monitoring = False


class TestErrorHandling:
    """Test error handling and edge cases."""

    def test_invalid_duration(self):
        """Test behavior capture with invalid duration."""
        profiler = BehaviorProfiler("0000:03:00.0")

        with pytest.raises(ValueError):
            profiler.capture_behavior_profile(-1.0)

        with pytest.raises(ValueError):
            profiler.capture_behavior_profile(0.0)

    def test_monitoring_already_active(self):
        """Test starting monitoring when already active."""
        profiler = BehaviorProfiler("0000:03:00.0")
        profiler.monitoring = True

        with pytest.raises(RuntimeError, match="Monitoring already active"):
            profiler._start_monitoring()

    def test_stop_monitoring_not_active(self):
        """Test stopping monitoring when not active."""
        profiler = BehaviorProfiler("0000:03:00.0")

        # Should not raise exception
        profiler._stop_monitoring()

    def test_queue_overflow_handling(self):
        """Test handling of queue overflow scenarios."""
        profiler = BehaviorProfiler("0000:03:00.0")

        # Fill queue beyond capacity
        for i in range(1000):
            try:
                profiler.access_queue.put_nowait(
                    RegisterAccess(
                        timestamp=float(i),
                        register=f"REG_{i}",
                        offset=0x400 + i,
                        operation="read",
                    )
                )
            except queue.Full:
                break

        # Should handle gracefully without crashing
        assert profiler.access_queue.qsize() > 0


class TestPerformanceCharacteristics:
    """Test performance characteristics and optimization."""

    def test_large_dataset_analysis_performance(self):
        """Test performance with large datasets."""
        # Generate large dataset
        accesses = []
        base_time = 1000.0

        for i in range(10000):
            accesses.append(
                RegisterAccess(
                    timestamp=base_time + (i * 0.001),
                    register=f"REG_{i % 100}",
                    offset=0x400 + (i % 100) * 4,
                    operation="read" if i % 2 == 0 else "write",
                    value=i if i % 2 == 1 else None,
                )
            )

        profiler = BehaviorProfiler("0000:03:00.0")

        start_time = time.time()
        patterns = profiler._detect_timing_patterns(accesses)
        analysis_time = time.time() - start_time

        # Should complete within reasonable time (< 5 seconds for 10k accesses)
        assert analysis_time < 5.0
        assert isinstance(patterns, list)

    def test_memory_usage_optimization(self):
        """Test memory usage optimization for large datasets."""
        import sys

        profiler = BehaviorProfiler("0000:03:00.0")

        # Create large profile
        large_accesses = [
            RegisterAccess(
                timestamp=float(i),
                register=f"REG_{i % 10}",
                offset=0x400 + (i % 10) * 4,
                operation="read",
            )
            for i in range(50000)
        ]

        # Measure memory usage
        initial_size = sys.getsizeof(large_accesses)

        # Process the data
        analysis = profiler.analyze_patterns(
            BehaviorProfile(
                device_bdf="0000:03:00.0",
                capture_duration=50.0,
                total_accesses=len(large_accesses),
                register_accesses=large_accesses,
                timing_patterns=[],
                state_transitions={},
                power_states=["D0"],
                interrupt_patterns={},
            )
        )

        # Analysis should not consume excessive additional memory
        assert isinstance(analysis, dict)
        assert len(analysis) > 0


class TestIntegrationWithBuildSystem:
    """Test integration with the build system."""

    def test_profile_data_serialization(self, mock_behavior_profile):
        """Test serialization of profile data for build system integration."""
        profile = mock_behavior_profile

        # Should be serializable to JSON for integration
        try:
            from dataclasses import asdict

            profile_dict = asdict(profile)
            json_str = json.dumps(profile_dict, default=str)

            # Should be able to deserialize
            loaded_dict = json.loads(json_str)
            assert loaded_dict["device_bdf"] == profile.device_bdf
            assert loaded_dict["capture_duration"] == profile.capture_duration

        except (TypeError, ValueError) as e:
            pytest.fail(f"Profile data should be serializable: {e}")

    def test_enhanced_register_context_generation(self, mock_behavior_profile):
        """Test generation of enhanced register context for build system."""
        profiler = BehaviorProfiler("0000:03:00.0")

        # Simulate generating enhanced context from profile
        enhanced_context = profiler._generate_enhanced_context(mock_behavior_profile)

        assert isinstance(enhanced_context, dict)
        assert "timing_characteristics" in enhanced_context
        assert "access_patterns" in enhanced_context
        assert "performance_metrics" in enhanced_context

    def test_build_system_compatibility(self):
        """Test compatibility with build system expectations."""
        profiler = BehaviorProfiler("0000:03:00.0")

        # Test that profiler can be imported and used as expected by build.py
        assert hasattr(profiler, "capture_behavior_profile")
        assert hasattr(profiler, "analyze_patterns")

        # Test method signatures match build system expectations
        import inspect

        capture_sig = inspect.signature(profiler.capture_behavior_profile)
        assert "duration" in capture_sig.parameters

        # Mock a minimal profile for analysis
        minimal_profile = BehaviorProfile(
            device_bdf="0000:03:00.0",
            capture_duration=1.0,
            total_accesses=0,
            register_accesses=[],
            timing_patterns=[],
            state_transitions={},
            power_states=[],
            interrupt_patterns={},
        )

        analysis = profiler.analyze_patterns(minimal_profile)
        assert isinstance(analysis, dict)
        assert "device_characteristics" in analysis
