"""
Integration tests for ESPal daemon architecture.

These tests verify end-to-end functionality with mocked serial devices.
"""

import pytest
import os
import time
import tempfile
import threading
from unittest.mock import Mock, patch, MagicMock

from esppal.daemon import ESPalDaemon
from esppal.client import ESPalClient, DaemonNotRunning
from esppal.config import DaemonConfig
from esppal.buffer import RingBuffer
from esppal.protocol import DataMessage, MarkerMessage, StatusMessage


@pytest.fixture
def temp_paths():
    """Create temporary socket and PID paths."""
    with tempfile.TemporaryDirectory() as tmpdir:
        socket_path = os.path.join(tmpdir, "esppal.sock")
        pid_path = os.path.join(tmpdir, "esppal.pid")
        yield socket_path, pid_path


@pytest.fixture
def mock_serial():
    """Create a mock serial port."""
    with patch('esppal.serial_manager.serial.Serial') as mock:
        port = MagicMock()
        port.is_open = True
        port.read.return_value = b""
        port.in_waiting = 0
        mock.return_value = port
        yield port


@pytest.fixture
def daemon_config(temp_paths):
    """Create daemon config with test paths."""
    socket_path, pid_path = temp_paths

    config = DaemonConfig(
        buffer_size_bytes=1024 * 1024,  # 1MB for tests
        idle_timeout_seconds=60,  # Short timeout
        baudrate=115200,
        vid="test",
        pid="test"
    )
    return config


class TestDaemonClientIntegration:
    """Test daemon and client working together."""

    def test_client_connects_to_daemon(self, temp_paths, mock_serial):
        """Client can connect to running daemon."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig(
                    buffer_size_bytes=1024 * 1024,
                    idle_timeout_seconds=300
                )

                daemon = ESPalDaemon(config)

                # Override paths
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    assert daemon.start()

                    # Give daemon time to start
                    time.sleep(0.2)

                    # Client connects
                    client = ESPalClient(auto_spawn=False)
                    client._socket_path = socket_path
                    client._pid_path = pid_path

                    assert client.connect()
                    assert client.connected

                    client.disconnect()
                finally:
                    daemon.stop()

    def test_client_gets_status(self, temp_paths, mock_serial):
        """Client can retrieve daemon status."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    client = ESPalClient(auto_spawn=False)
                    client._socket_path = socket_path
                    client._pid_path = pid_path

                    status = client.status()

                    assert status is not None
                    assert isinstance(status, StatusMessage)
                    assert status.clients >= 0
                    assert status.uptime > 0

                    client.disconnect()
                finally:
                    daemon.stop()

    def test_client_shutdown_daemon(self, temp_paths, mock_serial):
        """Client can shut down daemon."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    client = ESPalClient(auto_spawn=False)
                    client._socket_path = socket_path
                    client._pid_path = pid_path

                    assert client.shutdown()

                    # Give daemon time to shut down
                    time.sleep(0.5)

                    assert not daemon.is_running

                    client.disconnect()
                finally:
                    if daemon.is_running:
                        daemon.stop()


class TestBufferDataFlow:
    """Test data flow through the buffer."""

    def test_data_added_to_buffer(self, temp_paths, mock_serial):
        """Data added to daemon buffer is accessible."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    # Add data directly to daemon buffer
                    daemon.add_data("serial", b"test data")

                    # Verify buffer has data
                    assert daemon._buffer.size > 0
                    assert daemon._buffer.entry_count > 0

                finally:
                    daemon.stop()

    def test_marker_added_to_buffer(self, temp_paths, mock_serial):
        """Markers are added to buffer."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    # Add marker
                    daemon.add_marker("test_marker")

                    # Verify marker exists
                    markers = daemon._buffer.get_markers()
                    assert "test_marker" in markers

                finally:
                    daemon.stop()


class TestMultipleClients:
    """Test multiple clients connecting simultaneously."""

    def test_two_clients_connect(self, temp_paths, mock_serial):
        """Multiple clients can connect to daemon."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    # First client
                    client1 = ESPalClient(auto_spawn=False)
                    client1._socket_path = socket_path
                    client1._pid_path = pid_path
                    assert client1.connect()

                    # Second client
                    client2 = ESPalClient(auto_spawn=False)
                    client2._socket_path = socket_path
                    client2._pid_path = pid_path
                    assert client2.connect()

                    # Both connected
                    assert client1.connected
                    assert client2.connected

                    # Daemon shows 2 clients
                    time.sleep(0.1)
                    assert daemon.client_count == 2

                    client1.disconnect()
                    client2.disconnect()
                finally:
                    daemon.stop()

    def test_client_disconnect_updates_count(self, temp_paths, mock_serial):
        """Client disconnect updates client count."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon = ESPalDaemon(config)
                daemon._socket_path = socket_path
                daemon._pid_path = pid_path

                try:
                    daemon.start()
                    time.sleep(0.2)

                    client1 = ESPalClient(auto_spawn=False)
                    client1._socket_path = socket_path
                    client1._pid_path = pid_path
                    client1.connect()

                    client2 = ESPalClient(auto_spawn=False)
                    client2._socket_path = socket_path
                    client2._pid_path = pid_path
                    client2.connect()

                    time.sleep(0.1)
                    assert daemon.client_count == 2

                    client1.disconnect()
                    time.sleep(0.2)

                    assert daemon.client_count == 1

                    client2.disconnect()
                finally:
                    daemon.stop()


class TestDaemonNotRunning:
    """Test behavior when daemon is not running."""

    def test_client_fails_without_daemon(self, temp_paths):
        """Client connection fails when daemon not running."""
        socket_path, pid_path = temp_paths

        client = ESPalClient(auto_spawn=False)
        client._socket_path = socket_path
        client._pid_path = pid_path

        with pytest.raises(DaemonNotRunning):
            client.connect()

    def test_status_returns_none_without_daemon(self, temp_paths):
        """Status returns None when daemon not running."""
        socket_path, pid_path = temp_paths

        client = ESPalClient(auto_spawn=False)
        client._socket_path = socket_path
        client._pid_path = pid_path

        # status() should raise DaemonNotRunning or return None
        try:
            result = client.status()
            assert result is None
        except DaemonNotRunning:
            pass  # This is also acceptable


class TestDaemonDuplicatePrevention:
    """Test that only one daemon can run per device."""

    def test_second_daemon_fails_to_start(self, temp_paths, mock_serial):
        """Second daemon instance fails to start."""
        socket_path, pid_path = temp_paths

        with patch('esppal.config.get_socket_path', return_value=socket_path):
            with patch('esppal.config.get_pid_path', return_value=pid_path):
                config = DaemonConfig()
                daemon1 = ESPalDaemon(config)
                daemon1._socket_path = socket_path
                daemon1._pid_path = pid_path

                daemon2 = ESPalDaemon(config)
                daemon2._socket_path = socket_path
                daemon2._pid_path = pid_path

                try:
                    assert daemon1.start()
                    time.sleep(0.2)

                    # Second daemon should fail
                    assert not daemon2.start()

                finally:
                    daemon1.stop()
                    daemon2.stop()
