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

require "./spec_helper"

describe TCPServer, tags: "network" do
  describe ".new" do
    each_ip_family do |family, address|
      it "listens on local address" do
        port = unused_local_tcp_port

        server = TCPServer.new(address, port)

        server.reuse_port?.should be_false
        server.reuse_address?.should be_true

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

        server.closed?.should be_false

        server.close

        server.closed?.should be_true
        expect_raises(Socket::Error, "getsockname: ") do
          server.local_address
        end
      end

      it "binds to port 0" do
        server = TCPServer.new(address, 0)

        begin
          server.local_address.address.should eq(address)
          server.local_address.port.should be > 0
        ensure
          server.close
        end
      end

      it "raises when port is negative" do
        error = expect_raises(Socket::Addrinfo::Error) do
          TCPServer.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

      describe "reuse_port" do
        it "raises when port is in use" do
          TCPServer.open(address, 0) do |server|
            expect_raises(Socket::BindError, "Could not bind to '#{address}:#{server.local_address.port}': ") do
              TCPServer.open(address, server.local_address.port) { }
            end
          end
        end

        it "raises when not binding with reuse_port" do
          TCPServer.open(address, 0, reuse_port: true) do |server|
            expect_raises(Socket::BindError) do
              TCPServer.open(address, server.local_address.port) { }
            end
          end
        end

        it "raises when port is not ready to be reused" do
          TCPServer.open(address, 0) do |server|
            expect_raises(Socket::BindError) do
              TCPServer.open(address, server.local_address.port, reuse_port: true) { }
            end
          end
        end

        it "binds to used port with reuse_port = true" do
          TCPServer.open(address, 0, reuse_port: true) do |server|
            TCPServer.open(address, server.local_address.port, reuse_port: true) { }
          end
        end
      end
    end

    describe "address resolution" do
      it "binds to localhost" do
        server = TCPServer.new("localhost", unused_local_tcp_port)
        server.close
      end

      it "raises when host doesn't exist" do
        err = expect_raises(Socket::Error, "Hostname lookup for doesnotexist.example.org. failed") do
          TCPServer.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
          TCPServer.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 "binds to all interfaces" do
      port = unused_local_tcp_port
      TCPServer.open(Socket::IPAddress::UNSPECIFIED, port) do |server|
        server.local_address.port.should eq port
      end
    end
  end

  {% if flag?(:linux) || flag?(:solaris) %}
    pending "settings"
  {% else %}
    it "settings" do
      TCPServer.open("::", unused_local_tcp_port) do |server|
        (server.recv_buffer_size = 42).should eq 42
        server.recv_buffer_size.should eq 42
      end
    end
  {% end %}

  describe "accept" do
    it "sets close on exec flag" do
      TCPServer.open("localhost", 0) do |server|
        TCPSocket.open("localhost", server.local_address.port) do |client|
          server.accept? do |sock|
            sock.close_on_exec?.should eq CLOSE_ON_EXEC_AVAILABLE
          end
        end
      end
    end

    it "supports IPv6 dual stack" do
      server = TCPServer.new(:inet6)
      server.ipv6_only = false
      server.bind("::", 0)
      server.listen

      TCPSocket.open("127.0.0.1", server.local_address.port) do |client|
        server.accept? do |sock|
          sock.ipv6_only?.should be_false

          # should raise when changing ipv6_only when not applicable
          expect_raises(Socket::Error, /invalid argument/i) do
            sock.ipv6_only = true
          end
        end
      end

      server.close
    end

    it "supports IPv6 only" do
      server = TCPServer.new(:inet6)
      server.ipv6_only = true
      server.bind("::", 0)
      server.listen

      expect_raises(Socket::ConnectError) do
        TCPSocket.new("127.0.0.1", server.local_address.port, connect_timeout: 1.second)
      end
      server.close
    end
  end
end
