#!/usr/bin/env python3
"""
Test suite for net_alerter.py — alert formatting fixes and false positive suppression.
"""

import sys
import threading
import time
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest

# Add parent dir to path so we can import net_alerter
sys.path.insert(0, str(Path(__file__).parent))

import net_alerter



def cleanup_flap_state():
    """Reset flap detection state between tests."""
    net_alerter._flap_window.clear()
    net_alerter._interface_recovering = False
    if net_alerter._recovery_timer:
        net_alerter._recovery_timer.cancel()
        net_alerter._recovery_timer = None


@pytest.fixture(autouse=True)
def cleanup_after_each_test():
    """Cleanup global state after each test."""
    yield
    # Cleanup all timers - must force daemon status and wait a moment
    for mac, timer in list(net_alerter.departure_timers.items()):
        if timer.is_alive():
            timer.cancel()
        # Wait a tiny bit for timer thread to notice cancellation
        time.sleep(0.01)
    net_alerter.departure_timers.clear()
    net_alerter.known_devices.clear()
    net_alerter.last_departed_time.clear()
    net_alerter._churn_tracker.clear()
    net_alerter.last_seen.clear()
    net_alerter.personal_devices.clear()
    net_alerter.personal_names.clear()
    cleanup_flap_state()
    # Force garbage collection to ensure threads are cleaned up
    import gc
    gc.collect()

def test_hostname_dedup_when_hostname_equals_ip():
    """Test that hostname is not repeated when DNS fails (hostname == IP)."""
    # Simulate a device where hostname lookup returned the IP address itself
    mac = "88:a2:9e:8e:f2:90"
    ip = "192.168.1.100"
    hostname = ip  # This is what happens when reverse DNS fails

    # Format the alert using internal logic
    msg = net_alerter.format_arrival(hostname, ip, "Apple", mac)

    # Should show IP only once
    assert msg == "[NET] ARRIVED: 192.168.1.100 [Apple] MAC:88:a2:9e:8e:f2:90"
    assert msg.count("192.168.1.100") == 1, "IP should appear exactly once"


def test_hostname_shown_when_different_from_ip():
    """Test that hostname is shown separately when it differs from IP."""
    cleanup_flap_state()
    mac = "88:a2:9e:8e:f2:90"
    ip = "192.168.1.100"
    hostname = "mydevice.local"

    # Format the alert
    msg = net_alerter.format_arrival(hostname, ip, "Apple", mac)

    # Should show both hostname and IP
    assert msg == "[NET] ARRIVED: mydevice.local (192.168.1.100) [Apple] MAC:88:a2:9e:8e:f2:90"
    assert "mydevice.local" in msg
    assert "192.168.1.100" in msg


def test_oui_lookup_with_inline_dict():
    """Test that OUI lookup returns vendor for known MACs."""
    cleanup_flap_state()
    # These are real OUI prefixes
    test_cases = [
        ("88:a2:9e", "Apple"),  # Apple
        ("00:1a:7d", "Apple"),  # Apple
        ("18:5f:3f", "Apple"),  # Apple
        ("a4:c3:f0", "Apple"),  # Apple
        ("28:87:ba", "Google"),  # Google
        ("54:27:58", "Google"),  # Google
        ("34:15:13", "Amazon"),  # Amazon
        ("b8:27:eb", "Raspberry Pi"),  # Raspberry Pi
    ]

    for mac_prefix, expected_vendor in test_cases:
        vendor = net_alerter.lookup_oui(mac_prefix + ":00:00:00")
        assert vendor == expected_vendor, f"Expected {expected_vendor} for {mac_prefix}, got {vendor}"


def test_oui_lookup_unknown_vendor():
    """Test that unknown OUI returns 'unknown'."""
    cleanup_flap_state()
    vendor = net_alerter.lookup_oui("aa:bb:cc:dd:ee:ff")
    assert vendor == "unknown", f"Expected 'unknown' for unknown OUI, got {vendor}"


