"""Unit tests for _ipc.py — IPCConnection and fork-safety."""

import json
import os
import socket
import tempfile
import threading
from typing import Generator

import pytest

from tyko._ipc import IPCConnection, _socket_path

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


class _EchoServer:
    """Minimal Unix socket server for testing.

    For each message received, it immediately sends {"ok": true}.
    Runs in a background thread.
    """

    def __init__(self, path: str) -> None:
        self.path = path
        self.received: list[dict] = []
        self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self._sock.bind(path)
        self._sock.listen(1)
        self._thread = threading.Thread(target=self._serve, daemon=True)
        self._thread.start()

    def _serve(self) -> None:
        try:
            conn, _ = self._sock.accept()
            buf = b""
            while True:
                chunk = conn.recv(4096)
                if not chunk:
                    break
                buf += chunk
                while b"\n" in buf:
                    line, buf = buf.split(b"\n", 1)
                    msg = json.loads(line.decode())
                    self.received.append(msg)
                    conn.sendall(b'{"ok": true}\n')
        except Exception:
            pass

    def close(self) -> None:
        self._sock.close()
        try:
            os.unlink(self.path)
        except FileNotFoundError:
            pass


@pytest.fixture
def server_path() -> Generator[str, None, None]:
    """Provide a temp socket path and clean it up after the test."""
    with tempfile.TemporaryDirectory() as tmpdir:
        path = os.path.join(tmpdir, "test.sock")
        yield path


@pytest.fixture
def echo_server(server_path: str) -> Generator[_EchoServer, None, None]:
    server = _EchoServer(server_path)
    yield server
    server.close()


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


def test_socket_path_format():
    path = _socket_path(client_uuid="abc123")
    assert "tyko-abc123.sock" in path


def test_connect_success(echo_server: _EchoServer, server_path: str):
    conn = _connect_to(server_path)
    assert conn._owner_pid == os.getpid()
    conn.close()


def test_connect_fails_when_no_socket():
    with pytest.raises(OSError):
        IPCConnection.connect(client_uuid="nonexistent-uuid-xyz-that-cannot-exist", timeout=0.2)


def test_send_delivers_message(echo_server: _EchoServer, server_path: str):
    conn = _connect_to(server_path)
    conn.send({"type": "log", "values": {"loss": 0.5}})
    conn.close()

    # Give the server thread time to process
    import time

    time.sleep(0.05)

    assert len(echo_server.received) == 1
    assert echo_server.received[0]["type"] == "log"
    assert echo_server.received[0]["values"]["loss"] == 0.5


def test_send_and_wait_returns_true(echo_server: _EchoServer, server_path: str):
    conn = _connect_to(server_path)
    ok = conn.send_and_wait({"type": "close", "status": "completed"}, timeout=2.0)
    conn.close()
    assert ok is True


def test_send_and_wait_receives_message(echo_server: _EchoServer, server_path: str):
    conn = _connect_to(server_path)
    conn.send_and_wait({"type": "shutdown"}, timeout=2.0)
    conn.close()

    import time

    time.sleep(0.05)

    assert any(m["type"] == "shutdown" for m in echo_server.received)


def test_send_is_noop_from_forked_pid(echo_server: _EchoServer, server_path: str, monkeypatch):
    """send() must silently no-op when os.getpid() != owner_pid."""
    conn = _connect_to(server_path)
    # Simulate a fork by making getpid return a different value
    monkeypatch.setattr(os, "getpid", lambda: conn._owner_pid + 9999)

    conn.send({"type": "log", "values": {"loss": 1.0}})
    conn.close()

    import time

    time.sleep(0.05)

    assert len(echo_server.received) == 0


def test_send_and_wait_is_noop_from_forked_pid(echo_server: _EchoServer, server_path: str, monkeypatch):
    conn = _connect_to(server_path)
    monkeypatch.setattr(os, "getpid", lambda: conn._owner_pid + 9999)

    result = conn.send_and_wait({"type": "close", "status": "completed"}, timeout=0.5)
    conn.close()

    assert result is False


def test_send_silent_on_broken_pipe(server_path: str):
    """send() must not raise when the socket is closed on the other end."""
    # Open a server, connect, then close the server immediately
    srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    srv.bind(server_path)
    srv.listen(1)

    conn = _connect_to(server_path)
    # Accept and immediately close
    client_sock, _ = srv.accept()
    client_sock.close()
    srv.close()

    # This should not raise
    conn.send({"type": "log", "values": {}})
    conn.close()


def test_close_is_idempotent(echo_server: _EchoServer, server_path: str):
    conn = _connect_to(server_path)
    conn.close()
    conn.close()  # should not raise


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------


def _connect_to(path: str) -> IPCConnection:
    """Connect to a server at an arbitrary path (bypasses _socket_path)."""
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.settimeout(2.0)
    sock.connect(path)
    sock.settimeout(None)
    return IPCConnection(sock=sock, client_uuid="test", owner_pid=os.getpid())
