import socket
import types
import pytest

from target_simulator.utils import network


class DummySocket:
    def __init__(self, fail_on_bind=False, rcvbuf=4096):
        self._closed = False
        self._fail_on_bind = fail_on_bind
        self._rcvbuf = rcvbuf
        self._timeout = None

    def getsockopt(self, level, optname):
        return self._rcvbuf

    def setsockopt(self, level, optname, value):
        # Simulate refusal if value is absurd (for edge test)
        if value > 10 * 1024 * 1024:
            raise OSError("too large")

    def bind(self, addr):
        if self._fail_on_bind:
            raise socket.error("bind failed")

    def recvfrom(self, bufsize):
        if self._timeout:
            raise socket.timeout()
        return b"data", ("127.0.0.1", 1234)

    def fileno(self):
        return -1 if self._closed else 3

    def close(self):
        if self._closed:
            raise socket.error("already closed")
        self._closed = True


def test_create_udp_socket_success(monkeypatch):
    dummy = DummySocket()

    def fake_socket(family, type_):
        return dummy

    monkeypatch.setattr(network.socket, "socket", fake_socket)

    s = network.create_udp_socket("127.0.0.1", 0)
    # Should return our dummy socket object
    assert s is dummy


def test_create_udp_socket_bind_failure(monkeypatch):
    dummy = DummySocket(fail_on_bind=True)

    def fake_socket(family, type_):
        return dummy

    monkeypatch.setattr(network.socket, "socket", fake_socket)

    s = network.create_udp_socket("127.0.0.1", 0)
    # On bind failure implementation returns None
    assert s is None


def test_create_udp_socket_buf_set_failure(monkeypatch):
    # Simulate setsockopt raising OSError when trying to set very large buffer
    dummy = DummySocket()

    def fake_socket(family, type_):
        return dummy

    # Also patch dummy.setsockopt to raise
    def bad_setsockopt(level, optname, value):
        raise OSError("no permission")

    dummy.setsockopt = bad_setsockopt
    monkeypatch.setattr(network.socket, "socket", fake_socket)

    # Should still return the socket even if buffer set fails
    s = network.create_udp_socket("127.0.0.1", 0)
    assert s is dummy


def test_receive_udp_packet_success_and_timeout(monkeypatch):
    dummy = DummySocket()

    # Normal receive
    data, addr = network.receive_udp_packet(dummy)
    assert data == b"data"
    assert addr == ("127.0.0.1", 1234)

    # Simulate timeout
    dummy._timeout = True
    data, addr = network.receive_udp_packet(dummy)
    assert data is None and addr is None


def test_receive_udp_packet_error(monkeypatch):
    class ErrSock(DummySocket):
        def recvfrom(self, bufsize):
            raise socket.error("recv failed")

    err = ErrSock()
    data, addr = network.receive_udp_packet(err)
    assert data is None and addr is None


def test_close_udp_socket_open_and_closed(monkeypatch):
    dummy = DummySocket()

    # Open socket: fileno != -1 -> will call close
    dummy._closed = False
    network.close_udp_socket(dummy)
    assert dummy._closed is True

    # Now calling close on an already-closed socket should hit the debug branch
    # and not raise
    network.close_udp_socket(dummy)  # should be no-op and not raise
