require "./spec_helper"
require "socket"

describe UDPSocket, tags: "network" do
  # Note: This spec fails with a IPv6 address. See pending below.
  it "#remote_address resets after connect" do
    socket = UDPSocket.new
    socket.connect("127.0.0.1", 1)
    socket.remote_address.port.should eq 1
    socket.connect("127.0.0.1", 2)
    socket.remote_address.port.should eq 2
    socket.close
  end

  pending "#connect with a IPv6 address" do
    socket = UDPSocket.new
    socket.connect("::1", 1)
    socket.close
  end

  it "supports IPv6 dual stack" do
    socket = UDPSocket.new(:inet6)

    socket.ipv6_only = false
    socket.ipv6_only?.should be_false

    socket.ipv6_only = true
    socket.ipv6_only?.should be_true

    socket = UDPSocket.new
    expect_raises(Socket::Error, "Unsupported IP address family: INET. For use with IPv6 only") do
      socket.ipv6_only?
    end
    expect_raises(Socket::Error, "Unsupported IP address family: INET. For use with IPv6 only") do
      socket.ipv6_only = false
    end
  end

  each_ip_family do |family, address, unspecified_address|
    it "#bind" do
      port = unused_local_udp_port
      socket = UDPSocket.new(family)
      socket.bind(address, port)
      socket.local_address.should eq(Socket::IPAddress.new(address, port))
      socket.close
      socket = UDPSocket.new(family)
      socket.bind(address, 0)
      socket.local_address.address.should eq address
    ensure
      socket.try &.close
    end

    it "sends and receives messages" do
      port = unused_local_udp_port

      server = UDPSocket.new(family)
      server.bind(address, port)
      server.local_address.should eq(Socket::IPAddress.new(address, port))

      client = UDPSocket.new(family)
      client.bind(address, 0)

      client.send "message", to: server.local_address
      server.receive.should eq({"message", client.local_address})

      client.connect(address, port)
      client.local_address.family.should eq(family)
      client.local_address.address.should eq(address)
      client.remote_address.should eq(Socket::IPAddress.new(address, port))

      client.send "message"
      server.receive.should eq({"message", client.local_address})

      client.send("laus deo semper")

      buffer = uninitialized UInt8[256]

      bytes_read, client_addr = server.receive(buffer.to_slice)
      message = String.new(buffer.to_slice[0, bytes_read])
      message.should eq("laus deo semper")

      client.send("laus deo semper")

      # WSA errors with WSAEMSGSIZE if the buffer is not large enough to receive the message
      {% unless flag?(:win32) %}
        bytes_read, client_addr = server.receive(buffer.to_slice[0, 4])
        message = String.new(buffer.to_slice[0, bytes_read])
        message.should eq("laus")
      {% end %}

      client.close
      server.close
    end

    if {{ flag?(:darwin) }}
      # Darwin is failing to join IPv6 multicast groups on older versions.
      # However this is known to work on macOS Mojave with Darwin 18.2.0.
      # Darwin also has a bug that prevents selecting the "default" interface.
      # https://lists.apple.com/archives/darwin-kernel/2014/Mar/msg00012.html
      # Since macOS Sequoia (version 15), the ability to send UDP multicasts
      # seems to have some extra requirements.
      pending "joins and transmits to multicast groups"
    elsif {{ flag?(:dragonfly) }} && family == Socket::Family::INET6
      # TODO: figure out why updating `multicast_loopback` produces a
      # `setsockopt 9: Can't assign requested address
      pending "joins and transmits to multicast groups"
    elsif {{ flag?(:solaris) }} && family == Socket::Family::INET
      # TODO: figure out why updating `multicast_loopback` produces a
      # `setsockopt 18: Invalid argument` error
      pending "joins and transmits to multicast groups"
    elsif {{ flag?(:freebsd) }} && family == Socket::Family::INET6
      # FIXME: fails with "Error sending datagram to [ipv6]:port: Network is unreachable"
      pending "joins and transmits to multicast groups"
    elsif {{ flag?(:netbsd) }} && family == Socket::Family::INET6
      # FIXME: fails with "setsockopt: EADDRNOTAVAIL"
      pending "joins and transmits to multicast groups"
    elsif {{ flag?(:openbsd) }}
      # FIXME: fails with "setsockopt: EINVAL (ipv4) or EADDRNOTAVAIL (ipv6)"
      pending "joins and transmits to multicast groups"
    else
      it "joins and transmits to multicast groups" do
        udp = UDPSocket.new(family)
        port = unused_local_udp_port
        udp.bind(unspecified_address, port)

        udp.multicast_loopback = false
        udp.multicast_loopback?.should be_false

        udp.multicast_hops = 4
        udp.multicast_hops.should eq(4)
        udp.multicast_hops = 0
        udp.multicast_hops.should eq(0)

        addr = case family
               when Socket::Family::INET
                 expect_raises(Socket::Error, "Unsupported IP address family: INET. For use with IPv6 only") do
                   udp.multicast_interface 0
                 end

                 begin
                   udp.multicast_interface Socket::IPAddress.new(unspecified_address, 0)
                 rescue e : Socket::Error
                   if e.os_error == Errno::ENOPROTOOPT
                     pending!("Multicast device selection not available on this host")
                   else
                     raise e
                   end
                 end

                 Socket::IPAddress.new("224.0.0.254", port)
               when Socket::Family::INET6
                 expect_raises(Socket::Error, "Unsupported IP address family: INET6. For use with IPv4 only") do
                   udp.multicast_interface(Socket::IPAddress.new(unspecified_address, 0))
                 end

                 begin
                   udp.multicast_interface(0)
                 rescue e : Socket::Error
                   if e.os_error == Errno::ENOPROTOOPT
                     pending!("Multicast device selection not available on this host")
                   else
                     raise e
                   end
                 end

                 Socket::IPAddress.new("ff02::102", port)
               else
                 raise "Unsupported IP address family: #{family}"
               end

        begin
          udp.join_group(addr)
        rescue e : Socket::Error
          if e.os_error == Errno::ENODEV
            pending!("Multicast device selection not available on this host")
          else
            raise e
          end
        end

        udp.multicast_loopback = true
        udp.multicast_loopback?.should be_true

        udp.send("testing", addr)
        udp.read_timeout = 1.second
        begin
          udp.receive[0].should eq("testing")
        rescue IO::TimeoutError
          # Since this test doesn't run over the loopback interface, this test
          # fails when there is a firewall in use. Don't fail in that case.
        end

        udp.leave_group(addr)
        udp.send("testing", addr)

        # Test that nothing was received after leaving the multicast group
        spawn do
          sleep 100.milliseconds
          udp.close
        end
        expect_raises(IO::Error) { udp.receive }
        udp.closed?.should be_true
      end
    end
  end

  {% if flag?(:darwin) %}
    # Fails since macOS Sequoia (version 15) with EHOSTUNREACH. The ability to
    # send UDP broadcasts might need extra requirements.
    pending "sends broadcast message"
  {% else %}
    it "sends broadcast message" do
      server = UDPSocket.new(Socket::Family::INET)
      server.bind("0.0.0.0", 0)
      addr = Socket::IPAddress.new("255.255.255.255", server.local_address.port)

      client = UDPSocket.new(Socket::Family::INET)
      client.broadcast = true
      client.broadcast?.should be_true

      client.send("broadcast", to: addr).should eq(9)
      client.close

      server.read_timeout = 1.second
      begin
        message, _ = server.receive
        message.should eq("broadcast")
      rescue IO::TimeoutError
        # Since this test doesn't run over the loopback interface, this test
        # fails when there is a firewall in use. Don't fail in that case.
      end
    end
  {% end %}
end
