"""
Tests for the daemon module.
"""

import pytest
import os
import socket
import threading
import time
import tempfile
import signal
from esppal.daemon import ESPalDaemon
from esppal.config import DaemonConfig, get_socket_path, get_pid_path
from esppal.protocol import (
    serialize, parse_json_line, MessageFramer,
    StatusCommand, ShutdownCommand
)


@pytest.fixture
def temp_paths():
    """Create temporary socket and PID paths."""
    fd, base_path = tempfile.mkstemp()
    os.close(fd)
    os.unlink(base_path)

    socket_path = base_path + ".sock"
    pid_path = base_path + ".pid"

    yield socket_path, pid_path

    # Clean up
    for path in [socket_path, pid_path]:
        if os.path.exists(path):
            os.unlink(path)


@pytest.fixture
def config(temp_paths):
    """Create test configuration."""
    socket_path, pid_path = temp_paths
    cfg = DaemonConfig(
        buffer_size_bytes=1024 * 1024,  # 1MB
        idle_timeout_seconds=5,  # Short for testing
        vid="test",
        pid="1234"
    )
    return cfg


@pytest.fixture
def daemon(config, temp_paths, monkeypatch):
    """Create a daemon instance with temp paths."""
    socket_path, pid_path = temp_paths

    # Monkey-patch the path functions to use temp paths
    monkeypatch.setattr("esppal.daemon.get_socket_path",
                        lambda *args, **kwargs: socket_path)
    monkeypatch.setattr("esppal.daemon.get_pid_path",
                        lambda *args, **kwargs: pid_path)

    d = ESPalDaemon(config)
    yield d

    # Ensure cleanup
    if d.is_running:
        d.stop()


def connect_client(socket_path: str) -> socket.socket:
    """Connect a client to the daemon."""
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.settimeout(5.0)
    sock.connect(socket_path)
    return sock


def send_command(sock: socket.socket, cmd) -> None:
    """Send a command."""
    sock.sendall(serialize(cmd))


def recv_response(sock: socket.socket) -> dict:
    """Receive a response."""
    framer = MessageFramer()
    while True:
        data = sock.recv(4096)
        if not data:
            raise ConnectionError("Connection closed")
        for line in framer.feed(data):
            return parse_json_line(line)


class TestDaemonLifecycle:
    """Tests for daemon startup and shutdown."""

    def test_daemon_starts(self, daemon, temp_paths):
        """Daemon starts and creates files."""
        socket_path, pid_path = temp_paths

        assert daemon.start()
        assert daemon.is_running
        assert os.path.exists(socket_path)
        assert os.path.exists(pid_path)

        daemon.stop()

    def test_pid_file_created(self, daemon, temp_paths):
        """PID file contains correct PID."""
        socket_path, pid_path = temp_paths

        daemon.start()

        with open(pid_path, "r") as f:
            pid = int(f.read().strip())

        assert pid == os.getpid()

        daemon.stop()

    def test_socket_created(self, daemon, temp_paths):
        """Socket accepts connections."""
        socket_path, pid_path = temp_paths

        daemon.start()

        sock = connect_client(socket_path)
        time.sleep(0.1)

        assert daemon.client_count == 1

        sock.close()
        daemon.stop()

    def test_stop_removes_files(self, daemon, temp_paths):
        """Stop removes socket and PID files."""
        socket_path, pid_path = temp_paths

        daemon.start()
        daemon.stop()

        assert not daemon.is_running
        assert not os.path.exists(pid_path)
        # Socket file removal handled by IPC server

    def test_duplicate_prevention(self, config, temp_paths, monkeypatch):
        """Second daemon fails to start."""
        socket_path, pid_path = temp_paths

        monkeypatch.setattr("esppal.daemon.get_socket_path",
                            lambda *args, **kwargs: socket_path)
        monkeypatch.setattr("esppal.daemon.get_pid_path",
                            lambda *args, **kwargs: pid_path)

        daemon1 = ESPalDaemon(config)
        daemon2 = ESPalDaemon(config)

        try:
            assert daemon1.start()
            assert not daemon2.start()  # Should fail
        finally:
            daemon1.stop()


