describe 'Calculator', ->

  KEY =
    TAB   : 9
    ENTER : 13
    ALT   : 18
    ESC   : 27
    SPACE : 32
    LEFT  : 37
    UP    : 38
    RIGHT : 39
    DOWN  : 40

  beforeEach ->
    loadFixtures 'coffee/fixtures/calculator.html'
    @calculator = new Calculator

  describe 'bind', ->
    it 'bind the calculator button', ->
      expect($('.calc')).toHandleWith 'click', @calculator.toggle

    it 'bind key up on calculator', ->
      expect($('#calculator_wrapper')).toHandle 'keyup', @calculator.handleKeyUpOnHint

    it 'bind the help button', ->
      # This events is bind by $.click()
      expect($('#calculator_hint')).toHandle 'click'

    it 'bind the calculator submit', ->
      expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate

    xit 'prevent default behavior on form submit', ->
      jasmine.stubRequests()
      $('form#calculator').submit (e) ->
        expect(e.isDefaultPrevented()).toBeTruthy()
        e.preventDefault()
      $('form#calculator').submit()

  describe 'toggle', ->
    it 'focuses the input when toggled', ->

      # Since the focus is called asynchronously, we need to
      # wait until focus() is called.
      didFocus = false
      runs ->
          spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true
          @calculator.toggle(jQuery.Event("click"))

      waitsFor (-> didFocus), "focus() should have been called on the input", 1000

      runs ->
          expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()

    it 'toggle the close button on the calculator button', ->
      @calculator.toggle(jQuery.Event("click"))
      expect($('.calc')).toHaveClass('closed')

      @calculator.toggle(jQuery.Event("click"))
      expect($('.calc')).not.toHaveClass('closed')

  describe 'showHint', ->
    it 'show the help overlay', ->
      @calculator.showHint()
      expect($('.calc-help-popup')).toHaveClass('shown')
      expect($('.calc-help-popup')).toHaveAttr('aria-hidden', 'false')


  describe 'hideHint', ->
    it 'show the help overlay', ->
      @calculator.hideHint()
      expect($('.calc-help-popup')).not.toHaveClass('shown')
      expect($('.calc-help-popup')).toHaveAttr('aria-hidden', 'true')

  describe 'handleClickOnHintButton', ->
    it 'on click hint button hint popup becomes visible ', ->
      e = jQuery.Event('click');
      $('#calculator_hint').trigger(e);
      expect($('.calc-help-popup')).toHaveClass 'shown'

  describe 'handleClickOnDocument', ->
    it 'on click out of the hint popup it becomes hidden', ->
      @calculator.showHint()
      e = jQuery.Event('click');
      $(document).trigger(e);
      expect($('.calc-help-popup')).not.toHaveClass 'shown'

  describe 'handleClickOnHintPopup', ->
    it 'on click of hint popup it remains visible', ->
      @calculator.showHint()
      e = jQuery.Event('click');
      $('#calculator_input_help').trigger(e);
      expect($('.calc-help-popup')).toHaveClass 'shown'

  describe 'selectHint', ->
    it 'select correct hint item', ->
      spyOn($.fn, 'focus')
      element = $('.hint-item').eq(1)
      @calculator.selectHint(element)

      expect(element.focus).toHaveBeenCalled()
      expect(@calculator.activeHint).toEqual(element)
      expect(@calculator.hintPopup).toHaveAttr('aria-activedescendant', element.attr('id'))

    it 'select the first hint if argument element is not passed', ->
          @calculator.selectHint()
          expect(@calculator.activeHint.attr('id')).toEqual($('.hint-item').first().attr('id'))

    it 'select the first hint if argument element is empty', ->
          @calculator.selectHint([])
          expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').first().attr('id'))

  describe 'prevHint', ->

    it 'Prev hint item is selected', ->
      @calculator.activeHint = $('.hint-item').eq(1)
      @calculator.prevHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))

    it 'if this was the second item, select the first one', ->
      @calculator.activeHint = $('.hint-item').eq(1)
      @calculator.prevHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))

    it 'if this was the first item, select the last one', ->
      @calculator.activeHint = $('.hint-item').eq(0)
      @calculator.prevHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'))

    it 'if this was the last item, select the second last', ->
      @calculator.activeHint = $('.hint-item').eq(2)
      @calculator.prevHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'))

  describe 'nextHint', ->

    it 'if this was the first item, select the second one', ->
      @calculator.activeHint = $('.hint-item').eq(0)
      @calculator.nextHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id'))

    it 'If this was the second item, select the last one', ->
      @calculator.activeHint = $('.hint-item').eq(1)
      @calculator.nextHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(2).attr('id'))

    it 'If this was the last item, select the first one', ->
      @calculator.activeHint = $('.hint-item').eq(2)
      @calculator.nextHint()

      expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id'))

  describe 'handleKeyDown', ->
    assertHintIsHidden = (calc, key) ->
      spyOn(calc, 'hideHint')
      calc.showHint()
      e = jQuery.Event('keydown', { keyCode: key });
      value = calc.handleKeyDown(e)

      expect(calc.hideHint).toHaveBeenCalled
      expect(value).toBeFalsy()
      expect(e.isDefaultPrevented()).toBeTruthy()

    assertHintIsVisible = (calc, key) ->
      spyOn(calc, 'showHint')
      spyOn($.fn, 'focus')
      e = jQuery.Event('keydown', { keyCode: key });
      value = calc.handleKeyDown(e)

      expect(calc.showHint).toHaveBeenCalled
      expect(value).toBeFalsy()
      expect(e.isDefaultPrevented()).toBeTruthy()
      expect(calc.activeHint.focus).toHaveBeenCalled()

    assertNothingHappens = (calc, key) ->
      spyOn(calc, 'showHint')
      e = jQuery.Event('keydown', { keyCode: key });
      value = calc.handleKeyDown(e)

      expect(calc.showHint).not.toHaveBeenCalled
      expect(value).toBeTruthy()
      expect(e.isDefaultPrevented()).toBeFalsy()

    it 'hint popup becomes hidden on press ENTER', ->
      assertHintIsHidden(@calculator, KEY.ENTER)

    it 'hint popup becomes visible on press ENTER', ->
      assertHintIsVisible(@calculator, KEY.ENTER)

    it 'hint popup becomes hidden on press SPACE', ->
      assertHintIsHidden(@calculator, KEY.SPACE)

    it 'hint popup becomes visible on press SPACE', ->
      assertHintIsVisible(@calculator, KEY.SPACE)

    it 'Nothing happens on press ALT', ->
      assertNothingHappens(@calculator, KEY.ALT)

    it 'Nothing happens on press any other button', ->
      assertNothingHappens(@calculator, KEY.DOWN)

  describe 'handleKeyDownOnHint', ->
    it 'Navigation works in proper way', ->
      calc = @calculator

      eventToShowHint = jQuery.Event('keydown', { keyCode: KEY.ENTER } );
      $('#calculator_hint').trigger(eventToShowHint);

      spyOn(calc, 'hideHint')
      spyOn(calc, 'prevHint')
      spyOn(calc, 'nextHint')
      spyOn($.fn, 'focus')

      cases =
        left:
          event:
            keyCode: KEY.LEFT
            shiftKey: false
          returnedValue: false
          called:
            'prevHint': calc
          isPropagationStopped: true

        leftWithShift:
          returnedValue: true
          event:
            keyCode: KEY.LEFT
            shiftKey: true
          not_called:
            'prevHint': calc

        up:
          event:
            keyCode: KEY.UP
            shiftKey: false
          returnedValue: false
          called:
            'prevHint': calc
          isPropagationStopped: true

        upWithShift:
          returnedValue: true
          event:
            keyCode: KEY.UP
            shiftKey: true
          not_called:
            'prevHint': calc

        right:
          event:
            keyCode: KEY.RIGHT
            shiftKey: false
          returnedValue: false
          called:
            'nextHint': calc
          isPropagationStopped: true

        rightWithShift:
          returnedValue: true
          event:
            keyCode: KEY.RIGHT
            shiftKey: true
          not_called:
            'nextHint': calc

        down:
          event:
            keyCode: KEY.DOWN
            shiftKey: false
          returnedValue: false
          called:
            'nextHint': calc
          isPropagationStopped: true

        downWithShift:
          returnedValue: true
          event:
            keyCode: KEY.DOWN
            shiftKey: true
          not_called:
            'nextHint': calc

        esc:
          returnedValue: false
          event:
            keyCode: KEY.ESC
            shiftKey: false
          called:
            'hideHint': calc
            'focus': $.fn
          isPropagationStopped: true

        alt:
          returnedValue: true
          event:
            which: KEY.ALT
          not_called:
            'hideHint': calc
            'nextHint': calc
            'prevHint': calc

      $.each(cases, (key, data) ->
        calc.hideHint.reset()
        calc.prevHint.reset()
        calc.nextHint.reset()
        $.fn.focus.reset()

        e = jQuery.Event('keydown', data.event or {});
        value = calc.handleKeyDownOnHint(e)

        if data.called
          $.each(data.called, (method, obj) ->
            expect(obj[method]).toHaveBeenCalled()
          )

        if data.not_called
          $.each(data.not_called, (method, obj) ->
            expect(obj[method]).not.toHaveBeenCalled()
          )

        if data.isPropagationStopped
          expect(e.isPropagationStopped()).toBeTruthy()
        else
          expect(e.isPropagationStopped()).toBeFalsy()

        expect(value).toBe(data.returnedValue)
      )

  describe 'calculate', ->
    beforeEach ->
      $('#calculator_input').val '1+2'
      spyOn($, 'getWithPrefix').andCallFake (url, data, callback) ->
        callback({ result: 3 })
      @calculator.calculate()

    it 'send data to /calculate', ->
      expect($.getWithPrefix).toHaveBeenCalledWith '/calculate',
        equation: '1+2'
      , jasmine.any(Function)

    it 'update the calculator output', ->
      expect($('#calculator_output').val()).toEqual('3')
