import pytest
import threading
import gc
from unittest.mock import MagicMock, patch
from weakref import WeakValueDictionary

from app.core.grpc.services.grpc_server import AgentOrchestrator
from app.protos import agent_pb2

@pytest.fixture
def mock_registry():
    return MagicMock()

@pytest.fixture
def orchestrator(mock_registry, tmp_path):
    with patch('threading.Thread'), \
         patch('app.core.grpc.services.grpc_server.settings') as mock_settings:
        mock_settings.DATA_DIR = str(tmp_path)
        return AgentOrchestrator(registry=mock_registry)

def test_io_locks_is_weak_value_dict(orchestrator):
    assert isinstance(orchestrator.io_locks, WeakValueDictionary)

def test_io_locks_garbage_collection(orchestrator):
    # Simulate adding a lock
    lock_key = "session1:file1"
    
    # We need to create a lock and put it in
    lock = threading.Lock()
    orchestrator.io_locks[lock_key] = lock
    
    assert lock_key in orchestrator.io_locks
    
    # Now remove the strong reference
    del lock
    
    # Force garbage collection
    gc.collect()
    
    # It should be gone!
    assert lock_key not in orchestrator.io_locks

def test_build_sandbox_policy_default_strict(orchestrator):
    policy = orchestrator._build_sandbox_policy({})
    assert policy.mode == agent_pb2.SandboxPolicy.STRICT

def test_build_sandbox_policy_explicit_strict(orchestrator):
    policy = orchestrator._build_sandbox_policy({"shell": {"sandbox": {"mode": "STRICT"}}})
    assert policy.mode == agent_pb2.SandboxPolicy.STRICT

def test_build_sandbox_policy_explicit_permissive(orchestrator):
    policy = orchestrator._build_sandbox_policy({"shell": {"sandbox": {"mode": "PERMISSIVE"}}})
    assert policy.mode == agent_pb2.SandboxPolicy.PERMISSIVE

def test_manifest_accumulation(orchestrator):
    node_id = "test-node"
    session_id = "session1"
    
    orchestrator.mirror = MagicMock()
    orchestrator.mirror.reconcile.return_value = None
    
    # Send chunk 0
    msg1 = agent_pb2.ClientTaskMessage(
        file_sync=agent_pb2.FileSyncMessage(
            session_id=session_id,
            manifest=agent_pb2.DirectoryManifest(
                root_path="/tmp",
                chunk_index=0,
                is_final=False,
                files=[agent_pb2.FileInfo(path="file1", is_dir=False, size=100)]
            )
        )
    )
    orchestrator._handle_client_message(msg1, node_id, MagicMock())
    
    key = (node_id, session_id)
    assert key in orchestrator.manifest_accumulators
    assert len(orchestrator.manifest_accumulators[key]) == 1
    
    # Send chunk 1 (final)
    msg2 = agent_pb2.ClientTaskMessage(
        file_sync=agent_pb2.FileSyncMessage(
            session_id=session_id,
            manifest=agent_pb2.DirectoryManifest(
                root_path="/tmp",
                chunk_index=1,
                is_final=True,
                files=[agent_pb2.FileInfo(path="file2", is_dir=False, size=200)]
            )
        )
    )
    
    orchestrator._handle_client_message(msg2, node_id, MagicMock())
    
    # After final chunk, it should be removed from accumulators
    assert key not in orchestrator.manifest_accumulators
    
    # And mirror.reconcile should be called with full manifest!
    orchestrator.mirror.reconcile.assert_called_once()
    args, kwargs = orchestrator.mirror.reconcile.call_args
    assert args[0] == session_id
    full_manifest = args[1]
    assert full_manifest.root_path == "/tmp"
    assert len(full_manifest.files) == 2
    assert full_manifest.files[0].path == "file1"
    assert full_manifest.files[1].path == "file2"
