class Opal::CliRunners::Safari

def self.call(data)

def self.call(data)
  runner = new(data)
  runner.run
end

def initialize(data)

def initialize(data)
  argv = data[:argv]
  if argv && argv.any?
    warn "warning: ARGV is not supported by the Safari runner #{argv.inspect}"
  end
  options  = data[:options]
  @output  = options.fetch(:output, $stdout)
  @builder = data[:builder].call
end

def mktmpdir(&block)

def mktmpdir(&block)
  Dir.mktmpdir('safari-opal-', &block)
end

def prepare_files_in(dir)

def prepare_files_in(dir)
  # The safaridriver is very limited in capabilities, basically it can trigger visiting sites
  # and interact a bit with the page. So this runner starts its own server, overwrites the
  # console log, warn, error functions of the browser and triggers a request after execution
  # to exit. Certain exceptions cannot be caught that way and everything may fail in between,
  # thats why execution is timed out after EXECUTION_TIMEOUT (10 minutes).
  # As a side effect, console messages may arrive out of order and timing anything may be inaccurate.
  builder.build_str <<~RUBY, '(exit)', no_export: true
  %x{
    var req = new XMLHttpRequest();
    req.open("GET", '/exit');
    req.send();
  }
  RUBY
  js = builder.to_s
  map = builder.source_map.to_json
  ext = builder.output_extension
  module_type = ' type="module"' if builder.esm?
  File.binwrite("#{dir}/index.#{ext}", js)
  File.binwrite("#{dir}/index.map", map)
  File.binwrite("#{dir}/index.html", <<~HTML)
    <html><head>
      <meta charset='utf-8'>
      <link rel="icon" href="data:;base64,=">
    </head><body>
      <script>
        var orig_log = console.log;
        var orig_err = console.error;
        var orig_warn = console.warn;
        function send_log_request(args) {
          var req = new XMLHttpRequest();
          req.open("POST", '/log');
          req.setRequestHeader("Content-Type", "application/json");
          req.send(JSON.stringify(args));
        }
        console.log = function() {
          orig_log.apply(null, arguments);
          send_log_request(arguments);
        }
        console.error = function() {
          orig_err.apply(null, arguments);
          send_log_request(arguments);
        }
        console.warn = function() {
          orig_warn.apply(null, arguments);
          send_log_request(arguments);
        }
      </script>
      <script src='./index.#{ext}'#{module_type}></script>
    </body></html>
  HTML
  # <script src='./index.#{ext}'#{module_type}></script>
end

def run

def run
  mktmpdir do |dir|
    with_http_server(dir) do |http_port, server_thread|
      with_safari_driver do
        prepare_files_in(dir)
        # Safaridriver commands are very limitied, for supported commands see:
        # https://developer.apple.com/documentation/webkit/macos_webdriver_commands_for_safari_12_and_later
        Net::HTTP.start(safari_driver_host, safari_driver_port) do |con|
          con.read_timeout = EXECUTION_TIMEOUT
          res = con.post('/session', { capabilities: { browserName: 'Safari' } }.to_json, 'Content-Type' => 'application/json')
          session_id = JSON.parse(res.body).dig('value', 'sessionId')
          if session_id
            session_path = "/session/#{session_id}"
            con.post("#{session_path}/url", { url: "http://#{safari_driver_host}:#{http_port}/index.html" }.to_json, 'Content-Type' => 'application/json')
            server_thread.join(EXECUTION_TIMEOUT)
          else
            STDERR.puts "Could not create session: #{res.body}"
          end
        end
        0
      end
    end
  end
end

def run_safari_driver

def run_safari_driver
  raise 'Safari driver can be started only on localhost' if safari_driver_host != DEFAULT_SAFARI_DRIVER_HOST
  started = false
  safari_driver_cmd = %{/usr/bin/safaridriver \
    -p #{safari_driver_port} \
    #{ENV['SAFARI_DRIVER_OPTS']}}
  safari_driver_pid = Process.spawn(safari_driver_cmd, in: OS.dev_null, out: OS.dev_null, err: OS.dev_null)
  Timeout.timeout(30) do
    loop do
      break if safari_driver_running?
      sleep 0.5
    end
  end
  started = true
  yield
rescue Timeout::Error => e
  puts started ? 'Execution timed out' : 'Failed to start Safari driver'
  raise e
ensure
  Process.kill('HUP', safari_driver_pid) if safari_driver_pid
end

def safari_driver_host

def safari_driver_host
  ENV['SAFARI_DRIVER_HOST'] || DEFAULT_SAFARI_DRIVER_HOST
end

def safari_driver_port

def safari_driver_port
  ENV['SAFARI_DRIVER_PORT'] || DEFAULT_SAFARI_DRIVER_PORT
end

def safari_driver_running?

def safari_driver_running?
  puts "Connecting to #{safari_driver_host}:#{safari_driver_port}..."
  TCPSocket.new(safari_driver_host, safari_driver_port).close
  true
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL
  false
end

def with_http_server(dir)

def with_http_server(dir)
  port = safari_driver_port.to_i + 1
  server_thread = Thread.new do
    server = WEBrick::HTTPServer.new(Port: port, DocumentRoot: dir, Logger: WEBrick::Log.new('/dev/null'), AccessLog: [])
    server.mount_proc('/log') do |req, res|
      if req.body
        par = JSON.parse(req.body)
        par.each_value do |value|
          print value.to_s
        end
      end
      res.header['Content-Type'] = 'text/plain'
      res.body = ''
    end
    server.mount_proc('/exit') do
      server_thread.kill
    end
    server.start
  end
  yield port, server_thread
rescue
  exit(1)
ensure
  server_thread.kill if server_thread
end

def with_safari_driver

def with_safari_driver
  if safari_driver_running?
    yield
  else
    run_safari_driver { yield }
  end
end