module Flows
  class Contract
    # Makes a contract fixed size array.
    #
    # Underlying contracts' transformations are applied.
    #
    # @example
    #     name_age = Flows::Contract::Tuple.new(String, Integer)
    #
    #     name_age === ['Roman', 29]
    #     # => true
    #
    #     name_age === [10, 20]
    #     # => false
    class Tuple < Contract
      ARRAY_CONTRACT = CaseEq.new(::Array)

      # @param contracts [Array<Contract, Object>] contract list. {CaseEq} applied to non-contract values.
      def initialize(*contracts)
        @contracts = contracts.map(&method(:to_contract))
      end

      # @see Contract#check!
      def check!(other)
        ARRAY_CONTRACT.check!(other)
        check_length(other)

        errors = collect_errors(other)
        return true if errors.empty?

        raise Error.new(other, render_errors(other, errors))
      end

      # @see Contract#transform!
      def transform!(other)
        check!(other)

        other.map.with_index do |elem, index|
          @contracts[index].transform!(elem)
        end
      end

      private

      def check_length(other)
        return if other.length == @contracts.length

        raise Error.new(other, "array length mismatch: must be #{@contracts.length}, got #{other.length}")
      end

      def collect_errors(other)
        other.each_with_object({}).with_index do |(elem, errors), index|
          result = @contracts[index].check(elem)

          errors[index] = result.error if result.err?
        end
      end

      def render_errors(other, errors)
        errors.map do |index, err|
          elem = other[index]
          merge_nested_errors(
            "array element `#{elem.inspect}` with index #{index} is invalid:",
            err
          )
        end.join("\n")
      end
    end
  end
end