# frozen_string_literal: true

require "spec_helper"
require "timeout"

RSpec.describe NIO::Selector do
  let(:pair)   { IO.pipe }
  let(:reader) { pair.first }
  let(:writer) { pair.last }

  context ".backends" do
    it "knows all supported backends" do
      expect(described_class.backends).to be_a Array
      expect(described_class.backends.first).to be_a Symbol
    end
  end

  context "#initialize" do
    it "allows explicitly specifying a backend" do |example|
      backend = described_class.backends.first
      selector = described_class.new(backend)
      expect(selector.backend).to eq backend

      example.reporter.message "Supported backends: #{described_class.backends}"
    end

    it "raises ArgumentError if given an invalid backend" do
      expect { described_class.new(:derp) }.to raise_error ArgumentError
    end

    it "raises TypeError if given a non-Symbol parameter" do
      expect { described_class.new(42).to raise_error TypeError }
    end
  end

  context "backend" do
    it "knows its backend" do |example|
      expect(subject.backend).to be_a Symbol

      example.reporter.message "Current backend: #{subject.backend}"
    end
  end

  context "register" do
    it "registers IO objects" do
      monitor = subject.register(reader, :r)
      expect(monitor).not_to be_closed
    end

    it "raises TypeError if asked to register non-IO objects" do
      expect { subject.register(42, :r) }.to raise_exception TypeError
    end

    it "raises when asked to register after closing" do
      subject.close
      expect { subject.register(reader, :r) }.to raise_exception IOError
    end
  end

  it "knows which IO objects are registered" do
    subject.register(reader, :r)
    expect(subject).to be_registered(reader)
    expect(subject).not_to be_registered(writer)
  end

  it "deregisters IO objects" do
    subject.register(reader, :r)

    monitor = subject.deregister(reader)
    expect(subject).not_to be_registered(reader)
    expect(monitor).to be_closed
  end

  it "allows deregistering closed IO objects" do
    subject.register(reader, :r)
    reader.close

    expect do
      subject.deregister(reader)
    end.not_to raise_error
  end

  it "reports if it is empty" do
    expect(subject).to be_empty
    subject.register(reader, :r)
    expect(subject).not_to be_empty
  end

  # This spec might seem a bit silly, but this actually something the
  # Java NIO API specifically precludes that we need to work around
  it "allows reregistration of the same IO object across select calls" do
    monitor = subject.register(reader, :r)
    writer << "ohai"

    expect(subject.select).to include monitor
    expect(reader.read(4)).to eq("ohai")
    subject.deregister(reader)

    new_monitor = subject.register(reader, :r)
    writer << "thar"
    expect(subject.select).to include new_monitor
    expect(reader.read(4)).to eq("thar")
  end

  context "timeouts" do
    let(:select_precision) {0.2}
    let(:timeout) {2.0}
    let(:payload) {"hi there"}

    it "waits for timeout when selecting from empty selector" do
      started_at = Time.now
      expect(subject.select(timeout)).to be_nil
      expect(Time.now - started_at).to be_within(select_precision).of(timeout)
    end

    it "waits for a timeout when selecting with reader" do
      monitor = subject.register(reader, :r)

      writer << payload

      started_at = Time.now
      expect(subject.select(timeout)).to include monitor
      expect(Time.now - started_at).to be_within(select_precision).of(0)
      reader.read_nonblock(payload.size)

      started_at = Time.now
      expect(subject.select(timeout)).to be_nil
      expect(Time.now - started_at).to be_within(select_precision).of(timeout)
    end

    it "raises ArgumentError if given a negative timeout" do
      subject.register(reader, :r)

      expect do
        subject.select(-1)
      end.to raise_exception(ArgumentError)
    end
  end

  context "wakeup" do
    let(:select_precision) {0.2}

    it "wakes up if signaled to from another thread" do
      subject.register(reader, :r)

      thread = Thread.new do
        started_at = Time.now
        expect(subject.select).to eq []
        Time.now - started_at
      end

      timeout = 0.1
      sleep timeout
      subject.wakeup

      expect(thread.value).to be_within(select_precision).of(timeout)
    end

    it "raises IOError if asked to wake up a closed selector" do
      subject.close
      expect(subject).to be_closed

      expect { subject.wakeup }.to raise_exception IOError
    end
  end

  context "select" do
    it "does not block on super small precision intervals" do
      wait_interval = 1e-4

      expect do
        Timeout.timeout(2) do
          subject.select(wait_interval)
        end
      end.not_to raise_error
    end

    it "selects IO objects" do
      writer << "ohai"
      unready = IO.pipe.first

      reader_monitor  = subject.register(reader, :r)
      unready_monitor = subject.register(unready, :r)

      selected = subject.select(0)
      expect(selected.size).to eq(1)
      expect(selected).to include reader_monitor
      expect(selected).not_to include unready_monitor
    end

    it "selects closed IO objects" do
      monitor = subject.register(reader, :r)
      expect(subject.select(0)).to be_nil

      thread = Thread.new { subject.select }
      Thread.pass while thread.status && thread.status != "sleep"

      writer.close
      selected = thread.value
      expect(selected).to include monitor
    end

    it "iterates across selected objects with a block" do
      readable1, writer = IO.pipe
      writer << "ohai"

      readable2, writer = IO.pipe
      writer << "ohai"

      unreadable = IO.pipe.first

      monitor1 = subject.register(readable1, :r)
      monitor2 = subject.register(readable2, :r)
      monitor3 = subject.register(unreadable, :r)

      readables = []
      result = subject.select { |monitor| readables << monitor }
      expect(result).to eq(2)

      expect(readables).to include monitor1
      expect(readables).to include monitor2
      expect(readables).not_to include monitor3
    end

    it "raises IOError if asked to select on a closed selector" do
      subject.close

      expect { subject.select(0) }.to raise_exception IOError
    end
  end

  it "closes" do
    subject.close
    expect(subject).to be_closed
  end
end
# rubocop:enable Metrics/BlockLength
