require "helper"
require "timeout"

class BreakError < StandardError; end

describe SurfaceMaster::Launchpad::Interaction do
  # returns true/false whether the operation ended or the timeout was hit
  def timeout(timeout = 0.02, &block)
    Timeout.timeout(timeout, &block)
    true
  rescue Timeout::Error
    false
  end

  def press(interaction, type, opts = nil)
    interaction.respond_to(type, :down, opts)
    interaction.respond_to(type, :up, opts)
  end

  def press_all(interaction)
    %w(up down left right session user1 user2 mixer).each do |type|
      press(interaction, type.to_sym)
    end
    8.times do |y|
      8.times do |x|
        press(interaction, :grid, x: x, y: y)
      end
      press(interaction, :"scene#{y + 1}")
    end
  end

  describe '#initialize' do
    it "creates device if not given" do
      device = SurfaceMaster::Launchpad::Device.new
      SurfaceMaster::Launchpad::Device.expects(:new)
        .with(input: true, output: true, logger: nil)
        .returns(device)
      interaction = SurfaceMaster::Launchpad::Interaction.new
      assert_same device, interaction.device
    end

    it "creates device with given device_name" do
      device = SurfaceMaster::Launchpad::Device.new
      SurfaceMaster::Launchpad::Device.expects(:new)
        .with(device_name: "device", input: true, output: true, logger: nil)
        .returns(device)
      interaction = SurfaceMaster::Launchpad::Interaction.new(device_name: "device")
      assert_same device, interaction.device
    end

    it "creates device with given input_device_id" do
      device = SurfaceMaster::Launchpad::Device.new
      SurfaceMaster::Launchpad::Device.expects(:new)
        .with(input_device_id: "in", input: true, output: true, logger: nil)
        .returns(device)
      interaction = SurfaceMaster::Launchpad::Interaction.new(input_device_id: "in")
      assert_same device, interaction.device
    end

    it "creates device with given output_device_id" do
      device = SurfaceMaster::Launchpad::Device.new
      SurfaceMaster::Launchpad::Device.expects(:new)
        .with(output_device_id: "out", input: true, output: true, logger: nil)
        .returns(device)
      interaction = SurfaceMaster::Launchpad::Interaction.new(output_device_id: "out")
      assert_same device, interaction.device
    end

    it "creates device with given input_device_id/output_device_id" do
      device = SurfaceMaster::Launchpad::Device.new
      SurfaceMaster::Launchpad::Device
        .expects(:new)
        .with(input_device_id:  "in",
              output_device_id: "out",
              input:            true,
              output:           true,
              logger:           nil)
        .returns(device)
      interaction = SurfaceMaster::Launchpad::Interaction.new(input_device_id:  "in",
                                                              output_device_id: "out")
      assert_same device, interaction.device
    end

    it "initializes device if given" do
      device = SurfaceMaster::Launchpad::Device.new
      interaction = SurfaceMaster::Launchpad::Interaction.new(device: device)
      assert_same device, interaction.device
    end

    it "stores the logger given" do
      logger = Logger.new(nil)
      interaction = SurfaceMaster::Launchpad::Interaction.new(logger: logger)
      assert_same logger, interaction.logger
      assert_same logger, interaction.device.logger
    end

    it 'doesn\'t activate the interaction' do
      assert !SurfaceMaster::Launchpad::Interaction.new.active
    end
  end

  describe '#logger=' do
    it "stores the logger and passes it to the device as well" do
      logger              = Logger.new(nil)
      interaction         = SurfaceMaster::Launchpad::Interaction.new(logger: logger)
      assert_same logger, interaction.logger
      assert_same logger, interaction.device.logger
    end
  end

  describe '#close' do
    it "stops the interaction" do
      interaction = SurfaceMaster::Launchpad::Interaction.new
      interaction.expects(:stop)
      interaction.close
    end

    it "closes the device" do
      interaction = SurfaceMaster::Launchpad::Interaction.new
      interaction.device.expects(:close)
      interaction.close
    end
  end

  describe '#closed?' do
    it "returns false on a newly created interaction, but true after closing" do
      interaction = SurfaceMaster::Launchpad::Interaction.new
      assert !interaction.closed?
      interaction.close
      assert interaction.closed?
    end
  end

  describe '#start' do
    before do
      @interaction = SurfaceMaster::Launchpad::Interaction.new
    end

    after do
      mocha_teardown # so that expectations on Thread.join don't fail in here
      begin
        @interaction.close
      # rubocop:disable Lint/HandleExceptions
      rescue
      # rubocop:enable Lint/HandleExceptions
        # ignore, should be handled in tests, this is just to close all the spawned threads
      end
    end

    it "sets active to true in blocking mode" do
      refute @interaction.active
      erg = timeout { @interaction.start }
      refute erg, "there was no timeout"
      assert @interaction.active
    end

    it "sets active to true in detached mode" do
      refute @interaction.active
      @interaction.start(detached: true)
      assert @interaction.active
    end

    it "blocks in blocking mode" do
      erg = timeout { @interaction.start }
      refute erg, "there was no timeout"
    end

    it "returns immediately in detached mode" do
      erg = timeout { @interaction.start(detached: true) }
      assert erg, "there was a timeout"
    end

    it "raises CommunicationError when Portmidi::DeviceError occurs" do
      @interaction.device.stubs(:read).raises(Portmidi::DeviceError.new(0))
      assert_raises SurfaceMaster::CommunicationError do
        @interaction.start
      end
    end

    describe "action handling" do
      before do
        @interaction.response_to(:mixer, :down) { @mixer_down = true }
        @interaction.response_to(:mixer, :up) do |i, _a|
          sleep 0.001 # sleep to make "sure" :mixer :down has been processed
          i.stop
        end
        @interaction.device.expects(:read)
          .at_least_once
          .returns([
            { timestamp: 0,
              state:     :down,
              type:      :mixer },
            { timestamp: 0,
              state:     :up,
              type:      :mixer },
          ])
      end

      it "calls respond_to_action with actions from respond_to_action in blocking mode" do
        erg = timeout(0.5) { @interaction.start }
        assert erg, 'the actions weren\'t called'
        assert @mixer_down, 'the mixer button wasn\'t pressed'
      end

      it "calls respond_to_action with actions from respond_to_action in detached mode" do
        @interaction.start(detached: true)
        erg = timeout(0.5) { sleep 0.01 while @interaction.active }
        assert erg, "there was a timeout"
        assert @mixer_down, 'the mixer button wasn\'t pressed'
      end
    end

    describe "latency" do
      # TODO: This seems like a REALLY janky way to handle these tests...

      # TODO: Also, at best the flow of which-code-creates-which-instance is
      # TODO: confusing here, and at worst it's doing something unexpected
      # TODO: that only works by accident.

      before do
        @device = @interaction.device
        @times = []
        @device.instance_variable_set("@test_interaction_latency_times", @times)
        def @device.read
          @test_interaction_latency_times << Time.now.to_f
          []
        end
      end

      it "sleeps with default latency of 0.001s when none given" do
        @interaction = SurfaceMaster::Launchpad::Interaction.new(device: @device)
        timeout { @interaction.start }
        assert @times.size > 1
        @times.each_cons(2) do |a, b|
          # TODO: This.. this is meaningless.  WTF.
          assert_in_delta 0.001, b - a, 0.01
        end
      end

      it "sleeps with given latency" do
        @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: 0.5, device: @device)
        timeout(0.55) { @interaction.start }
        assert @times.size > 1
        @times.each_cons(2) do |a, b|
          assert_in_delta 0.5, b - a, 0.01
        end
      end

      it "sleeps with absolute value of given negative latency" do
        @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: -0.1, device: @device)
        timeout(0.15) { @interaction.start }
        assert @times.size > 1
        @times.each_cons(2) do |a, b|
          assert_in_delta 0.1, b - a, 0.11
        end
      end

      it "does not sleep when latency is 0" do
        @interaction = SurfaceMaster::Launchpad::Interaction.new(latency: 0, device: @device)
        timeout(0.01) { @interaction.start }
        assert @times.size > 1
        @interaction.stop
        @times.each_cons(2) do |a, b|
          assert_in_delta 0, b - a, 0.01
        end
      end
    end

    # TODO: This semantic doesn't appear to be working, and begs the question of whether
    # TODO: that semantics is DESIRABLE.
    # it 'resets the device after the loop' do
    #   @interaction.device.expects(:reset)
    #   @interaction.start(detached: true)
    #   @interaction.stop
    # end

    it "raises NoInputAllowedError on closed interaction" do
      @interaction.close
      assert_raises SurfaceMaster::NoInputAllowedError do
        @interaction.start
      end
    end
  end

  describe '#stop' do
    before do
      @interaction = SurfaceMaster::Launchpad::Interaction.new
    end

    it "sets active to false in blocking mode" do
      erg = timeout { @interaction.start }
      refute erg, "there was no timeout"
      assert @interaction.active
      @interaction.stop
      assert !@interaction.active
    end

    it "sets active to false in detached mode" do
      @interaction.start(detached: true)
      assert @interaction.active
      @interaction.stop
      assert !@interaction.active
    end

    it "is callable anytime" do
      @interaction.stop
      @interaction.start(detached: true)
      @interaction.stop
      @interaction.stop
    end

    # this is kinda greybox tested, since I couldn't come up with another way to test thread
    # handling [thomas, 2010-01-24]
    it "raises pending exceptions in detached mode" do
      t = Thread.new { fail BreakError }
      Thread.expects(:new).returns(t)
      @interaction.start(detached: true)
      assert_raises BreakError do
        @interaction.stop
      end
    end
  end

  describe '#response_to/#no_response_to/#respond_to' do
    before do
      @interaction = SurfaceMaster::Launchpad::Interaction.new
    end

    it "calls all responses that match, and not others" do
      @interaction.response_to(:mixer, :down) { |_i, _a| @mixer_down = true }
      @interaction.response_to(:all, :down) { |_i, _a| @all_down = true }
      @interaction.response_to(:all, :up) { |_i, _a| @all_up = true }
      @interaction.response_to(:grid, :down) { |_i, _a| @grid_down = true }
      @interaction.respond_to(:mixer, :down)
      assert @mixer_down
      assert @all_down
      assert !@all_up
      assert !@grid_down
    end

    it "does not call responses when they are deregistered" do
      @interaction.response_to(:mixer, :down) { |_i, _a| @mixer_down = true }
      @interaction.response_to(:mixer, :up) { |_i, _a| @mixer_up = true }
      @interaction.response_to(:all, :both) { |_i, a| @all_down = a[:state] == :down }
      @interaction.no_response_to(:mixer, :down)
      @interaction.respond_to(:mixer, :down)
      assert !@mixer_down
      assert !@mixer_up
      assert @all_down
      @interaction.respond_to(:mixer, :up)
      assert !@mixer_down
      assert @mixer_up
      assert !@all_down
    end

    it "does not call responses registered for both when removing for one of both states" do
      @interaction.response_to(:mixer, :both) { |_i, _a| @mixer = true }
      @interaction.no_response_to(:mixer, :down)
      @interaction.respond_to(:mixer, :down)
      assert !@mixer
      @interaction.respond_to(:mixer, :up)
      assert @mixer
    end

    it "removes other responses when adding a new exclusive response" do
      @interaction.response_to(:mixer, :both) { |_i, _a| @mixer = true }
      @interaction.response_to(:mixer, :down, exclusive: true) { |_i, _a| @exclusive_mixer = true }
      @interaction.respond_to(:mixer, :down)
      assert !@mixer
      assert @exclusive_mixer
      @interaction.respond_to(:mixer, :up)
      assert @mixer
      assert @exclusive_mixer
    end

    it "allows for multiple types" do
      @downs = []
      @interaction.response_to([:up, :down], :down) { |_i, a| @downs << a[:type] }
      @interaction.respond_to(:up, :down)
      @interaction.respond_to(:down, :down)
      @interaction.respond_to(:up, :down)
      assert_equal [:up, :down, :up], @downs
    end

    describe "allows to bind to specific grid buttons" do
      before do
        @downs = []
        @action = ->(_i, a) { @downs << [a[:x], a[:y]] }
      end

      it "one specific grid button" do
        @interaction.response_to(:grid, :down, x: 4, y: 2, &@action)
        press_all @interaction
        assert_equal [[4, 2]], @downs
      end

      it "a complete row of grid buttons" do
        @interaction.response_to(:grid, :down, y: 2, &@action)
        press_all @interaction
        assert_equal [[0, 2], [1, 2], [2, 2], [3, 2], [4, 2], [5, 2], [6, 2], [7, 2]], @downs
      end

      it "a complete column of grid buttons" do
        @interaction.response_to(:grid, :down, x: 3, &@action)
        press_all @interaction
        assert_equal [[3, 0], [3, 1], [3, 2], [3, 3], [3, 4], [3, 5], [3, 6], [3, 7]], @downs
      end

      it "a complex range of grid buttons" do
        @interaction.response_to(:grid, :down, x: [1, [2]], y: [1, 3..5], &@action)
        press_all @interaction
        assert_equal [[1, 1], [2, 1], [1, 3], [2, 3], [1, 4], [2, 4], [1, 5], [2, 5]], @downs
      end

      it "a specific grid buttons, a column, a row, all grid buttons and all buttons" do
        @interaction.response_to(:all, :down) { |_i, a| @downs << [a[:x], a[:y], :all] }
        @interaction.response_to(:grid, :down) { |_i, a| @downs << [a[:x], a[:y], :grid] }
        @interaction.response_to(:grid, :down, x: 0) { |_i, a| @downs << [a[:x], a[:y], :col] }
        @interaction.response_to(:grid, :down, y: 0) { |_i, a| @downs << [a[:x], a[:y], :row] }
        @interaction.response_to(:grid, :down, x: 0, y: 0, &@action)
        press @interaction, :grid, x: 0, y: 0
        assert_equal [[0, 0], [0, 0, :col], [0, 0, :row], [0, 0, :grid], [0, 0, :all]], @downs
      end
    end
  end

  describe "regression tests" do
    it "doesn't kvetch when calling stop in response in attached mode" do
      log = StringIO.new
      logger = Logger.new(log)
      logger.level = Logger::ERROR
      inter = SurfaceMaster::Launchpad::Interaction.new(logger: logger)
      inter.response_to(:mixer, :down) { |i, _a| i.stop }
      inter.device.expects(:read)
        .at_least_once
        .returns([{ timestamp: 0,
                    state:     :down,
                    type:      :mixer }])
      # erg =
      timeout { inter.start }
      # assert erg, 'the actions weren\'t called'
      assert_equal "", log.string
    end
  end
end