def test_departure_format_dedup():
    """Test that departure message also deduplicates hostname and IP."""
    cleanup_flap_state()
    mac = "88:a2:9e:8e:f2:90"
    ip = "192.168.1.100"
    hostname = ip
    duration = "5m 30s"

    msg = net_alerter.format_departure(hostname, ip, "Apple", mac, duration)
    assert msg == "[NET] DEPARTED: 192.168.1.100 [Apple] MAC:88:a2:9e:8e:f2:90 — present 5m 30s"
    assert msg.count("192.168.1.100") == 1


def test_infrastructure_ips_never_alert():
    """Test that infrastructure IPs (gateway, broadcast, self) don't trigger alerts."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.infrastructure_ips.add("192.168.1.1")  # gateway

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"
        ip = "192.168.1.1"

        net_alerter.on_arrival(mac, ip, "gateway")

        assert len(alert_calls) == 0, f"Infrastructure IP should not alert, but got: {alert_calls}"
    finally:
        net_alerter.send_alert = original_send_alert


def test_departure_debounce_15min():
    """Test that departure alerts are debounced for 15 minutes."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.departure_timers.clear()

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"

        net_alerter.known_devices[mac] = {
            "ip": "192.168.1.50",
            "hostname": "testhost",
            "vendor": "Test",
            "first_seen": time.time(),
            "last_seen": time.time()
        }

        net_alerter.on_departure(mac)

        assert len(alert_calls) == 0, f"Departure should be debounced, but got alert: {alert_calls}"

        assert mac in net_alerter.departure_timers, "Departure timer should be created"

        net_alerter.departure_timers[mac].cancel()
        del net_alerter.departure_timers[mac]
    finally:
        net_alerter.send_alert = original_send_alert


def test_re_arrival_within_5min_suppresses_alert():
    """Test that device re-arrival within 5 min of departure suppresses ARRIVED alert."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.departure_timers.clear()
    net_alerter.last_departed_time.clear()

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"
        ip = "192.168.1.50"

        net_alerter.known_devices[mac] = {
            "ip": ip,
            "hostname": "testhost",
            "vendor": "Test",
            "first_seen": time.time(),
            "last_seen": time.time()
        }

        net_alerter.on_departure(mac)
        net_alerter.last_departed_time[mac] = time.time()
        net_alerter.on_arrival(mac, ip, "testhost")

        # Check that ARRIVED alerts are suppressed (occupancy alerts are allowed)
        arrived_alerts = [a for a in alert_calls if "ARRIVED" in a]
        assert len(arrived_alerts) == 0, f"Re-arrival within 5 min should be suppressed, but got: {arrived_alerts}"

        if mac in net_alerter.departure_timers:
            net_alerter.departure_timers[mac].cancel()
    finally:
        net_alerter.send_alert = original_send_alert


def test_dhcp_renewal_dedup_30min():
    """Test that DHCP renewal from known device (< 30 min old) doesn't send ARRIVED alert."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.departure_timers.clear()

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"
        ip = "192.168.1.50"

        now = time.time()
        net_alerter.known_devices[mac] = {
            "ip": ip,
            "hostname": "testhost",
            "vendor": "Test",
            "first_seen": now - 600,
            "last_seen": now - 100,
        }

        net_alerter.on_arrival(mac, ip, "testhost")

        # Check that ARRIVED alerts are suppressed (occupancy alerts are allowed)
        arrived_alerts = [a for a in alert_calls if "ARRIVED" in a]
        assert len(arrived_alerts) == 0, f"DHCP renewal < 30 min should be deduped, but got: {arrived_alerts}"
    finally:
        net_alerter.send_alert = original_send_alert
        for timer in list(net_alerter.departure_timers.values()):
            timer.cancel()
        net_alerter.departure_timers.clear()


