import pytest
from unittest.mock import patch


@pytest.fixture(autouse=True)
def mock_registry_client():
    with patch("mca_sdk.registry.client.RegistryClient._fetch_model_config_internal") as mock_fetch:
        # Prevent TypeError: '>=' not supported between instances of 'MagicMock' and 'int'
        from mca_sdk.registry.models import ModelConfig

        mock_fetch.return_value = ModelConfig(
            model_id="test",
            model_version="1.0",
            service_name="test-service",
            team_name="test-team",
            model_category="internal",
            model_type="regression",
        )
        yield mock_fetch


"""Unit tests for code review fixes from Story 3-4 review.

Tests validate fixes for:
- Multi-exporter unique persist paths
- Item validation during recovery
- Final metrics emission on shutdown
- Disk space TOCTOU handling
- Configurable shutdown timeout
"""

import errno
import json
import os
import tempfile
import time
from unittest.mock import Mock

import pytest
import sys
from pathlib import Path

try:
    from mca_sdk.buffering.queue import TelemetryQueue
    from mca_sdk.buffering.exporters import (
        BufferedOTLPMetricExporter,
        BufferedOTLPLogExporter,
        BufferedOTLPSpanExporter,
    )
except ImportError:
    mca_prototype_dir = Path(__file__).parent.parent.parent / "mca-prototype"
    if mca_prototype_dir.exists():
        sys.path.insert(0, str(mca_prototype_dir))
    from mca_sdk.buffering.queue import TelemetryQueue
    from mca_sdk.buffering.exporters import (
        BufferedOTLPMetricExporter,
        BufferedOTLPLogExporter,
        BufferedOTLPSpanExporter,
    )


class TestMultiExporterUniquePaths:
    """Test that each exporter type gets unique default persist path."""

    def test_metrics_exporter_default_path(self):
        """Test: Metrics exporter uses queue_metrics.json by default."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Mock expanduser to use tmpdir
            with patch(
                "os.path.expanduser",
                return_value=os.path.join(tmpdir, "queue_metrics.json"),
            ):
                exporter = BufferedOTLPMetricExporter(
                    persist_queue=True,
                    persist_path=None,  # Use default
                )

                # Check that queue has metrics-specific path
                assert exporter._queue._persist_path.endswith("queue_metrics.json")
                exporter.shutdown()

    def test_logs_exporter_default_path(self):
        """Test: Logs exporter uses queue_logs.json by default."""
        with tempfile.TemporaryDirectory() as tmpdir:
            with patch(
                "os.path.expanduser",
                return_value=os.path.join(tmpdir, "queue_logs.json"),
            ):
                exporter = BufferedOTLPLogExporter(persist_queue=True, persist_path=None)

                assert exporter._queue._persist_path.endswith("queue_logs.json")
                exporter.shutdown()

    def test_traces_exporter_default_path(self):
        """Test: Traces exporter uses queue_traces.json by default."""
        with tempfile.TemporaryDirectory() as tmpdir:
            with patch(
                "os.path.expanduser",
                return_value=os.path.join(tmpdir, "queue_traces.json"),
            ):
                exporter = BufferedOTLPSpanExporter(persist_queue=True, persist_path=None)

                assert exporter._queue._persist_path.endswith("queue_traces.json")
                exporter.shutdown()

    def test_all_three_exporters_use_different_paths(self):
        """Test: Three exporters can coexist without file contention."""
        with tempfile.TemporaryDirectory() as tmpdir:
            metrics_path = os.path.join(tmpdir, "queue_metrics.json")
            logs_path = os.path.join(tmpdir, "queue_logs.json")
            traces_path = os.path.join(tmpdir, "queue_traces.json")

            # Create all three exporters
            metrics_exporter = BufferedOTLPMetricExporter(
                persist_queue=True, persist_path=metrics_path
            )
            logs_exporter = BufferedOTLPLogExporter(persist_queue=True, persist_path=logs_path)
            traces_exporter = BufferedOTLPSpanExporter(persist_queue=True, persist_path=traces_path)

            # Trigger overflow on all three (to persist)
            for i in range(1001):
                metrics_exporter._queue.enqueue({"metric": i})
                logs_exporter._queue.enqueue({"log": i})
                traces_exporter._queue.enqueue({"span": i})

            # Shutdown to flush
            metrics_exporter.shutdown()
            logs_exporter.shutdown()
            traces_exporter.shutdown()

            # All three files should exist independently
            assert os.path.exists(metrics_path), "Metrics queue should be persisted"
            assert os.path.exists(logs_path), "Logs queue should be persisted"
            assert os.path.exists(traces_path), "Traces queue should be persisted"


class TestItemValidation:
    """Test item validation during recovery."""

    def test_valid_items_loaded(self):
        """Test: Valid items are loaded from disk."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            # Create queue with valid items
            queue_data = {
                "version": "1.0",
                "items": [
                    {"type": "metric", "value": 1},
                    {"type": "metric", "value": 2},
                    {"type": "metric", "value": 3},
                ],
                "max_size": 1000,
            }
            with open(persist_path, "w") as f:
                json.dump(queue_data, f)

            # Load queue
            queue = TelemetryQueue(max_size=1000, persist=True, persist_path=persist_path)

            # Should have loaded all 3 items
            stats = queue.buffer_stats()
            assert stats["persistence"]["items_recovered_on_startup"] == 3

            queue.shutdown()

    def test_invalid_items_skipped(self):
        """Test: Invalid items are skipped with warning."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            # Create queue with mix of valid and invalid items
            queue_data = {
                "version": "1.0",
                "items": [
                    {"type": "metric", "value": 1},  # valid
                    "invalid string item",  # INVALID - not a dict
                    {},  # INVALID - empty dict
                    {"type": "metric", "value": 2},  # valid
                    None,  # INVALID - not a dict
                    {"type": "metric", "value": 3},  # valid
                ],
                "max_size": 1000,
            }
            with open(persist_path, "w") as f:
                json.dump(queue_data, f)

            # Load queue
            queue = TelemetryQueue(max_size=1000, persist=True, persist_path=persist_path)

            # Should have loaded only valid items (3)
            stats = queue.buffer_stats()
            assert stats["persistence"]["items_recovered_on_startup"] == 3

            queue.shutdown()


class TestDiskSpaceTOCTOU:
    """Test disk space TOCTOU race condition handling."""

    def test_disk_full_during_write_updates_metrics(self):
        """Test: Disk full error during write increments disk_full_count."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            queue = TelemetryQueue(max_size=5, persist=True, persist_path=persist_path)

            # Mock os.replace to raise ENOSPC error (simulates disk full during atomic write)
            with patch("os.replace") as mock_replace:
                error = OSError()
                error.errno = errno.ENOSPC
                mock_replace.side_effect = error

                # Trigger overflow
                for i in range(10):
                    queue.enqueue({"id": i})

                time.sleep(0.5)  # Let background writer process

                # Check that disk_full counter was incremented
                stats = queue.buffer_stats()
                assert stats["persistence"]["disk_full_skipped"] >= 1

            queue.shutdown()