class TestStatusCommand:
    """Tests for status command."""

    def test_status_command(self, daemon, temp_paths):
        """Status returns valid response."""
        socket_path, pid_path = temp_paths

        daemon.start()

        sock = connect_client(socket_path)
        send_command(sock, StatusCommand())
        response = recv_response(sock)

        assert response["type"] == "status"
        assert "uptime" in response
        assert "clients" in response
        assert response["clients"] >= 1  # At least this client
        assert "buffer_bytes" in response
        assert "idle_timeout" in response

        sock.close()
        daemon.stop()

    def test_status_shows_vidpid(self, daemon, temp_paths):
        """Status includes VID:PID."""
        socket_path, pid_path = temp_paths

        daemon.start()

        sock = connect_client(socket_path)
        send_command(sock, StatusCommand())
        response = recv_response(sock)

        assert response["vid_pid"] == "test:1234"

        sock.close()
        daemon.stop()


class TestIdleTimeout:
    """Tests for idle timeout."""

    def test_idle_timeout_detection(self, config, temp_paths, monkeypatch):
        """Daemon detects when idle timeout exceeded."""
        socket_path, pid_path = temp_paths

        monkeypatch.setattr("esppal.daemon.get_socket_path",
                            lambda *args, **kwargs: socket_path)
        monkeypatch.setattr("esppal.daemon.get_pid_path",
                            lambda *args, **kwargs: pid_path)

        # Use very short timeout
        config.idle_timeout_seconds = 0.5

        daemon = ESPalDaemon(config)
        daemon.start()

        try:
            # Initially, time until shutdown should be positive
            assert daemon.time_until_idle_shutdown > 0

            # Wait for idle time to exceed timeout
            time.sleep(0.7)

            # Now time until shutdown should be 0 (exceeded)
            assert daemon.time_until_idle_shutdown == 0
            assert daemon.idle_time >= 0.5
        finally:
            daemon.stop()

    def test_client_resets_idle(self, daemon, temp_paths):
        """Client activity resets idle timer."""
        socket_path, pid_path = temp_paths

        daemon.start()
        initial_idle = daemon.idle_time

        time.sleep(0.1)
        assert daemon.idle_time > initial_idle

        # Connect client
        sock = connect_client(socket_path)
        time.sleep(0.1)

        # Idle time should be reset
        assert daemon.idle_time < 0.5

        sock.close()
        daemon.stop()


class TestShutdownCommand:
    """Tests for shutdown command."""

    def test_shutdown_via_command(self, daemon, temp_paths):
        """Daemon shuts down on shutdown command."""
        socket_path, pid_path = temp_paths

        daemon.start()

        sock = connect_client(socket_path)
        send_command(sock, ShutdownCommand())

        # Wait for shutdown
        time.sleep(0.5)

        assert not daemon.is_running

        sock.close()


class TestDataHandling:
    """Tests for data and marker handling."""

    def test_add_data(self, daemon, temp_paths):
        """Data can be added to buffer."""
        socket_path, pid_path = temp_paths

        daemon.start()

        seq = daemon.add_data("serial", b"test data")
        assert seq >= 0

        # Verify in status
        sock = connect_client(socket_path)
        send_command(sock, StatusCommand())
        response = recv_response(sock)

        assert response["buffer_entries"] >= 1

        sock.close()
        daemon.stop()

    def test_add_marker(self, daemon, temp_paths):
        """Markers can be added to buffer."""
        socket_path, pid_path = temp_paths

        daemon.start()

        seq = daemon.add_marker("test_marker")
        assert seq >= 0

        daemon.stop()


class TestUptimeTracking:
    """Tests for uptime tracking."""

    def test_uptime_increases(self, daemon, temp_paths):
        """Uptime increases over time."""
        socket_path, pid_path = temp_paths

        daemon.start()

        initial_uptime = daemon.uptime
        time.sleep(0.2)
        new_uptime = daemon.uptime

        assert new_uptime > initial_uptime

        daemon.stop()

    def test_time_until_idle(self, daemon, temp_paths):
        """Time until idle shutdown calculated correctly."""
        socket_path, pid_path = temp_paths

        daemon.start()

        # Initial time should be close to full timeout
        remaining = daemon.time_until_idle_shutdown
        assert remaining > 0
        assert remaining <= daemon._config.idle_timeout_seconds

        daemon.stop()
