"""Comprehensive unit tests for TelemetryQueue (buffering/queue.py).

Tests cover initialization, enqueue/dequeue, overflow, disk persistence,
background writer, thread safety, shutdown, and error handling paths.
Target: >90% coverage for mca_sdk.buffering.queue module.
"""

import json
import os
import queue as queue_module
import threading
import time
from collections import deque
from unittest.mock import MagicMock, Mock, patch, mock_open

import pytest

from mca_sdk.buffering.queue import TelemetryQueue
from mca_sdk.utils.exceptions import BufferingError


# ---------------------------------------------------------------------------
# Initialization Tests
# ---------------------------------------------------------------------------
class TestTelemetryQueueInit:
    """Tests for TelemetryQueue.__init__ and validation."""

    def test_default_initialization(self):
        q = TelemetryQueue()
        assert q._max_size == 1000
        assert q.size() == 0
        assert q._persist is False
        assert q._persist_path is None

    def test_custom_max_size(self):
        q = TelemetryQueue(max_size=50)
        assert q._max_size == 50

    def test_zero_max_size_raises(self):
        with pytest.raises(BufferingError, match="max_size must be positive"):
            TelemetryQueue(max_size=0)

    def test_negative_max_size_raises(self):
        with pytest.raises(BufferingError, match="max_size must be positive"):
            TelemetryQueue(max_size=-5)

    def test_zero_shutdown_timeout_raises(self):
        with pytest.raises(BufferingError, match="shutdown_timeout must be positive"):
            TelemetryQueue(shutdown_timeout=0)

    def test_negative_shutdown_timeout_raises(self):
        with pytest.raises(BufferingError, match="shutdown_timeout must be positive"):
            TelemetryQueue(shutdown_timeout=-1.0)

    def test_persist_without_path_raises(self):
        with pytest.raises(BufferingError, match="persist_path required"):
            TelemetryQueue(persist=True)

    def test_persist_with_path_starts_writer(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            assert q._persist is True
            assert q._writer_thread is not None
            assert q._writer_thread.is_alive()
            assert q._write_queue is not None
            assert q._shutdown_event is not None
        finally:
            q.shutdown()

    def test_persist_path_expanded(self, tmp_path):
        """Test that persist_path with ~ is expanded and validated."""
        q = TelemetryQueue(persist=False, persist_path="~/.mca_sdk/test_queue.json")
        assert "~" not in q._persist_path
        assert ".mca_sdk" in q._persist_path

    def test_persistence_metrics_initialized(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            assert q._persist_writes == 0
            assert q._persist_success_count == 0
            assert q._persist_failure_count == 0
            assert q._persist_disk_full_count == 0
            assert q._corruption_count == 0
            assert q._items_recovered == 0
        finally:
            q.shutdown()


# ---------------------------------------------------------------------------
# Enqueue / Dequeue Tests
# ---------------------------------------------------------------------------
class TestEnqueueDequeue:
    """Tests for enqueue, dequeue, dequeue_batch operations."""

    def test_enqueue_returns_true_when_not_full(self):
        q = TelemetryQueue(max_size=10)
        assert q.enqueue({"metric": "test"}) is True
        assert q.size() == 1

    def test_enqueue_returns_false_when_full(self):
        q = TelemetryQueue(max_size=2)
        q.enqueue({"a": 1})
        q.enqueue({"b": 2})
        # Queue is now full, next enqueue evicts oldest
        result = q.enqueue({"c": 3})
        assert result is False
        assert q.size() == 2

    def test_dequeue_fifo_order(self):
        q = TelemetryQueue(max_size=10)
        q.enqueue({"id": 1})
        q.enqueue({"id": 2})
        assert q.dequeue() == {"id": 1}
        assert q.dequeue() == {"id": 2}

    def test_dequeue_empty_returns_none(self):
        q = TelemetryQueue(max_size=10)
        assert q.dequeue() is None

    def test_dequeue_batch_returns_items(self):
        q = TelemetryQueue(max_size=10)
        for i in range(5):
            q.enqueue({"id": i})
        batch = q.dequeue_batch(3)
        assert len(batch) == 3
        assert batch == [{"id": 0}, {"id": 1}, {"id": 2}]
        assert q.size() == 2

    def test_dequeue_batch_returns_fewer_when_queue_smaller(self):
        q = TelemetryQueue(max_size=10)
        q.enqueue({"id": 1})
        batch = q.dequeue_batch(5)
        assert len(batch) == 1

    def test_dequeue_batch_zero_size_raises(self):
        q = TelemetryQueue(max_size=10)
        with pytest.raises(BufferingError, match="dequeue_batch size must be positive"):
            q.dequeue_batch(0)

    def test_dequeue_batch_negative_size_raises(self):
        q = TelemetryQueue(max_size=10)
        with pytest.raises(BufferingError, match="dequeue_batch size must be positive"):
            q.dequeue_batch(-1)

    def test_dequeue_batch_empty_queue(self):
        q = TelemetryQueue(max_size=10)
        batch = q.dequeue_batch(5)
        assert batch == []


# ---------------------------------------------------------------------------
# Peek, Size, Empty, Full Tests
# ---------------------------------------------------------------------------
class TestQueueStateChecks:
    """Tests for peek, size, is_empty, is_full."""

    def test_peek_returns_oldest_without_removal(self):
        q = TelemetryQueue(max_size=10)
        q.enqueue({"id": 1})
        q.enqueue({"id": 2})
        assert q.peek() == {"id": 1}
        assert q.size() == 2  # Not removed

    def test_peek_empty_returns_none(self):
        q = TelemetryQueue(max_size=10)
        assert q.peek() is None

    def test_is_empty(self):
        q = TelemetryQueue(max_size=10)
        assert q.is_empty() is True
        q.enqueue({"x": 1})
        assert q.is_empty() is False

    def test_is_full(self):
        q = TelemetryQueue(max_size=2)
        assert q.is_full() is False
        q.enqueue({"a": 1})
        q.enqueue({"b": 2})
        assert q.is_full() is True


# ---------------------------------------------------------------------------
# Clear Tests
# ---------------------------------------------------------------------------
class TestClear:
    """Tests for clear operation."""

    def test_clear_returns_count(self):
        q = TelemetryQueue(max_size=10)
        q.enqueue({"a": 1})
        q.enqueue({"b": 2})
        q.enqueue({"c": 3})
        count = q.clear()
        assert count == 3
        assert q.size() == 0

    def test_clear_empty_queue(self):
        q = TelemetryQueue(max_size=10)
        count = q.clear()
        assert count == 0

    def test_clear_with_persist_triggers_save(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            q.enqueue({"a": 1})
            q.clear()
            assert q.size() == 0
        finally:
            q.shutdown()


# ---------------------------------------------------------------------------
# Buffer Stats Tests
# ---------------------------------------------------------------------------
class TestBufferStats:
    """Tests for buffer_stats monitoring method."""

    def test_stats_basic(self):
        q = TelemetryQueue(max_size=100)
        q.enqueue({"a": 1})
        stats = q.buffer_stats()
        assert stats["size"] == 1
        assert stats["max_size"] == 100
        assert stats["is_full"] is False
        assert stats["is_empty"] is False
        assert stats["utilization"] == 1.0
        assert stats["persist_enabled"] is False
        assert "persistence" not in stats

    def test_stats_empty_queue(self):
        q = TelemetryQueue(max_size=100)
        stats = q.buffer_stats()
        assert stats["size"] == 0
        assert stats["is_empty"] is True
        assert stats["utilization"] == 0

    def test_stats_full_queue(self):
        q = TelemetryQueue(max_size=2)
        q.enqueue({"a": 1})
        q.enqueue({"b": 2})
        stats = q.buffer_stats()
        assert stats["is_full"] is True
        assert stats["utilization"] == 100.0

    def test_stats_with_persistence_enabled(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            stats = q.buffer_stats()
            assert stats["persist_enabled"] is True
            assert "persistence" in stats
            assert stats["persistence"]["writes_attempted"] == 0
            assert stats["persistence"]["writes_succeeded"] == 0
            assert stats["persistence"]["writes_failed"] == 0
            assert stats["persistence"]["disk_full_skipped"] == 0
            assert stats["persistence"]["corruptions_detected"] == 0
            assert stats["persistence"]["items_recovered_on_startup"] == 0
        finally:
            q.shutdown()


# ---------------------------------------------------------------------------
# Overflow and Disk Persistence Tests
# ---------------------------------------------------------------------------
class TestOverflowPersistence:
    """Tests for queue overflow triggering disk persistence."""

    def test_overflow_triggers_persist(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(max_size=2, persist=True, persist_path=persist_file)
        try:
            q.enqueue({"id": 1})
            q.enqueue({"id": 2})
            # Overflow - should trigger persist
            q.enqueue({"id": 3})
            assert q._persist_writes == 1
            # Flush queue to ensure write completes
            q._save_to_disk_sync()
        finally:
            q.shutdown()

    def test_no_persist_on_enqueue_without_overflow(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(max_size=10, persist=True, persist_path=persist_file)
        try:
            q.enqueue({"id": 1})
            assert q._persist_writes == 0
        finally:
            q.shutdown()

    def test_overflow_drops_oldest_without_memory_leak(self):
        """Test overflow eviction doesn't retain references to dropped items (memory leak prevention).

        Verifies that when queue overflows and drops oldest items, the internal queue
        data structure (deque) does not retain references to the dropped items, allowing
        them to be garbage collected. This is critical for long-running telemetry SDKs.
        """
        import sys
        import gc

        q = TelemetryQueue(max_size=3)

        # Create a test object and track its reference count
        test_item = {"data": "x" * 1000, "id": "overflow_test"}
        initial_refcount = sys.getrefcount(test_item)

        # Add to queue (refcount increases by 1 - stored in deque)
        q.enqueue(test_item)
        in_queue_refcount = sys.getrefcount(test_item)
        assert in_queue_refcount == initial_refcount + 1, "Item should be referenced by queue"

        # Fill queue to trigger overflow eviction
        q.enqueue({"id": 2})
        q.enqueue({"id": 3})
        q.enqueue({"id": 4})  # This should evict test_item

        # Force garbage collection
        gc.collect()

        # Verify test_item is no longer in queue and refcount decreased
        final_refcount = sys.getrefcount(test_item)
        assert (
            final_refcount == initial_refcount
        ), f"Dropped item still has queue reference (refcount: {final_refcount}, expected: {initial_refcount})"

        # Verify test_item is not in queue
        assert test_item not in list(q._queue), "Dropped item should not be in queue"


# ---------------------------------------------------------------------------
# Disk Space Check Tests
# ---------------------------------------------------------------------------
class TestDiskSpaceCheck:
    """Tests for _check_disk_space method."""

    def test_check_disk_space_sufficient(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file, min_disk_space_mb=0.001)
        assert q._check_disk_space() is True

    def test_check_disk_space_no_persist_path(self):
        q = TelemetryQueue()
        assert q._check_disk_space() is True

    @patch("mca_sdk.buffering.queue.shutil.disk_usage")
    def test_check_disk_space_low_space(self, mock_usage, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file, min_disk_space_mb=100.0)
        # Simulate only 1MB free
        mock_usage.return_value = Mock(free=1 * 1024 * 1024)
        assert q._check_disk_space() is False

    @patch("mca_sdk.buffering.queue.shutil.disk_usage")
    def test_check_disk_space_exception_returns_false(self, mock_usage, tmp_path):
        """Fail-closed: returns False if check fails."""
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file)
        mock_usage.side_effect = OSError("permission denied")
        assert q._check_disk_space() is False


# ---------------------------------------------------------------------------
# Save To Disk Tests
# ---------------------------------------------------------------------------
class TestSaveToDisk:
    """Tests for _save_to_disk (async) and _save_to_disk_sync."""

    def test_save_to_disk_no_persist(self):
        q = TelemetryQueue()
        # Should be no-op, not raise
        q._save_to_disk()

    def test_save_to_disk_disk_full_increments_counter(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            with patch.object(q, "_check_disk_space", return_value=False):
                q._save_to_disk()
                assert q._persist_disk_full_count == 1
        finally:
            q.shutdown()

    def test_save_to_disk_write_queue_full(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            # Fill the write queue
            with patch.object(q._write_queue, "put_nowait", side_effect=queue_module.Full):
                q._save_to_disk()
                # Should not raise
        finally:
            q.shutdown()

    def test_save_to_disk_sync(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            q.enqueue({"test": "data"})
            q._save_to_disk_sync()
            # File should exist after sync write
            assert os.path.exists(persist_file)

            # Verify JSON content
            with open(persist_file, "r") as f:
                data = json.load(f)
            assert data["_queue_format_version"] == "1.0"
            assert len(data["items"]) == 1
        finally:
            q.shutdown()


# ---------------------------------------------------------------------------
# Save Snapshot To Disk Tests
# ---------------------------------------------------------------------------
class TestSaveSnapshotToDisk:
    """Tests for _save_snapshot_to_disk method."""

    def test_save_snapshot_no_persist_path(self):
        q = TelemetryQueue()
        q._persist_path = None
        # Should be no-op
        q._save_snapshot_to_disk([{"a": 1}])

    def test_save_snapshot_creates_directory(self, tmp_path):
        persist_file = str(tmp_path / "subdir" / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file)
        q._save_snapshot_to_disk([{"test": "item"}])
        assert os.path.exists(persist_file)

    def test_save_snapshot_atomic_write(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file)
        snapshot = [{"id": 1}, {"id": 2}]
        q._save_snapshot_to_disk(snapshot)

        with open(persist_file, "r") as f:
            data = json.load(f)
        assert len(data["items"]) == 2
        assert q._persist_success_count == 1

    def test_save_snapshot_failure_increments_counter(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file)

        # Mock os.open (used in Finding #8 TOCTOU fix) to simulate failure
        with patch("mca_sdk.buffering.queue.os.open", side_effect=OSError("fail")):
            q._save_snapshot_to_disk([{"a": 1}])
        assert q._persist_failure_count == 1

    def test_save_snapshot_disk_full_enospc(self, tmp_path):
        import errno

        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=False, persist_path=persist_file)

        error = OSError("No space left")
        error.errno = errno.ENOSPC

        with patch("mca_sdk.buffering.queue.os.fdopen", side_effect=error):
            with patch(
                "mca_sdk.buffering.queue.tempfile.mkstemp",
                return_value=(99, str(tmp_path / ".tmp")),
            ):
                q._save_snapshot_to_disk([{"a": 1}])

        assert q._persist_disk_full_count == 1
        assert q._persist_failure_count == 1


# ---------------------------------------------------------------------------
# Cleanup Temp File Tests
# ---------------------------------------------------------------------------
class TestCleanupTempFile:
    """Tests for _cleanup_temp_file static method."""

    def test_cleanup_existing_temp_file(self, tmp_path):
        temp_file = str(tmp_path / "temp.tmp")
        with open(temp_file, "w") as f:
            f.write("test")
        TelemetryQueue._cleanup_temp_file(temp_file)
        assert not os.path.exists(temp_file)

    def test_cleanup_nonexistent_file(self, tmp_path):
        TelemetryQueue._cleanup_temp_file(str(tmp_path / "nonexistent.tmp"))
        # Should not raise

    def test_cleanup_none_path(self):
        TelemetryQueue._cleanup_temp_file(None)
        # Should not raise

    def test_cleanup_permission_error(self, tmp_path):
        temp_file = str(tmp_path / "temp.tmp")
        with open(temp_file, "w") as f:
            f.write("test")
        with patch("mca_sdk.buffering.queue.os.unlink", side_effect=PermissionError("denied")):
            TelemetryQueue._cleanup_temp_file(temp_file)
            # Should not raise, just log warning


# ---------------------------------------------------------------------------
# Serialize Item Tests
# ---------------------------------------------------------------------------
class TestSerializeItem:
    """Tests for _serialize_item method."""

    def test_serialize_dict_passthrough(self):
        q = TelemetryQueue()
        item = {"metric": "test", "value": 42}
        assert q._serialize_item(item) == item

    def test_serialize_otel_object_with_attributes(self):
        q = TelemetryQueue()
        mock_item = Mock()
        mock_item.__class__.__name__ = "MetricData"
        mock_item.attributes = {"key": "value"}
        mock_item.timestamp = 12345
        mock_item.name = "test_metric"
        del mock_item.resource  # Remove resource attribute
        result = q._serialize_item(mock_item)
        assert result["type"] == "MetricData"
        assert result["attributes"] == {"key": "value"}
        assert result["name"] == "test_metric"

    def test_serialize_otel_object_with_resource(self):
        q = TelemetryQueue()
        mock_item = Mock()
        mock_item.__class__.__name__ = "MetricData"
        mock_item.attributes = {"key": "val"}
        mock_item.timestamp = None
        mock_item.name = "m"
        mock_item.resource.attributes = {"service.name": "test"}
        result = q._serialize_item(mock_item)
        assert result["resource"] == {"service.name": "test"}

    def test_serialize_object_with_dict(self):
        q = TelemetryQueue()

        class CustomObj:
            def __init__(self):
                self.field = "value"

        obj = CustomObj()
        result = q._serialize_item(obj)
        assert result["type"] == "CustomObj"
        assert result["field"] == "value"

    def test_serialize_primitive(self):
        q = TelemetryQueue()
        assert q._serialize_item(42) == {"value": 42}
        assert q._serialize_item("hello") == {"value": "hello"}

    def test_serialize_otel_attributes_no_items(self):
        """Test OTel object with attributes that has no items method (e.g. tuple)."""
        q = TelemetryQueue()
        mock_item = Mock()
        mock_item.__class__.__name__ = "SpanData"
        mock_item.attributes = (1, 2, 3)  # No .items() method
        mock_item.timestamp = None
        del mock_item.name
        del mock_item.resource
        result = q._serialize_item(mock_item)
        assert result["attributes"] == {}  # Fallback


# ---------------------------------------------------------------------------
# Validate Item Tests
# ---------------------------------------------------------------------------
class TestValidateItem:
    """Tests for _validate_item method."""

    def test_valid_dict(self):
        q = TelemetryQueue()
        assert q._validate_item({"key": "value"}) is True

    def test_invalid_non_dict(self):
        q = TelemetryQueue()
        assert q._validate_item("string") is False
        assert q._validate_item(42) is False
        assert q._validate_item([1, 2]) is False

    def test_invalid_empty_dict(self):
        q = TelemetryQueue()
        assert q._validate_item({}) is False

    def test_invalid_type_field(self):
        q = TelemetryQueue()
        assert q._validate_item({"type": 123}) is False

    def test_valid_with_version_field(self):
        """User data can have 'version' field (not confused with format version)."""
        q = TelemetryQueue()
        assert q._validate_item({"version": "1.0", "data": "x"}) is True
        assert q._validate_item({"version": 123, "data": "x"}) is True


# ---------------------------------------------------------------------------
# Load From Disk Tests
# ---------------------------------------------------------------------------
class TestLoadFromDisk:
    """Tests for _load_from_disk method."""

    def test_load_no_persist_path(self):
        q = TelemetryQueue()
        q._load_from_disk()  # Should be no-op

    def test_load_file_not_exists(self, tmp_path):
        q = TelemetryQueue(persist=False, persist_path=str(tmp_path / "nonexistent.json"))
        q._load_from_disk()  # Should be no-op

    def test_load_valid_json(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        data = {
            "_queue_format_version": "1.0",
            "items": [{"metric": "test"}, {"metric": "test2"}],
            "max_size": 1000,
        }
        with open(persist_file, "w") as f:
            json.dump(data, f)

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        q._load_from_disk()
        assert q.size() == 2
        assert q._items_recovered == 2

    def test_load_filters_invalid_items(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        data = {
            "_queue_format_version": "1.0",
            "items": [{"valid": "item"}, {}, "not_a_dict", {"type": 123}],
            "max_size": 1000,
        }
        with open(persist_file, "w") as f:
            json.dump(data, f)

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        q._load_from_disk()
        assert q.size() == 1  # Only the valid dict

    def test_load_corrupt_json_renames_file(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        with open(persist_file, "w") as f:
            f.write("not valid json{{{")

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        q._load_from_disk()
        assert q.size() == 0
        assert q._corruption_count == 1
        # Original file should be renamed
        assert not os.path.exists(persist_file)

    def test_load_invalid_structure(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        with open(persist_file, "w") as f:
            json.dump({"no_items_key": True}, f)

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        q._load_from_disk()
        assert q.size() == 0
        assert q._corruption_count == 1

    def test_load_items_not_list_raises(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        with open(persist_file, "w") as f:
            json.dump({"_queue_format_version": "1.0", "items": "not_a_list"}, f)

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        q._load_from_disk()
        assert q._corruption_count == 1

    def test_load_respects_max_size(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        data = {
            "_queue_format_version": "1.0",
            "items": [{"id": i} for i in range(20)],
            "max_size": 1000,
        }
        with open(persist_file, "w") as f:
            json.dump(data, f)

        q = TelemetryQueue(max_size=5, persist=False, persist_path=persist_file)
        q._load_from_disk()
        # Should only load last max_size items
        assert q.size() == 5

    def test_load_rename_failure_handled(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        with open(persist_file, "w") as f:
            f.write("corrupt")

        q = TelemetryQueue(max_size=100, persist=False, persist_path=persist_file)
        with patch("mca_sdk.buffering.queue.os.rename", side_effect=OSError("rename failed")):
            q._load_from_disk()
        assert q._corruption_count == 1


# ---------------------------------------------------------------------------
# Shutdown Tests
# ---------------------------------------------------------------------------
class TestShutdown:
    """Tests for shutdown method."""

    def test_shutdown_no_persist_is_noop(self):
        q = TelemetryQueue()
        q.shutdown()  # Should not raise

    def test_shutdown_with_persist(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        q.enqueue({"test": "data"})
        q.shutdown()
        assert not q._writer_thread.is_alive()

    def test_shutdown_with_custom_timeout(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file, shutdown_timeout=2.0)
        q.shutdown(timeout=1.0)

    def test_shutdown_uses_default_timeout(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file, shutdown_timeout=2.0)
        q.shutdown()

    def test_shutdown_handles_exception(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        # Force exception during shutdown
        q._shutdown_event = None
        q.shutdown()  # Should not raise due to exception handling


# ---------------------------------------------------------------------------
# Background Writer Tests
# ---------------------------------------------------------------------------
class TestBackgroundWriter:
    """Tests for _background_writer thread."""

    def test_writer_processes_snapshot(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        try:
            # Enqueue enough to trigger overflow
            q.enqueue({"id": 1})
            # Manually put a snapshot and wait for processing via sync flush
            q._write_queue.put([{"id": 1}])
            q._save_to_disk_sync()
            assert os.path.exists(persist_file)
        finally:
            q.shutdown()

    def test_writer_handles_shutdown_event_signal(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        q.enqueue({"test": "data"})
        q.shutdown()
        # Writer should have exited cleanly
        assert not q._writer_thread.is_alive()

    def test_writer_handles_shutdown_event(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        # Signal shutdown via event (not SHUTDOWN message)
        q._shutdown_event.set()
        q._writer_thread.join(timeout=3.0)
        assert not q._writer_thread.is_alive()

    def test_shutdown_drains_remaining_writes(self, tmp_path):
        """Shutdown drains pending writes before final flush.

        When shutdown_event is set, the writer drains remaining items
        from write_queue before exiting.
        """
        import queue as queue_module

        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        # Stop the auto-started writer
        q._shutdown_event.set()
        q._writer_thread.join(timeout=3.0)

        # Reset for manual test: create a fresh write_queue with controlled content
        q._write_queue = queue_module.Queue()
        q._shutdown_event = threading.Event()
        q._write_queue.put([{"id": "pending_write"}])

        # Set shutdown event and call _background_writer directly
        q._shutdown_event.set()
        q._background_writer()
        assert os.path.exists(persist_file)

    def test_shutdown_with_pending_writes_in_queue(self, tmp_path):
        """Shutdown with items still in write_queue drains them before exit.

        When shutdown_event is set with pending writes, the writer
        drains all items via get_nowait() before final sync flush.
        """
        import queue as queue_module

        persist_file = str(tmp_path / "queue.json")
        q = TelemetryQueue(persist=True, persist_path=persist_file)
        # Stop the auto-started writer
        q._shutdown_event.set()
        q._writer_thread.join(timeout=3.0)

        # Reset for manual test with pending writes
        q._write_queue = queue_module.Queue()
        q._shutdown_event = threading.Event()
        q._write_queue.put([{"id": "pending_1"}])
        q._write_queue.put([{"id": "pending_2"}])

        # Set shutdown and run writer - should drain both pending writes
        q._shutdown_event.set()
        q._background_writer()
        assert os.path.exists(persist_file)


# ---------------------------------------------------------------------------
# Thread Safety Tests
# ---------------------------------------------------------------------------
class TestThreadSafety:
    """Tests for concurrent queue operations."""

    @pytest.mark.timeout(5)
    def test_concurrent_enqueue_dequeue(self):
        """Test concurrent enqueue/dequeue with deterministic race condition using threading.Barrier.

        Uses threading.Barrier to force simultaneous execution and expose potential race conditions.
        Timeout protection: Fails after 5 seconds to prevent CI deadlock if code introduces a deadlock.
        """
        q = TelemetryQueue(max_size=1000)
        errors = []
        num_threads = 3
        barrier = threading.Barrier(num_threads)  # Force simultaneous start

        def producer():
            try:
                barrier.wait()  # Synchronize thread start for deterministic race condition
                for i in range(100):
                    q.enqueue({"id": i})
            except Exception as e:
                errors.append(e)

        def consumer():
            try:
                barrier.wait()  # Synchronize thread start for deterministic race condition
                for _ in range(100):
                    q.dequeue()
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=producer),
            threading.Thread(target=producer),
            threading.Thread(target=consumer),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0

    @pytest.mark.timeout(5)
    def test_concurrent_enqueue_batch_dequeue(self):
        """Test concurrent enqueue with batch dequeue using threading.Barrier.

        Timeout protection: Prevents CI deadlock if code introduces a race condition.
        """
        q = TelemetryQueue(max_size=500)
        errors = []
        num_threads = 3
        barrier = threading.Barrier(num_threads)  # Force simultaneous operations

        def producer():
            try:
                barrier.wait()  # Synchronize start
                for i in range(50):
                    q.enqueue({"id": i})
            except Exception as e:
                errors.append(e)

        def batch_consumer():
            try:
                barrier.wait()  # Synchronize start
                for _ in range(10):
                    q.dequeue_batch(5)
            except Exception as e:
                errors.append(e)

        threads = [
            threading.Thread(target=producer),
            threading.Thread(target=batch_consumer),
            threading.Thread(target=producer),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0

    def test_concurrent_stats_during_operations(self):
        q = TelemetryQueue(max_size=100)
        errors = []

        def worker():
            try:
                for _ in range(50):
                    q.enqueue({"x": 1})
                    q.buffer_stats()
                    q.dequeue()
                    q.is_empty()
                    q.is_full()
                    q.peek()
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=worker) for _ in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0


# ---------------------------------------------------------------------------
# Persistence Integration Tests
# ---------------------------------------------------------------------------
class TestPersistenceIntegration:
    """Tests for full persistence round-trip."""

    def test_persist_and_recover(self, tmp_path):
        persist_file = str(tmp_path / "queue.json")

        # Create queue, add items, shutdown (persists)
        q1 = TelemetryQueue(max_size=10, persist=True, persist_path=persist_file)
        q1.enqueue({"metric": "m1"})
        q1.enqueue({"metric": "m2"})
        q1.shutdown()

        # Verify file exists
        assert os.path.exists(persist_file)

        # Create new queue, should recover items
        q2 = TelemetryQueue(max_size=10, persist=True, persist_path=persist_file)
        try:
            assert q2._items_recovered == 2
            assert q2.size() == 2
        finally:
            q2.shutdown()

    def test_hipaa_warning_logged(self, tmp_path, caplog):
        """Verify HIPAA compliance warning is logged when persistence enabled."""
        import logging

        persist_file = str(tmp_path / "queue.json")
        with caplog.at_level(logging.WARNING):
            q = TelemetryQueue(persist=True, persist_path=persist_file)
            q.shutdown()
        assert "HIPAA" in caplog.text or "HIPAA" in caplog.text.upper()