class TestConfigurableShutdownTimeout:
    """Test configurable shutdown timeout."""

    def test_default_shutdown_timeout(self):
        """Test: Default shutdown timeout is 5.0 seconds."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            queue = TelemetryQueue(max_size=10, persist=True, persist_path=persist_path)

            # Default should be 5.0
            assert queue._shutdown_timeout == 5.0

            queue.shutdown()

    def test_custom_shutdown_timeout(self):
        """Test: Custom shutdown timeout can be set."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            queue = TelemetryQueue(
                max_size=10,
                persist=True,
                persist_path=persist_path,
                shutdown_timeout=10.0,  # Custom timeout
            )

            assert queue._shutdown_timeout == 10.0

            queue.shutdown()

    def test_shutdown_uses_instance_timeout(self):
        """Test: Shutdown uses instance timeout if not specified."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            queue = TelemetryQueue(
                max_size=10,
                persist=True,
                persist_path=persist_path,
                shutdown_timeout=15.0,
            )

            # Mock join to verify timeout value
            with patch.object(queue._writer_thread, "join") as mock_join:
                queue.shutdown()  # No timeout arg
                mock_join.assert_called_once()
                # Should use instance default (15.0)
                assert mock_join.call_args[1]["timeout"] == 15.0


class TestFinalMetricsEmission:
    """Test that final metrics are emitted on shutdown."""

    def test_metrics_exporter_emits_on_shutdown(self):
        """Test: Metrics exporter emits final persistence metrics on shutdown."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            exporter = BufferedOTLPMetricExporter(persist_queue=True, persist_path=persist_path)

            # Set up meter to enable metrics
            mock_meter = Mock()
            exporter.set_meter(mock_meter)

            # Mock _emit_persistence_metrics to verify it's called
            with patch.object(exporter, "_emit_persistence_metrics") as mock_emit:
                exporter.shutdown()

                # Should have called emit before shutdown
                assert mock_emit.call_count >= 1, "Should emit final metrics on shutdown"


class TestPHIProtection:
    """Test PHI protection fixes."""

    def test_json_dump_no_default_str(self):
        """Test: json.dump no longer uses default=str (PHI leak fix)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            persist_path = os.path.join(tmpdir, "queue.json")

            queue = TelemetryQueue(max_size=5, persist=True, persist_path=persist_path)

            # Add items that would fail without default=str
            # But should still work with proper serialization
            queue.enqueue({"id": 1, "name": "test"})

            # Trigger overflow
            for i in range(10):
                queue.enqueue({"id": i})

            queue.shutdown()

            # Verify file was created
            assert os.path.exists(persist_path)

            # Verify file contains valid JSON (not stringified objects)
            with open(persist_path, "r") as f:
                data = json.load(f)
                # Should have items as dicts, not stringified
                assert isinstance(data["items"], list)
                if data["items"]:
                    # Items should be dicts
                    for item in data["items"]:
                        assert isinstance(item, dict)