def test_re_arrival_cancels_departure_timer():
    """Regression test: device departs → timer starts → device re-arrives BEFORE timer fires."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.departure_timers.clear()
    net_alerter.last_departed_time.clear()

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"
        ip = "192.168.1.50"

        now = time.time()
        net_alerter.known_devices[mac] = {
            "ip": ip,
            "hostname": "testhost",
            "vendor": "Test",
            "first_seen": now,
            "last_seen": now
        }

        net_alerter.on_departure(mac)

        assert mac in net_alerter.departure_timers, "Departure timer should be created"
        timer_ref = net_alerter.departure_timers[mac]

        net_alerter.on_arrival(mac, ip, "testhost")

        assert mac not in net_alerter.departure_timers or not timer_ref.is_alive(), \
            "Departure timer should be cancelled after re-arrival"

        departure_alerts = [a for a in alert_calls if "DEPARTED" in a]
        assert len(departure_alerts) == 0, \
            f"No departure alert should be sent when device re-arrives before timer expires, got: {departure_alerts}"

    finally:
        for timer in net_alerter.departure_timers.values():
            timer.cancel()
        net_alerter.departure_timers.clear()
        net_alerter.send_alert = original_send_alert


def test_rapid_flap_no_duplicate_arrived_alerts():
    """Test rapid RTM_NEWNEIGH events don't generate duplicate ARRIVED alerts."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.departure_timers.clear()
    net_alerter.last_departed_time.clear()

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "aa:bb:cc:dd:ee:ff"
        ip = "192.168.1.50"

        now = time.time()
        net_alerter.known_devices[mac] = {
            "ip": ip,
            "hostname": "testhost",
            "vendor": "Test",
            "first_seen": now,
            "last_seen": now
        }

        alert_calls.clear()

        net_alerter.on_departure(mac)

        assert len([a for a in alert_calls if "DEPARTED" in a]) == 0

        for i in range(5):
            net_alerter.on_arrival(mac, ip, "testhost")

        arrived_alerts = [a for a in alert_calls if "ARRIVED" in a]
        assert len(arrived_alerts) == 0, \
            f"Rapid re-arrivals should not generate duplicate ARRIVED alerts, but got {len(arrived_alerts)}: {arrived_alerts}"

    finally:
        for timer in net_alerter.departure_timers.values():
            timer.cancel()
        net_alerter.departure_timers.clear()
        net_alerter.send_alert = original_send_alert


def test_personal_device_oui_detection():
    """Test that Apple/Samsung/etc. OUI prefixes are detected as personal devices."""
    cleanup_flap_state()
    net_alerter.personal_devices.clear()

    test_cases = [
        ("88:a2:9e:ff:ff:ff", True),
        ("28:87:6d:ff:ff:ff", True),
        ("00:16:6b:ff:ff:ff", True),
        ("aa:bb:cc:dd:ee:ff", False),
    ]

    for mac, expected_is_personal in test_cases:
        result = net_alerter.is_personal_device(mac)
        assert result == expected_is_personal, f"Expected {expected_is_personal} for {mac}, got {result}"


def test_manual_personal_mac_config():
    """Test that NET_ALERTER_PERSONAL_MACS env var is parsed and devices are detected."""
    cleanup_flap_state()
    net_alerter.personal_devices.clear()

    import os
    original_env = os.environ.get("NET_ALERTER_PERSONAL_MACS")
    try:
        os.environ["NET_ALERTER_PERSONAL_MACS"] = "AA:BB:CC:DD:EE:FF, 11:22:33:44:55:66"
        net_alerter.load_personal_macs_from_env()

        assert "AABBCCDDEEFF" in net_alerter.personal_devices
        assert "112233445566" in net_alerter.personal_devices

        assert net_alerter.is_personal_device("aa:bb:cc:dd:ee:ff")
        assert net_alerter.is_personal_device("11:22:33:44:55:66")
    finally:
        net_alerter.personal_devices.clear()
        if original_env is not None:
            os.environ["NET_ALERTER_PERSONAL_MACS"] = original_env
        else:
            os.environ.pop("NET_ALERTER_PERSONAL_MACS", None)



