require File.expand_path(File.dirname(__FILE__) + '/test_helper')

class EitherTest < Test::Unit::TestCase
  include Rumonade
  include MonadAxiomTestHelpers

  def test_when_either_constructor_raises
    assert_raise(TypeError) { Either.new }
  end

  def test_when_left_or_right_returns_new_left_or_right
    assert_equal Left.new("error"), Left("error")
    assert_equal Right.new(42), Right(42)
  end

  def test_predicates_for_left_and_right
    assert Left("error").left?
    assert !Right(42).left?
    assert Right(42).right?
    assert !Left("error").right?
  end

  def test_swap_for_left_and_right
    assert_equal Left(42), Right(42).swap
    assert_equal Right("error"), Left("error").swap
  end

  def test_fold_for_left_and_right
    times_two = lambda { |v| v * 2 }
    times_ten = lambda { |v| v * 10 }
    assert_equal "errorerror", Left("error").fold(times_two, times_ten)
    assert_equal 420, Right(42).fold(times_two, times_ten)
  end

  def test_projections_for_left_and_right
    assert_equal Either::LeftProjection.new(Left("error")), Left("error").left
    assert_equal Either::RightProjection.new(Left("error")), Left("error").right
    assert_equal Either::LeftProjection.new(Right(42)), Right(42).left
    assert_equal Either::RightProjection.new(Right(42)), Right(42).right

    assert_not_equal Either::LeftProjection.new(Left("error")), Left("error").right
    assert_not_equal Either::RightProjection.new(Left("error")), Left("error").left
    assert_not_equal Either::LeftProjection.new(Right(42)), Right(42).right
    assert_not_equal Either::RightProjection.new(Right(42)), Right(42).left
  end

  def test_flat_map_for_left_and_right_projections_returns_eithers
    assert_equal Left("42"), Right(42).right.flat_map { |n| Left(n.to_s) }
    assert_equal Right(42), Right(42).left.flat_map { |n| Left(n.to_s) }
    assert_equal Right("ERROR"), Left("error").left.flat_map { |n| Right(n.upcase) }
    assert_equal Left("error"), Left("error").right.flat_map { |n| Right(n.upcase) }
  end

  def test_any_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
    assert Left("error").left.any? { |s| s == "error" }
    assert !Left("error").left.any? { |s| s != "error" }
    assert !Left("error").right.any? { |s| s == "error" }

    assert Right(42).right.any? { |n| n == 42 }
    assert !Right(42).right.any? { |n| n != 42 }
    assert !Right(42).left.any? { |n| n == 42 }
  end

  def test_select_for_left_and_right_projects_returns_option_of_either_if_correct_type_and_block_returns_true
    assert_equal Some(Left("error")), Left("error").left.select { |s| s == "error" }
    assert_equal None, Left("error").left.select { |s| s != "error" }
    assert_equal None, Left("error").right.select { |s| s == "error" }

    assert_equal Some(Right(42)), Right(42).right.select { |n| n == 42 }
    assert_equal None, Right(42).right.select { |n| n != 42 }
    assert_equal None, Right(42).left.select { |n| n == 42 }    
  end
  
  def test_all_predicate_for_left_and_right_projections_returns_true_if_correct_type_and_block_returns_true
    assert Left("error").left.all? { |s| s == "error" }
    assert !Left("error").left.all? { |s| s != "error" }
    assert Left("error").right.all? { |s| s == "error" }

    assert Right(42).right.all? { |n| n == 42 }
    assert !Right(42).right.all? { |n| n != 42 }
    assert Right(42).left.all? { |n| n == 42 }
  end  

  def test_each_for_left_and_right_projections_executes_block_if_correct_type
    def side_effect_occurred_on_each(projection)
      side_effect_occurred = false
      projection.each { |s| side_effect_occurred = true }
      side_effect_occurred
    end

    assert side_effect_occurred_on_each(Left("error").left)
    assert !side_effect_occurred_on_each(Left("error").right)

    assert side_effect_occurred_on_each(Right(42).right)
    assert !side_effect_occurred_on_each(Right(42).left)
  end

  def test_unit_for_left_and_right_projections
    assert_equal Left("error").left, Either::LeftProjection.unit("error")
    assert_equal Right(42).right, Either::RightProjection.unit(42)
  end

  def test_empty_for_left_and_right_projections
    assert_equal Right(nil).left, Either::LeftProjection.empty
    assert_equal Left(nil).right, Either::RightProjection.empty
  end

  def test_monad_axioms_for_left_and_right_projections
    assert_monad_axiom_1(Either::LeftProjection, "error", lambda { |x| Left(x * 2).left })
    assert_monad_axiom_2(Left("error").left)
    assert_monad_axiom_3(Left("error").left, lambda { |x| Left(x * 2).left }, lambda { |x| Left(x * 5).left })

    assert_monad_axiom_1(Either::RightProjection, 42, lambda { |x| Right(x * 2).right })
    assert_monad_axiom_2(Right(42).right)
    assert_monad_axiom_3(Right(42).right, lambda { |x| Right(x * 2).right }, lambda { |x| Right(x * 5).right })
  end

  def test_get_for_left_and_right_projections_returns_value_if_correct_type_or_raises
    assert_equal "error", Left("error").left.get
    assert_raises(NoSuchElementError) { Left("error").right.get }
    assert_equal 42, Right(42).right.get
    assert_raises(NoSuchElementError) { Right(42).left.get }
  end

  def test_get_or_else_for_left_and_right_projections_returns_value_if_correct_type_or_returns_value_or_executes_block
    assert_equal "error", Left("error").left.get_or_else(:other_value)
    assert_equal :other_value, Left("error").right.get_or_else(:other_value)
    assert_equal :value_of_block, Left("error").right.get_or_else(lambda { :value_of_block })

    assert_equal 42, Right(42).right.get_or_else(:other_value)
    assert_equal :other_value, Right(42).left.get_or_else(:other_value)
    assert_equal :value_of_block, Right(42).left.get_or_else(lambda { :value_of_block })
  end

  def test_to_opt_for_left_and_right_projections_returns_Some_if_correct_type_or_None
    assert_equal Some("error"), Left("error").left.to_opt
    assert_equal None, Left("error").right.to_opt
    assert_equal Some(42), Right(42).right.to_opt
    assert_equal None, Right(42).left.to_opt
  end

  def test_to_a_for_left_and_right_projections_returns_single_element_Array_if_correct_type_or_zero_element_Array
    assert_equal ["error"], Left("error").left.to_a
    assert_equal [], Left("error").right.to_a
    assert_equal [42], Right(42).right.to_a
    assert_equal [], Right(42).left.to_a
  end

  def test_map_for_left_and_right_projections_returns_same_projection_type_of_new_value_if_correct_type
    assert_equal Left(:ERROR), Left("error").left.map { |s| s.upcase.to_sym }
    assert_equal Left("error"), Left("error").right.map { |s| s.upcase.to_sym }
    assert_equal Right(420), Right(42).right.map { |s| s * 10 }
    assert_equal Right(42), Right(42).left.map { |s| s * 10 }
  end

  def test_to_s_for_left_and_right_and_their_projections
    assert_equal "Left(error)", Left("error").to_s
    assert_equal "Right(42)", Right(42).to_s
    assert_equal "RightProjection(Left(error))", Left("error").right.to_s
    assert_equal "LeftProjection(Right(42))", Right(42).left.to_s
  end

  def test_inspect_for_left_and_right_and_their_projections
    assert_equal "Left(\"error\")", Left("error").inspect
    assert_equal "Right(\"success\")", Right("success").inspect
    assert_equal "RightProjection(Left(\"error\"))", Left("error").right.inspect
    assert_equal "LeftProjection(Right(\"success\"))", Right("success").left.inspect
  end

  def test_plus_concatenates_left_and_right_using_plus_operator
    assert_equal Left("badworse"), Left("bad") + Right(1) + Left("worse") + Right(2)
    assert_equal Left(["bad", "worse"]), Left(["bad"]) + Right(1) + Left(["worse"]) + Right(2)
    assert_equal Right(3), Right(1) + Right(2)
  end

  def test_concat_concatenates_left_and_right_with_custom_concatenation_function
    multiply = lambda { |a, b| a * b }
    assert_equal Left(33), Left(3).concat(Left(11), :concat_left => multiply)
    assert_equal Left(14), Left(3).concat(Left(11), :concat_right => multiply)
    assert_equal Right(44), Right(4).concat(Right(11), :concat_right => multiply)
    assert_equal Right(15), Right(4).concat(Right(11), :concat_left => multiply)
  end

  def test_lift_to_a_wraps_left_and_right_values_in_array
    assert_equal Left(["error"]), Left("error").lift_to_a
    assert_equal Right([42]), Right(42).lift_to_a
  end

  def test_plus_and_lift_to_a_work_together_to_concatenate_errors
    assert_equal Left([1, 2]), Left(1).lift_to_a + Right(:a).lift_to_a + Left(2).lift_to_a + Right(:b).lift_to_a
    assert_equal Right([:a, :b]), Right(:a).lift_to_a + Right(:b).lift_to_a
  end

  Person = Struct.new(:name, :age, :address)

  def test_concat_maps_concatenated_right_values_through_a_block
    assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
                 Right(["Joe"]).concat(Right([23])).concat(Right([["123 Some St", "Boston"]])) { |n, a, addr| Person.new(n, a, addr) }
    # this usage is equivalent, but since ruby can't pass a block to a binary operator, must use .right.map on result:
    assert_equal Right(Person.new("Joe", 23, ["123 Some St", "Boston"])),
                 (Right(["Joe"]) + Right([23]) + Right([["123 Some St", "Boston"]])).right.map { |n, a, addr| Person.new(n, a, addr) }
  end
end
