import os
import json
import pytest
import asyncio
from pathlib import Path
import threading
from unittest.mock import patch

from pulse.core import event_bus
from pulse.core.paths import get_state_dir

@pytest.fixture
def clean_events():
    """Clear out events directory before testing."""
    events_dir = get_state_dir() / "events"
    if events_dir.exists():
        for f in events_dir.glob("*.jsonl"):
            try:
                f.unlink()
            except OSError:
                pass
    # Reset subscribers
    event_bus._subscribers.clear()
    yield
    
    # Cleanup after
    if events_dir.exists():
        for f in events_dir.glob("*.jsonl"):
            try:
                f.unlink()
            except OSError:
                pass

def test_event_bus_publish(clean_events):
    channel = "agent.boot"
    payload = {"agent_id": "test_agent_1", "status": "online"}
    
    corr_id = event_bus.publish(channel, "test_source", payload)
    assert corr_id is not None
    
    events = event_bus.get_recent_events(channel)
    assert len(events) == 1
    assert events[0]["type"] == channel
    assert events[0]["source"] == "test_source"
    assert events[0]["payload"] == payload
    assert events[0]["correlation_id"] == corr_id

def test_event_bus_subscribe(clean_events):
    channel = "task.claimed"
    received = []
    
    def sync_callback(event):
        received.append(event)
        
    event_bus.subscribe(channel, sync_callback)
    
    event_bus.publish(channel, "test_source", {"task_id": "123"})
    
    assert len(received) == 1
    assert received[0]["type"] == channel
    assert received[0]["payload"]["task_id"] == "123"
    
    # Unsubscribe
    event_bus.unsubscribe(channel, sync_callback)
    event_bus.publish(channel, "test_source", {"task_id": "456"})
    
    # Still 1, didn't receive the second one
    assert len(received) == 1

@pytest.mark.asyncio
async def test_event_bus_async_subscribe(clean_events):
    channel = "brain.save"
    received = []
    
    async def async_callback(event):
        received.append(event)
        
    event_bus.subscribe(channel, async_callback)
    
    event_bus.publish(channel, "test_source", {"success": True})
    
    # Give the background task a tiny moment to run since it's dispatched to loop.create_task or threading
    await asyncio.sleep(0.1)
    
    assert len(received) == 1
    assert received[0]["payload"]["success"] is True

def test_event_bus_rotation(clean_events):
    channel = "test.rotation"
    file_path = event_bus._get_channel_file(channel)
    
    # Publish one event to create the file
    event_bus.publish(channel, "test_source", {"test": 1})
    assert file_path.exists()
    
    # Mock stat to pretend file is larger than 10MB only for our channel file
    original_stat = Path.stat
    
    def mock_stat(self, *args, **kwargs):
        if self == file_path:
            class MockStat:
                @property
                def st_size(self):
                    return 11 * 1024 * 1024
            return MockStat()
        return original_stat(self, *args, **kwargs)
            
    with patch.object(Path, "stat", side_effect=mock_stat, autospec=True):
        event_bus.publish(channel, "test_source", {"test": 2})
        
    # Check that rotation occurred: original file has only 1 event now (the one we just published)
    # And there should be a rotated file in the directory
    events = event_bus.get_recent_events(channel)
    assert len(events) == 1
    assert events[0]["payload"]["test"] == 2
    
    rotated_files = list(event_bus.EVENTS_DIR.glob(f"{channel}_*.jsonl"))
    assert len(rotated_files) == 1