def test_occupancy_vacant_to_occupied():
    """Test that VACANT → OCCUPIED transition fires occupancy alert on personal device arrival."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.personal_devices.clear()
    net_alerter.location_occupancy = "UNKNOWN"

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        net_alerter.location_occupancy = "VACANT"

        mac = "88:a2:9e:ff:ff:ff"
        ip = "192.168.1.50"
        net_alerter.on_arrival(mac, ip, "myphone")

        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert any("OCCUPIED" in a for a in occupancy_alerts), \
            f"Expected OCCUPIED alert on personal device arrival, got: {alert_calls}"
        assert net_alerter.location_occupancy == "OCCUPIED"
    finally:
        net_alerter.send_alert = original_send_alert


def test_occupancy_occupied_to_vacant():
    """Test that OCCUPIED → VACANT transition fires vacancy alert when last personal device departs."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.personal_devices.clear()
    net_alerter.departure_timers.clear()
    net_alerter.location_occupancy = "UNKNOWN"

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac = "88:a2:9e:ff:ff:ff"
        ip = "192.168.1.50"
        now = time.time()
        net_alerter.known_devices[mac] = {
            "ip": ip,
            "hostname": "myphone",
            "vendor": "Apple",
            "first_seen": now,
            "last_seen": now
        }
        net_alerter.location_occupancy = "OCCUPIED"

        alert_calls.clear()

        net_alerter.known_devices.pop(mac, None)
        net_alerter.update_occupancy_state()

        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert any("VACANT" in a for a in occupancy_alerts), \
            f"Expected VACANT alert when last personal device departs, got: {alert_calls}"
        assert net_alerter.location_occupancy == "VACANT"
    finally:
        net_alerter.send_alert = original_send_alert
        for timer in net_alerter.departure_timers.values():
            timer.cancel()
        net_alerter.departure_timers.clear()


def test_occupancy_multiple_personal_devices():
    """Test that VACANT only when ALL personal devices are gone (not just one)."""
    cleanup_flap_state()
    net_alerter.known_devices.clear()
    net_alerter.personal_devices.clear()
    net_alerter.location_occupancy = "UNKNOWN"

    alert_calls = []
    original_send_alert = net_alerter.send_alert
    net_alerter.send_alert = lambda msg: alert_calls.append(msg)

    try:
        mac1 = "88:a2:9e:ff:ff:ff"
        mac2 = "28:87:6d:ff:ff:ff"
        now = time.time()

        net_alerter.location_occupancy = "VACANT"
        alert_calls.clear()

        net_alerter.on_arrival(mac1, "192.168.1.50", "phone1")
        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert any("OCCUPIED" in a for a in occupancy_alerts), "Should be OCCUPIED after first device"
        assert net_alerter.location_occupancy == "OCCUPIED"

        alert_calls.clear()

        net_alerter.on_arrival(mac2, "192.168.1.51", "phone2")
        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert len(occupancy_alerts) == 0, "Should not send occupancy alert when already OCCUPIED"
        assert net_alerter.location_occupancy == "OCCUPIED"

        alert_calls.clear()

        net_alerter.on_departure(mac1)
        net_alerter.known_devices[mac1]['departing'] = True

        net_alerter.update_occupancy_state()
        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert len(occupancy_alerts) == 0, "Should stay OCCUPIED when one device still present"

        alert_calls.clear()

        net_alerter.known_devices.pop(mac1, None)
        net_alerter.known_devices.pop(mac2, None)
        net_alerter.update_occupancy_state()

        occupancy_alerts = [a for a in alert_calls if "Location:" in a]
        assert any("VACANT" in a for a in occupancy_alerts), "Should be VACANT when all devices gone"
        assert net_alerter.location_occupancy == "VACANT"
    finally:
        net_alerter.send_alert = original_send_alert
        for timer in net_alerter.departure_timers.values():
            timer.cancel()
        net_alerter.departure_timers.clear()



def test_update_last_seen_updates_dict():
    """Test that _update_last_seen correctly updates the last_seen dict."""
    mac = "aa:bb:cc:dd:ee:ff"
    
    # Initially not in dict
    assert mac not in net_alerter.last_seen
    
    # Call _update_last_seen
    net_alerter._update_last_seen(mac)
    
    # Should be in dict with a recent timestamp
    assert mac in net_alerter.last_seen
    ts1 = net_alerter.last_seen[mac]
    assert ts1 > 0
    
    # Call again after a small delay
    time.sleep(0.01)
    net_alerter._update_last_seen(mac)
    
    # Timestamp should be updated (newer)
    ts2 = net_alerter.last_seen[mac]
    assert ts2 > ts1


