{% skip_file if flag?(:wasm32) %}

require "./spec_helper"
require "../../support/win32"

describe TCPSocket, tags: "network" do
  describe "#connect" do
    each_ip_family do |family, address|
      it "connects to server" do
        port = unused_local_tcp_port

        TCPServer.open(address, port) do |server|
          TCPSocket.open(address, port) do |client|
            client.local_address.address.should eq address

            sock = server.accept

            sock.closed?.should be_false
            client.closed?.should be_false

            sock.local_address.port.should eq(port)
            sock.local_address.address.should eq(address)

            client.remote_address.port.should eq(port)
            sock.remote_address.address.should eq address
          end
        end
      end

      {% if flag?(:dragonfly) %}
        # FIXME: this spec regularly hangs in a vagrant/libvirt VM
        pending "raises when connection is refused"
      {% else %}
        it "raises when connection is refused" do
          port = unused_local_tcp_port

          expect_raises(Socket::ConnectError, "Error connecting to '#{address}:#{port}'") do
            TCPSocket.new(address, port)
          end
        end
      {% end %}

      it "raises when port is negative" do
        error = expect_raises(Socket::Addrinfo::Error) do
          TCPSocket.new(address, -12)
        end
        error.os_error.should eq({% if flag?(:win32) %}
          WinError::WSATYPE_NOT_FOUND
        {% elsif (flag?(:linux) && !flag?(:android)) || flag?(:openbsd) %}
          Errno.new(LibC::EAI_SERVICE)
        {% else %}
          Errno.new(LibC::EAI_NONAME)
        {% end %})
      end

      {% if flag?(:dragonfly) %}
        # FIXME: this spec regularly hangs in a vagrant/libvirt VM
        pending "raises when port is zero"
      {% else %}
        it "raises when port is zero" do
          expect_raises(Socket::ConnectError) do
            TCPSocket.new(address, 0)
          end
        end
      {% end %}
    end

    describe "address resolution" do
      it "connects to localhost" do
        port = unused_local_tcp_port

        TCPServer.open("localhost", port) do |server|
          TCPSocket.open("localhost", port) do |client|
            server.accept
          end
        end
      end

      it "raises when host doesn't exist" do
        err = expect_raises(Socket::Error, "Hostname lookup for doesnotexist.example.org. failed") do
          TCPSocket.new("doesnotexist.example.org.", 12345)
        end
        # FIXME: Resolve special handling for win32. The error code handling should be identical.
        {% if flag?(:win32) %}
          [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
        {% elsif flag?(:android) || flag?(:netbsd) || flag?(:openbsd) %}
          err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
        {% else %}
          [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_NODATA), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
        {% end %}
      end

      it "raises (rather than segfault on darwin) when host doesn't exist and port is 0" do
        err = expect_raises(Socket::Error, "Hostname lookup for doesnotexist.example.org. failed") do
          TCPSocket.new("doesnotexist.example.org.", 0)
        end
        # FIXME: Resolve special handling for win32. The error code handling should be identical.
        {% if flag?(:win32) %}
          [WinError::WSAHOST_NOT_FOUND, WinError::WSATRY_AGAIN].should contain err.os_error
        {% elsif flag?(:android) || flag?(:netbsd) || flag?(:openbsd) %}
          err.os_error.should eq(Errno.new(LibC::EAI_NODATA))
        {% else %}
          [Errno.new(LibC::EAI_NONAME), Errno.new(LibC::EAI_NODATA), Errno.new(LibC::EAI_AGAIN)].should contain err.os_error
        {% end %}
      end
    end

    it "fails to connect IPv6 to IPv4 server" do
      pending! "IPv6 is unavailable" unless SocketSpecHelper.supports_ipv6?

      port = unused_local_tcp_port

      TCPServer.open("0.0.0.0", port) do |server|
        expect_raises(Socket::ConnectError, "Error connecting to '::1:#{port}'") do
          TCPSocket.new("::1", port)
        end
      end
    end
  end

  {% if flag?(:dragonfly) %}
    # FIXME: these specs regularly hang in a vagrant/libvirt VM
    pending "sync from server"
    pending "settings"
    pending "fails when connection is refused"
    pending "sends and receives messages"
    pending "sends and receives messages (fibers & channels)"
  {% else %}
    it "sync from server" do
      port = unused_local_tcp_port

      TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server|
        TCPSocket.open("localhost", port) do |client|
          sock = server.accept
          sock.sync?.should eq(server.sync?)
        end

        # test sync flag propagation after accept
        server.sync = !server.sync?

        TCPSocket.open("localhost", port) do |client|
          sock = server.accept
          sock.sync?.should eq(server.sync?)
        end
      end
    end

    it "settings" do
      port = unused_local_tcp_port

      TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server|
        TCPSocket.open("localhost", port) do |client|
          # test protocol specific socket options
          (client.tcp_nodelay = true).should be_true
          client.tcp_nodelay?.should be_true
          (client.tcp_nodelay = false).should be_false
          client.tcp_nodelay?.should be_false

          {% unless flag?(:openbsd) || flag?(:netbsd) %}
            (client.tcp_keepalive_idle = 42).should eq 42
            client.tcp_keepalive_idle.should eq 42
            (client.tcp_keepalive_interval = 42).should eq 42
            client.tcp_keepalive_interval.should eq 42
            (client.tcp_keepalive_count = 42).should eq 42
            client.tcp_keepalive_count.should eq 42
          {% end %}
        end
      end
    end

    it "fails when connection is refused" do
      port = TCPServer.open("localhost", 0) do |server|
        server.local_address.port
      end

      expect_raises(Socket::ConnectError, "Error connecting to 'localhost:#{port}'") do
        TCPSocket.new("localhost", port)
      end
    end

    it "sends and receives messages" do
      port = unused_local_tcp_port

      TCPServer.open("::", port) do |server|
        TCPSocket.open("localhost", port) do |client|
          sock = server.accept

          client << "ping"
          sock.gets(4).should eq("ping")
          sock << "pong"
          client.gets(4).should eq("pong")
        end
      end
    end

    it "sends and receives messages (fibers & channels)" do
      port = unused_local_tcp_port

      channel = Channel(Exception?).new
      spawn do
        TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server|
          channel.send nil
          sock = server.accept
          sock.read_timeout = 3.second
          sock.write_timeout = 3.second

          sock.gets(4).should eq("ping")
          sock << "pong"
          channel.send nil
        end
      rescue exc
        channel.send exc
      end

      if exc = channel.receive
        raise exc
      end

      TCPSocket.open("localhost", port) do |client|
        client.read_timeout = 3.second
        client.write_timeout = 3.second
        client << "ping"
        client.gets(4).should eq("pong")
      end

      if exc = channel.receive
        raise exc
      end
    end
  {% end %}
end
