# frozen_string_literal: true

# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

module Selenium
  module WebDriver
    #
    # Base class implementing default behavior of service_manager object,
    # responsible for starting and stopping driver implementations.
    #
    # @api private
    #
    class ServiceManager
      START_TIMEOUT = 20
      SOCKET_LOCK_TIMEOUT = 45
      STOP_TIMEOUT = 20

      #
      # End users should use a class method for the desired driver, rather than using this directly.
      #
      # @api private
      #

      def initialize(config)
        @executable_path = config.executable_path
        @host = Platform.localhost
        @port = config.port
        @io = config.log
        @extra_args = config.args
        @shutdown_supported = config.shutdown_supported

        raise Error::WebDriverError, "invalid port: #{@port}" if @port < 1
      end

      def start
        raise "already started: #{uri.inspect} #{@executable_path.inspect}" if process_running?

        Platform.exit_hook { stop } # make sure we don't leave the server running

        socket_lock.locked do
          find_free_port
          start_process
          connect_until_stable
        end
      end

      def stop
        return unless @shutdown_supported
        return if process_exited?

        stop_server
        @process.poll_for_exit STOP_TIMEOUT
      rescue ChildProcess::TimeoutError, Errno::ECONNREFUSED
        nil # noop
      ensure
        stop_process
      end

      def uri
        @uri ||= URI.parse("http://#{@host}:#{@port}")
      end

      private

      def build_process(*command)
        WebDriver.logger.debug("Executing Process #{command}", id: :driver_service)
        @process = ChildProcess.build(*command)
        if ENV.key?('SE_DEBUG')
          if @io && @io != WebDriver.logger.io
            WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver log output to use stderr',
                                  id: :se_debug)
          end
          @io = WebDriver.logger.io
        end
        @process.io = @io if @io

        @process
      end

      def connect_to_server
        Net::HTTP.start(@host, @port) do |http|
          http.open_timeout = STOP_TIMEOUT / 2
          http.read_timeout = STOP_TIMEOUT / 2

          yield http
        end
      end

      def find_free_port
        @port = PortProber.above(@port)
      end

      def start_process
        @process = build_process(@executable_path, "--port=#{@port}", *@extra_args)
        @process.start
      end

      def stop_process
        return if process_exited?

        @process.stop STOP_TIMEOUT
      end

      def stop_server
        connect_to_server do |http|
          headers = WebDriver::Remote::Http::Common::DEFAULT_HEADERS.dup
          WebDriver.logger.debug('Sending shutdown request to server', id: :driver_service)
          http.get('/shutdown', headers)
        end
      end

      def process_running?
        defined?(@process) && @process&.alive?
      end

      def process_exited?
        @process.nil? || @process.exited?
      end

      def connect_until_stable
        deadline = current_time + START_TIMEOUT

        loop do
          error = check_connection_error
          return unless error

          raise Error::WebDriverError, "#{cannot_connect_error_text}: #{error}" if current_time > deadline

          sleep 0.1
        end
      end

      def check_connection_error
        response = Net::HTTP.start(@host, @port, open_timeout: 0.5, read_timeout: 1) do |http|
          http.get('/status', {'Connection' => 'close'})
        end

        return "status returned #{response.code}\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)

        status = JSON.parse(response.body)
        ready = status['ready'] || status.dig('value', 'ready')
        "driver not ready: #{response.body}" unless ready
      rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
             Errno::EADDRNOTAVAIL, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
             EOFError, SocketError, Net::HTTPBadResponse, JSON::ParserError => e
        "#{e.class}: #{e.message}"
      end

      def current_time
        Process.clock_gettime(Process::CLOCK_MONOTONIC)
      end

      def cannot_connect_error_text
        "unable to connect to #{@executable_path} #{@host}:#{@port}"
      end

      def socket_lock
        @socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
      end
    end # Service
  end # WebDriver
end # Selenium