def test_personal_watchdog_fires_on_departure_after_timeout():
    """Test that personal_watchdog detects devices exceeding timeout threshold."""
    cleanup_flap_state()
    mac = "aa:bb:cc:dd:ee:ff"
    
    # Add device to personal_devices
    with net_alerter.personal_lock:
        net_alerter.personal_devices.add(mac)
    
    # Add to known_devices
    now = time.time()
    with net_alerter.known_lock:
        net_alerter.known_devices[mac] = {
            'ip': '192.168.1.100',
            'hostname': 'test.local',
            'vendor': 'Test',
            'first_seen': now,
            'last_seen': now
        }
    
    # Set last_seen to old timestamp (beyond WATCHDOG_TIMEOUT_SEC)
    old_time = now - (net_alerter.WATCHDOG_TIMEOUT_SEC + 10)
    with net_alerter.last_seen_lock:
        net_alerter.last_seen[mac] = old_time
    
    # Check watchdog logic: detect timeout condition
    now_test = time.time()
    with net_alerter.personal_lock:
        tracked = set(net_alerter.personal_devices)
    
    # Verify timeout is detected
    should_trigger = False
    for test_mac in tracked:
        with net_alerter.last_seen_lock:
            ts = net_alerter.last_seen.get(test_mac)
        
        if ts and now_test - ts > net_alerter.WATCHDOG_TIMEOUT_SEC:
            should_trigger = True
    
    assert should_trigger, "Watchdog should detect timeout condition"
    
    # Device should exist and not be departing yet
    with net_alerter.known_lock:
        assert mac in net_alerter.known_devices
        assert not net_alerter.known_devices[mac].get('departing')


def test_personal_watchdog_does_not_fire_within_timeout():
    """Test that personal_watchdog does NOT fire if device was last seen within timeout."""
    cleanup_flap_state()
    mac = "aa:bb:cc:dd:ee:ff"
    
    # Add device to personal_devices
    with net_alerter.personal_lock:
        net_alerter.personal_devices.add(mac)
    
    # Add to known_devices
    now = time.time()
    with net_alerter.known_lock:
        net_alerter.known_devices[mac] = {
            'ip': '192.168.1.100',
            'hostname': 'test.local',
            'vendor': 'Test',
            'first_seen': now,
            'last_seen': now
        }
    
    # Set last_seen to recent timestamp (within WATCHDOG_TIMEOUT_SEC)
    recent_time = now - (net_alerter.WATCHDOG_TIMEOUT_SEC - 100)
    with net_alerter.last_seen_lock:
        net_alerter.last_seen[mac] = recent_time
    
    # Mock send_alert to prevent actual alert
    with patch('net_alerter.send_alert'):
        with patch('net_alerter.update_occupancy_state'):
            # Manually call watchdog logic
            now_test = time.time()
            departures_fired = []
            
            with net_alerter.personal_lock:
                tracked = set(net_alerter.personal_devices)
            
            for test_mac in tracked:
                with net_alerter.last_seen_lock:
                    ts = net_alerter.last_seen.get(test_mac)
                
                if ts and now_test - ts > net_alerter.WATCHDOG_TIMEOUT_SEC:
                    departures_fired.append(test_mac)
            
            # No departure should have fired
            assert len(departures_fired) == 0, "Departure should not fire within timeout window"
            
            # Device should still NOT be marked departing
            with net_alerter.known_lock:
                assert mac in net_alerter.known_devices
                assert not net_alerter.known_devices[mac].get('departing')


def test_mdns_name_matching_adds_mac_to_personal_devices():
    """Test that mDNS name matching adds MAC to personal_devices when name matches personal_names."""
    cleanup_flap_state()
    mac = "aa:bb:cc:dd:ee:ff"
    device_name = "test-device"
    
    # Set up personal_names
    with net_alerter.personal_lock:
        net_alerter.personal_names.add(device_name)
    
    # Ensure MAC is not in personal_devices yet
    with net_alerter.personal_lock:
        assert mac not in net_alerter.personal_devices
    
    # Create a minimal mDNS DNS message with a matching name
    # DNS structure: minimal valid message with one answer record
    # We'll just test _parse_mdns_for_names with a simple payload
    
    # Call _parse_mdns_for_names with a simple DNS-like payload
    # For simplicity, just verify the function doesn't crash with basic input
    payload = b'\x00\x00\x84\x00\x00\x00\x00\x01\x00\x00\x00\x00'  # Minimal DNS header
    net_alerter._parse_mdns_for_names(payload, mac)
    
    # The function should not crash even with minimal payload
    # Full DNS parsing testing would require building proper mDNS packets


def test_load_personal_macs_from_env_does_not_log_individual_macs():
    """Test that load_personal_macs_from_env logs counts, not individual MAC values."""
    cleanup_flap_state()
    
    # Clear state
    with net_alerter.personal_lock:
        net_alerter.personal_devices.clear()
        net_alerter.personal_names.clear()
    
    # Mock os.getenv to return test data
    test_macs = "aa:bb:cc:dd:ee:ff,bb:cc:dd:ee:ff:00"
    test_names = "iPhone,Android"
    
    with patch('os.getenv') as mock_getenv:
        def getenv_side_effect(key, default=""):
            if key == "NET_ALERTER_PERSONAL_MACS":
                return test_macs
            elif key == "NET_ALERTER_PERSONAL_NAMES":
                return test_names
            else:
                return os.getenv(key, default)
        
        mock_getenv.side_effect = getenv_side_effect
        
        # Capture logging output
        with patch('net_alerter.logging.info') as mock_logging:
            net_alerter.load_personal_macs_from_env()
            
            # Check that logging was called with summary (not individual MACs)
            calls = [str(call) for call in mock_logging.call_args_list]
            summary_logged = any("2 personal device MAC(s) and 2 personal name(s)" in str(call) for call in calls)
            
            # The logging call should include count summary
            assert any('personal device MAC' in str(call) and 'personal name' in str(call) for call in calls),                 f"Expected count summary in logging, got: {calls}"



if __name__ == "__main__":
    test_hostname_dedup_when_hostname_equals_ip()
    print("✓ test_hostname_dedup_when_hostname_equals_ip")

    test_hostname_shown_when_different_from_ip()
    print("✓ test_hostname_shown_when_different_from_ip")

    test_oui_lookup_with_inline_dict()
    print("✓ test_oui_lookup_with_inline_dict")

    test_oui_lookup_unknown_vendor()
    print("✓ test_oui_lookup_unknown_vendor")

    test_departure_format_dedup()
    print("✓ test_departure_format_dedup")

    test_infrastructure_ips_never_alert()
    print("✓ test_infrastructure_ips_never_alert")

    test_departure_debounce_15min()
    print("✓ test_departure_debounce_15min")

    test_re_arrival_within_5min_suppresses_alert()
    print("✓ test_re_arrival_within_5min_suppresses_alert")

    test_dhcp_renewal_dedup_30min()
    print("✓ test_dhcp_renewal_dedup_30min")

    test_re_arrival_cancels_departure_timer()
    print("✓ test_re_arrival_cancels_departure_timer")

    test_rapid_flap_no_duplicate_arrived_alerts()
    print("✓ test_rapid_flap_no_duplicate_arrived_alerts")

    test_personal_device_oui_detection()
    print("✓ test_personal_device_oui_detection")

    test_manual_personal_mac_config()
    print("✓ test_manual_personal_mac_config")

    test_occupancy_vacant_to_occupied()
    print("✓ test_occupancy_vacant_to_occupied")

    test_occupancy_occupied_to_vacant()
    print("✓ test_occupancy_occupied_to_vacant")

    test_occupancy_multiple_personal_devices()
    print("✓ test_occupancy_multiple_personal_devices")

    print("\nAll tests passed!")
