diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php
new file mode 100644
index 00000000..e5127308
--- /dev/null
+++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\Tests\commerce_cart\FunctionalJavascript;
+
+use Drupal\commerce_product\Entity\ProductVariationType;
+use Drupal\Tests\commerce\FunctionalJavascript\JavascriptTestTrait;
+use Drupal\Tests\commerce_cart\Functional\CartBrowserTestBase;
+
+/**
+ * Tests the add to cart form.
+ *
+ * @group commerce
+ */
+class AddToCartOptionalAttributeTest extends CartBrowserTestBase {
+
+  use JavascriptTestTrait;
+
+  /**
+   * Tests add-to-cart form where variation have mutually exclusive attributes.
+   *
+   * @see https://www.drupal.org/node/2730643
+   */
+  public function testMutuallyExclusiveAttributeMatrixTwoByTwo() {
+    /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */
+    $variation_type = ProductVariationType::load($this->variation->bundle());
+
+    // All attribute-groups have an 'x' value that stand for 'empty'.
+    $number_attributes = $this->createAttributeSet($variation_type, 'number', [
+      'one' => 'one',
+      'two' => 'two',
+    ]);
+    $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [
+      'alpha' => 'alpha',
+      'omega' => 'omega',
+    ]);
+
+    $attribute_values_matrix = [
+      ['one', 'omega'],
+      ['two', 'alpha'],
+    ];
+
+    // Generate variations from the attribute-matrix.
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = $this->createEntity('commerce_product_variation', [
+        'type' => $variation_type->id(),
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_number' => $number_attributes[$value[0]],
+        'attribute_greek' => $greek_attributes[$value[1]],
+      ]);
+      $variations[] = $variation;
+    }
+    $product = $this->createEntity('commerce_product', [
+      'type' => 'default',
+      'title' => 'OPTIONAL_ATTRIBUTES_TEST',
+      'stores' => [$this->store],
+      'variations' => $variations,
+    ]);
+
+    // Helper variables.
+    $number_selector = 'purchased_entity[0][attributes][attribute_number]';
+    $greek_selector = 'purchased_entity[0][attributes][attribute_greek]';
+
+    // Initial state: ['one', 'x'].
+    $this->drupalGet($product->toUrl());
+    $this->assertAttributeSelected($number_selector, 'one');
+    $this->assertAttributeSelected($greek_selector, 'omega');
+    // Expect that 'number' selector can be used.
+    $this->assertAttributeExists($number_selector, $number_attributes['two']->id());
+    // Expect that 'greek' selector cannot be used.
+    $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id());
+
+    // Use AJAX to change the number-attribute to 'x'.
+    $this->drupalGet($product->toUrl());
+    $this->getSession()->getPage()->selectFieldOption($number_selector, 'two');
+    $this->waitForAjaxToFinish();
+    // New state: ['x', 'alpha'].
+    $this->assertAttributeSelected($number_selector, 'two');
+    $this->assertAttributeSelected($greek_selector, 'alpha');
+    // Expect that 'number' selector can be used.
+    $this->assertAttributeExists($number_selector, $number_attributes['one']->id());
+    // Expect that 'greek' selector cannot be used.
+    $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['omega']->id());
+
+  }
+
+  /**
+   * Tests add-to-cart form where variation have mutually exclusive attributes.
+   *
+   * @group debug
+   *
+   * @see https://www.drupal.org/node/2730643
+   */
+  public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() {
+    /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */
+    $variation_type = ProductVariationType::load($this->variation->bundle());
+
+    // All attribute-groups have an 'x' value that stand for 'empty'.
+    $number_attributes = $this->createAttributeSet($variation_type, 'number', [
+      'one' => 'one',
+      'two' => 'two',
+    ]);
+    $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [
+      'alpha' => 'alpha',
+      'omega' => 'omega',
+    ]);
+    $city_attributes = $this->createAttributeSet($variation_type, 'city', [
+      'milano' => 'milano',
+      'pancevo' => 'pancevo',
+    ]);
+
+    $attribute_values_matrix = [
+      ['one', 'omega', 'pancevo'],
+      ['two', 'alpha', 'pancevo'],
+      ['two', 'omega', 'milano'],
+    ];
+
+    // Generate variations from the attribute-matrix.
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = $this->createEntity('commerce_product_variation', [
+        'type' => $variation_type->id(),
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_number' => $number_attributes[$value[0]],
+        'attribute_greek' => $greek_attributes[$value[1]],
+        'attribute_city' => $city_attributes[$value[2]],
+      ]);
+      $variations[] = $variation;
+    }
+    $product = $this->createEntity('commerce_product', [
+      'type' => 'default',
+      'title' => 'OPTIONAL_ATTRIBUTES_TEST',
+      'stores' => [$this->store],
+      'variations' => $variations,
+    ]);
+
+    // Helper variables.
+    $number_selector = 'purchased_entity[0][attributes][attribute_number]';
+    $greek_selector = 'purchased_entity[0][attributes][attribute_greek]';
+    $city_selector = 'purchased_entity[0][attributes][attribute_city]';
+
+    // Initial state: ['one', 'omega', 'pancevo'].
+    $this->drupalGet($product->toUrl());
+    $this->assertAttributeSelected($number_selector, 'one');
+    $this->assertAttributeSelected($greek_selector, 'omega');
+    $this->assertAttributeSelected($city_selector, 'pancevo');
+
+    // Use AJAX to change the number-attribute to 'two'.
+    $this->drupalGet($product->toUrl());
+    $this->getSession()->getPage()->selectFieldOption($number_selector, 'two');
+    $this->waitForAjaxToFinish();
+    $this->saveHtmlOutput();
+
+    // New state: ['two', 'alpha', 'pancevo'].
+    // The top level attribute was adjusted, so the options are reset.
+    $this->assertAttributeSelected($number_selector, 'two');
+    $this->assertAttributeSelected($greek_selector, 'alpha');
+    $this->assertAttributeSelected($city_selector, 'pancevo');
+
+    $this->assertAttributeExists($number_selector, $number_attributes['one']->id());
+    $this->assertAttributeExists($greek_selector, $greek_attributes['omega']->id());
+    $this->assertAttributeExists($city_selector, $city_attributes['pancevo']->id());
+
+    // Use AJAX to change the number-attribute to 'two'.
+    $this->drupalGet($product->toUrl());
+    $this->getSession()->getPage()->selectFieldOption($greek_selector, 'omega');
+    $this->waitForAjaxToFinish();
+    $this->saveHtmlOutput();
+
+    // New state: ['one', 'omega', 'pancevo'].
+    // The top level attribute was adjusted, so the options are reset.
+    $this->assertAttributeSelected($number_selector, 'one');
+    $this->assertAttributeSelected($greek_selector, 'omega');
+    $this->assertAttributeSelected($city_selector, 'pancevo');
+
+    $this->assertAttributeExists($number_selector, $number_attributes['two']->id());
+    $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id());
+    // We should not be able to change the city.
+    // There is one variation with "one" and "omega", which means there is only
+    // one city option.
+    $this->assertAttributeDoesNotExist($city_selector, $city_attributes['milano']->id());
+  }
+
+}
diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml
index 619897ee..f2e999e5 100644
--- a/modules/product/commerce_product.services.yml
+++ b/modules/product/commerce_product.services.yml
@@ -16,3 +16,7 @@ services:
     arguments: ['@current_route_match']
     tags:
       - { name: 'context_provider' }
+
+  commerce_product.variation_attribute_value_mapper:
+    class: Drupal\commerce_product\ProductVariationAttributeMapper
+    arguments: ['@entity_type.manager', '@entity.repository', '@commerce_product.attribute_field_manager']
diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php
index 8a347cf4..e60b11c2 100644
--- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php
+++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php
@@ -4,6 +4,7 @@ namespace Drupal\commerce_product\Plugin\Field\FieldWidget;
 
 use Drupal\commerce_product\Entity\ProductVariationInterface;
 use Drupal\commerce_product\ProductAttributeFieldManagerInterface;
+use Drupal\commerce_product\ProductVariationAttributeMapperInterface;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Entity\EntityRepositoryInterface;
@@ -41,6 +42,13 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
    */
   protected $attributeStorage;
 
+  /**
+   * The variation attribute value mapper.
+   *
+   * @var \Drupal\commerce_product\ProductVariationAttributeMapperInterface
+   */
+  protected $variationAttributeValueMapper;
+
   /**
    * Constructs a new ProductVariationAttributesWidget object.
    *
@@ -60,12 +68,15 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
    *   The entity repository.
    * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager
    *   The attribute field manager.
+   * @param \Drupal\commerce_product\ProductVariationAttributeMapperInterface $variation_attribute_value_mapper
+   *   The variation attribute value resolver.
    */
-  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) {
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager, ProductVariationAttributeMapperInterface $variation_attribute_value_mapper) {
     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $entity_repository);
 
     $this->attributeFieldManager = $attribute_field_manager;
     $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute');
+    $this->variationAttributeValueMapper = $variation_attribute_value_mapper;
   }
 
   /**
@@ -80,7 +91,8 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
       $configuration['third_party_settings'],
       $container->get('entity_type.manager'),
       $container->get('entity.repository'),
-      $container->get('commerce_product.attribute_field_manager')
+      $container->get('commerce_product.attribute_field_manager'),
+      $container->get('commerce_product.variation_attribute_value_mapper')
     );
   }
 
@@ -224,24 +236,8 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
    *   The selected variation.
    */
   protected function selectVariationFromUserInput(array $variations, array $user_input) {
-    $current_variation = reset($variations);
-    if (!empty($user_input['attributes'])) {
-      $attributes = $user_input['attributes'];
-      foreach ($variations as $variation) {
-        $match = TRUE;
-        foreach ($attributes as $field_name => $value) {
-          if ($variation->getAttributeValueId($field_name) != $value) {
-            $match = FALSE;
-          }
-        }
-        if ($match) {
-          $current_variation = $variation;
-          break;
-        }
-      }
-    }
-
-    return $current_variation;
+    $attributes = !empty($user_input['attributes']) ? $user_input['attributes'] : [];
+    return $this->variationAttributeValueMapper->getVariation($variations, $attributes);
   }
 
   /**
@@ -256,48 +252,7 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
    *   The attribute information, keyed by field name.
    */
   protected function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) {
-    $attributes = [];
-    $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle());
-    $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle());
-    $field_names = array_column($field_map, 'field_name');
-    $attribute_ids = array_column($field_map, 'attribute_id');
-    $index = 0;
-    foreach ($field_names as $field_name) {
-      $field = $field_definitions[$field_name];
-      /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */
-      $attribute = $this->attributeStorage->load($attribute_ids[$index]);
-      // Make sure we have translation for attribute.
-      $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId());
-
-      $attributes[$field_name] = [
-        'field_name' => $field_name,
-        'title' => $attribute->label(),
-        'required' => $field->isRequired(),
-        'element_type' => $attribute->getElementType(),
-      ];
-      // The first attribute gets all values. Every next attribute gets only
-      // the values from variations matching the previous attribute value.
-      // For 'Color' and 'Size' attributes that means getting the colors of all
-      // variations, but only the sizes of variations with the selected color.
-      $callback = NULL;
-      if ($index > 0) {
-        $previous_field_name = $field_names[$index - 1];
-        $previous_field_value = $selected_variation->getAttributeValueId($previous_field_name);
-        $callback = function ($variation) use ($previous_field_name, $previous_field_value) {
-          /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */
-          return $variation->getAttributeValueId($previous_field_name) == $previous_field_value;
-        };
-      }
-
-      $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback);
-      $index++;
-    }
-    // Filter out attributes with no values.
-    $attributes = array_filter($attributes, function ($attribute) {
-      return !empty($attribute['values']);
-    });
-
-    return $attributes;
+    return $this->variationAttributeValueMapper->getAttributeInfo($selected_variation, $variations);
   }
 
   /**
@@ -314,20 +269,7 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem
    *   The attribute values, keyed by attribute ID.
    */
   protected function getAttributeValues(array $variations, $field_name, callable $callback = NULL) {
-    $values = [];
-    foreach ($variations as $variation) {
-      if (is_null($callback) || call_user_func($callback, $variation)) {
-        $attribute_value = $variation->getAttributeValue($field_name);
-        if ($attribute_value) {
-          $values[$attribute_value->id()] = $attribute_value->label();
-        }
-        else {
-          $values['_none'] = '';
-        }
-      }
-    }
-
-    return $values;
+    return $this->variationAttributeValueMapper->getAttributeValues($variations, $field_name, $callback);
   }
 
 }
diff --git a/modules/product/src/ProductVariationAttributeMapper.php b/modules/product/src/ProductVariationAttributeMapper.php
new file mode 100644
index 00000000..1b61ae80
--- /dev/null
+++ b/modules/product/src/ProductVariationAttributeMapper.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Drupal\commerce_product;
+
+use Drupal\commerce_product\Entity\ProductVariationInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+
+class ProductVariationAttributeMapper implements ProductVariationAttributeMapperInterface {
+
+  /**
+   * The attribute field manager.
+   *
+   * @var \Drupal\commerce_product\ProductAttributeFieldManagerInterface
+   */
+  protected $attributeFieldManager;
+
+  /**
+   * The product attribute storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $attributeStorage;
+
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * ProductVariationAttributeValueResolver constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository.
+   * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager
+   *   The attribute field manager.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) {
+    $this->entityRepository = $entity_repository;
+    $this->attributeFieldManager = $attribute_field_manager;
+    $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getVariation(array $variations, array $attribute_values = []) {
+    $current_variation = reset($variations);
+    if (empty($attribute_values)) {
+      return $current_variation;
+    }
+    foreach ($attribute_values as $attribute_field_name => $attribute_value) {
+      if (!empty($variations)) {
+        foreach ($variations as $key => $variation) {
+          if ($variation->getAttributeValueId($attribute_field_name) != $attribute_value) {
+            unset($variations[$key]);
+          }
+        }
+        if (!empty($variations)) {
+          $current_variation = reset($variations);
+        }
+        else {
+          break;
+        }
+      }
+    }
+
+    return $current_variation;
+  }
+
+  /**
+   * Gets the attribute information for the selected product variation.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation
+   *   The selected product variation.
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations
+   *   The available product variations.
+   *
+   * @return array[]
+   *   The attribute information, keyed by field name.
+   */
+  public function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) {
+    $attributes = [];
+    $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle());
+    $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle());
+    $field_names = array_column($field_map, 'field_name');
+    $attribute_ids = array_column($field_map, 'attribute_id');
+    $index = 0;
+    foreach ($field_names as $field_name) {
+      $field = $field_definitions[$field_name];
+      /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */
+      $attribute = $this->attributeStorage->load($attribute_ids[$index]);
+      // Make sure we have translation for attribute.
+      $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId());
+
+      $attributes[$field_name] = [
+        'field_name' => $field_name,
+        'title' => $attribute->label(),
+        'required' => $field->isRequired(),
+        'element_type' => $attribute->getElementType(),
+      ];
+      // The first attribute gets all values. Every next attribute gets only
+      // the values from variations matching the previous attribute value.
+      // For 'Color' and 'Size' attributes that means getting the colors of all
+      // variations, but only the sizes of variations with the selected color.
+      $callback = NULL;
+      if ($index > 0) {
+        $index_limit = $index - 1;
+        // Get all previous field values.
+        $previous_variation_field_values = [];
+        for ($i = 0; $i <= $index_limit; $i++) {
+          $previous_variation_field_values[$field_names[$i]] = $selected_variation->getAttributeValueId($field_names[$i]);
+        }
+
+        $callback = function (ProductVariationInterface $variation) use ($previous_variation_field_values) {
+          $results = [];
+          foreach ($previous_variation_field_values as $previous_field_name => $previous_field_value) {
+            $results[] = $variation->getAttributeValueId($previous_field_name) == $previous_field_value;
+          }
+          return !in_array(FALSE, $results, TRUE);
+        };
+      }
+
+      $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback);
+      $index++;
+    }
+    // Filter out attributes with no values.
+    $attributes = array_filter($attributes, function ($attribute) {
+      return !empty($attribute['values']);
+    });
+
+    return $attributes;
+  }
+
+  /**
+   * Gets the attribute values of a given set of variations.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations
+   *   The variations.
+   * @param string $field_name
+   *   The field name of the attribute.
+   * @param callable|null $callback
+   *   An optional callback to use for filtering the list.
+   *
+   * @return array[]
+   *   The attribute values, keyed by attribute ID.
+   */
+  public function getAttributeValues(array $variations, $field_name, callable $callback = NULL) {
+    $values = [];
+    foreach ($variations as $variation) {
+      if (is_null($callback) || call_user_func($callback, $variation)) {
+        $attribute_value = $variation->getAttributeValue($field_name);
+        if ($attribute_value) {
+          $values[$attribute_value->id()] = $attribute_value->label();
+        }
+        else {
+          $values['_none'] = '';
+        }
+      }
+    }
+
+    return $values;
+  }
+
+}
diff --git a/modules/product/src/ProductVariationAttributeMapperInterface.php b/modules/product/src/ProductVariationAttributeMapperInterface.php
new file mode 100644
index 00000000..3625c446
--- /dev/null
+++ b/modules/product/src/ProductVariationAttributeMapperInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\commerce_product;
+
+use Drupal\commerce_product\Entity\ProductVariationInterface;
+
+/**
+ * Provides mapping between variations and attributes.
+ *
+ * This is used when working with variations and their attributes, such as
+ * the 'commerce_product_variation_attributes' widget.
+ */
+interface ProductVariationAttributeMapperInterface {
+
+  /**
+   * Gets the variation that best matches the provided the attributes.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations
+   *   The variations.
+   * @param array $attribute_values
+   *   An array of attribute values, keyed by the attribute name.
+   *
+   * @return \Drupal\commerce_product\Entity\ProductVariationInterface
+   *   The variation.
+   */
+  public function getVariation(array $variations, array $attribute_values = []);
+
+  /**
+   * Gets the attribute information for the selected product variation.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation
+   *   The selected product variation.
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations
+   *   The available product variations.
+   *
+   * @return array[]
+   *   The attribute information, keyed by field name.
+   */
+  public function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations);
+
+  /**
+   * Gets the attribute values of a given set of variations.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations
+   *   The variations.
+   * @param string $field_name
+   *   The field name of the attribute.
+   * @param callable|null $callback
+   *   An optional callback to use for filtering the list.
+   *
+   * @return array[]
+   *   The attribute values, keyed by attribute ID.
+   */
+  public function getAttributeValues(array $variations, $field_name, callable $callback = NULL);
+
+}
diff --git a/modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php b/modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php
new file mode 100644
index 00000000..4b239427
--- /dev/null
+++ b/modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php
@@ -0,0 +1,723 @@
+<?php
+
+namespace Drupal\Tests\commerce_product\Kernel;
+
+use Drupal\commerce_product\Entity\Product;
+use Drupal\commerce_product\Entity\ProductAttribute;
+use Drupal\commerce_product\Entity\ProductAttributeValue;
+use Drupal\commerce_product\Entity\ProductVariation;
+use Drupal\commerce_product\Entity\ProductVariationInterface;
+use Drupal\commerce_product\Entity\ProductVariationType;
+use Drupal\commerce_product\Entity\ProductVariationTypeInterface;
+use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
+
+/**
+ * Tests the product variation title generation.
+ *
+ * @group commerce
+ */
+class ProductVariationAttributeMapperTest extends CommerceKernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'path',
+    'commerce_product',
+    'language',
+    'content_translation',
+  ];
+
+  /**
+   * The color attributes values.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductAttributeValue[]
+   */
+  protected $colorAttributes;
+
+  /**
+   * The size attribute values.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductAttributeValue[]
+   */
+  protected $sizeAttributes;
+
+  /**
+   * The variation attribute value mapper.
+   *
+   * @var \Drupal\commerce_product\ProductVariationAttributeMapperInterface
+   */
+  protected $mapper;
+
+  /**
+   * The attribute field manager.
+   *
+   * @var \Drupal\commerce_product\ProductAttributeFieldManagerInterface
+   */
+  protected $attributeFieldManager;
+
+  /**
+   * The RAM attribute values.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductAttributeValue[]
+   */
+  protected $ramAttributes;
+
+  /**
+   * The Disk 1 attribute values.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductAttributeValue[]
+   */
+  protected $disk1Attributes;
+
+  /**
+   * The Disk 2 attribute values.
+   *
+   * @var \Drupal\commerce_product\Entity\ProductAttributeValue[]
+   */
+  protected $disk2Attributes;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('commerce_product_variation');
+    $this->installEntitySchema('commerce_product');
+    $this->installEntitySchema('commerce_product_attribute');
+    $this->installEntitySchema('commerce_product_attribute_value');
+    $this->installConfig(['commerce_product']);
+    $this->attributeFieldManager = $this->container->get('commerce_product.attribute_field_manager');
+    $this->mapper = $this->container->get('commerce_product.variation_attribute_value_mapper');
+
+    $variation_type = ProductVariationType::load('default');
+
+    // Create attributes.
+    $color_attributes = $this->createAttributeSet($variation_type, 'color', [
+      'black' => 'Black',
+      'blue' => 'Blue',
+      'green' => 'Green',
+      'red' => 'Red',
+      'white' => 'White',
+      'yellow' => 'Yellow',
+    ]);
+    $size_attributes = $this->createAttributeSet($variation_type, 'size', [
+      'small' => 'Small',
+      'medium' => 'Medium',
+      'large' => 'Large',
+    ]);
+
+    $ram_attributes = $this->createAttributeSet($variation_type, 'ram', [
+      '4gb' => '4GB',
+      '8gb' => '8GB',
+      '16gb' => '16GB',
+      '32gb' => '32GB',
+    ]);
+
+    $disk1_attributes = $this->createAttributeSet($variation_type, 'disk1', [
+      '1tb' => '1TB',
+      '2tb' => '2TB',
+      '3tb' => '3TB',
+    ]);
+    $disk2_attributes = $this->createAttributeSet($variation_type, 'disk2', [
+      '1tb' => '1TB',
+      '2tb' => '2TB',
+      '3tb' => '3TB',
+    ]);
+
+    $this->colorAttributes = $color_attributes;
+    $this->sizeAttributes = $size_attributes;
+
+    $this->ramAttributes = $ram_attributes;
+    $this->disk1Attributes = $disk1_attributes;
+    $this->disk2Attributes = $disk2_attributes;
+  }
+
+  /**
+   * Tests that if no attributes are passed, the default variation is returned.
+   */
+  public function testResolveWithNoAttributes() {
+    $product = $this->generateThreeByTwoScenario();
+    $resolved_variation = $this->mapper->getVariation($product->getVariations());
+    $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($product->getVariations(), [
+      'attribute_color' => '',
+    ]);
+    $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($product->getVariations(), [
+      'attribute_color' => '',
+      'attribute_size' => '',
+    ]);
+    $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id());
+  }
+
+  /**
+   * Tests that if one attribute passed, the proper variation is returned.
+   */
+  public function testResolveWithWithOneAttribute() {
+    $product = $this->generateThreeByTwoScenario();
+    $variations = $product->getVariations();
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_color' => $this->colorAttributes['blue']->id(),
+    ]);
+    $this->assertEquals($variations[3]->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_size' => $this->sizeAttributes['large']->id(),
+    ]);
+    $this->assertEquals($variations[2]->id(), $resolved_variation->id());
+  }
+
+  /**
+   * Tests that if two attributes are passed, the proper variation is returned.
+   */
+  public function testResolveWithWithTwoAttributes() {
+    $product = $this->generateThreeByTwoScenario();
+    $variations = $product->getVariations();
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_color' => $this->colorAttributes['red']->id(),
+      'attribute_size' => $this->sizeAttributes['large']->id(),
+    ]);
+    $this->assertEquals($variations[2]->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_color' => $this->colorAttributes['blue']->id(),
+      'attribute_size' => $this->sizeAttributes['large']->id(),
+    ]);
+    // An invalid arrangement was passed, fall back to last viable option.
+    // This returns Blue + Small.
+    $this->assertEquals($variations[3]->id(), $resolved_variation->id());
+    $this->assertEquals('Blue', $resolved_variation->getAttributeValue('attribute_color')->label());
+    $this->assertEquals('Small', $resolved_variation->getAttributeValue('attribute_size')->label());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_color' => '',
+      'attribute_size' => $this->sizeAttributes['large']->id(),
+    ]);
+    // A missing attribute was passed for first option.
+    $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_color' => $this->colorAttributes['blue']->id(),
+      'attribute_size' => $this->sizeAttributes['small']->id(),
+    ]);
+    // An empty second option defaults to first variation option.
+    $this->assertEquals($variations[3]->id(), $resolved_variation->id());
+  }
+
+  /**
+   * Tests optional attributes.
+   */
+  public function testResolveWithOptionalAttributes() {
+    $product = $this->generateThreeByTwoOptionalScenario();
+    $variations = $product->getVariations();
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_ram' => $this->ramAttributes['16gb']->id(),
+    ]);
+    $this->assertEquals($variations[1]->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_ram' => $this->ramAttributes['16gb']->id(),
+      'attribute_disk1' => $this->disk1Attributes['1tb']->id(),
+      'attribute_disk2' => $this->disk2Attributes['1tb']->id(),
+    ]);
+    $this->assertEquals($variations[2]->id(), $resolved_variation->id());
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_ram' => $this->ramAttributes['16gb']->id(),
+      'attribute_disk1' => $this->disk1Attributes['1tb']->id(),
+      'attribute_disk2' => $this->disk2Attributes['2tb']->id(),
+    ]);
+    // Falls back to 16GBx1TB, 16GBx1TBx2TB is invalid.
+    $this->assertEquals($variations[1]->id(), $resolved_variation->id());
+  }
+
+  /**
+   * Tests the getAttributeValues method.
+   */
+  public function testGetAttributeValues() {
+    $product = $this->generateThreeByTwoScenario();
+    $variations = $product->getVariations();
+
+    // With no callback, all value should be returned.
+    $values = $this->mapper->getAttributeValues($variations, 'attribute_color');
+    $this->assertTrue(in_array($this->colorAttributes['red']->label(), $values));
+    $this->assertTrue(in_array($this->colorAttributes['blue']->label(), $values));
+
+    // With callback, only specified values should be returned.
+    $values = $this->mapper->getAttributeValues($variations, 'attribute_color', function (ProductVariationInterface $variation) {
+      return $variation->getAttributeValueId('attribute_color') == $this->colorAttributes['blue']->id();
+    });
+    $this->assertTrue(in_array('Blue', $values));
+    $this->assertFalse(in_array('Red', $values));
+  }
+
+  /**
+   * Tests the getAttributeInfo method.
+   */
+  public function testGetAttributeInfo() {
+    $product = $this->generateThreeByTwoScenario();
+    $variations = $product->getVariations();
+
+    // Test from initial variation.
+    $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations);
+
+    $color_attribute_info = $attribute_info['attribute_color'];
+    $this->assertEquals('select', $color_attribute_info['element_type']);
+    $this->assertEquals(1, $color_attribute_info['required']);
+    $this->assertCount(2, $color_attribute_info['values']);
+
+    $size_attribute_info = $attribute_info['attribute_size'];
+    $this->assertEquals('select', $size_attribute_info['element_type']);
+    $this->assertEquals(1, $size_attribute_info['required']);
+    $this->assertCount(3, $size_attribute_info['values']);
+
+    // Test Blue Medium.
+    $attribute_info = $this->mapper->getAttributeInfo($variations[4], $variations);
+
+    $color_attribute_info = $attribute_info['attribute_color'];
+    $this->assertEquals('select', $color_attribute_info['element_type']);
+    $this->assertEquals(1, $color_attribute_info['required']);
+    $this->assertCount(2, $color_attribute_info['values']);
+
+    $size_attribute_info = $attribute_info['attribute_size'];
+    $this->assertEquals('select', $size_attribute_info['element_type']);
+    $this->assertEquals(1, $size_attribute_info['required']);
+    $this->assertCount(2, $size_attribute_info['values']);
+    $this->assertFalse(in_array('Large', $size_attribute_info['values']));
+  }
+
+  /**
+   * Tests the getAttributeInfo method.
+   */
+  public function testGetAttributeInfoOptional() {
+    $product = $this->generateThreeByTwoOptionalScenario();
+    $variations = $product->getVariations();
+
+    // Test from initial variation.
+    $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations);
+
+    $ram_attribute_info = $attribute_info['attribute_ram'];
+    $this->assertEquals('select', $ram_attribute_info['element_type']);
+    $this->assertEquals(1, $ram_attribute_info['required']);
+    $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.');
+    $this->assertCount(2, $ram_attribute_info['values']);
+
+    $disk1_attribute_info = $attribute_info['attribute_disk1'];
+    $this->assertEquals('select', $disk1_attribute_info['element_type']);
+    $this->assertEquals(1, $disk1_attribute_info['required']);
+    $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(1, $disk1_attribute_info['values']);
+
+    // The Disk 2 1TB option should not show. Only "none"
+    //
+    // The default variation is 8GB x 1TB, which does not have the Disk 2 value
+    // so it should only return "_none". The Disk 2 option should have only have
+    // this option is the 16GB RAM option is chosen.
+    $disk2_attribute_info = $attribute_info['attribute_disk2'];
+    $this->assertEquals('select', $disk2_attribute_info['element_type']);
+    $this->assertEquals(1, $disk2_attribute_info['required']);
+    $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    // There are two values. Since this is optional there is a "_none" option.
+    $this->assertCount(1, $disk2_attribute_info['values']);
+    $this->assertTrue(isset($disk2_attribute_info['values']['_none']));
+
+    // Test from with 16GB which has a variation with option.
+    $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations);
+
+    $ram_attribute_info = $attribute_info['attribute_ram'];
+    $this->assertEquals('select', $ram_attribute_info['element_type']);
+    $this->assertEquals(1, $ram_attribute_info['required']);
+    $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.');
+    $this->assertCount(2, $ram_attribute_info['values']);
+
+    $disk1_attribute_info = $attribute_info['attribute_disk1'];
+    $this->assertEquals('select', $disk1_attribute_info['element_type']);
+    $this->assertEquals(1, $disk1_attribute_info['required']);
+    $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(1, $disk1_attribute_info['values']);
+
+    $disk2_attribute_info = $attribute_info['attribute_disk2'];
+    $this->assertEquals('select', $disk2_attribute_info['element_type']);
+    $this->assertEquals(1, $disk2_attribute_info['required']);
+    $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    // There are two values. Since this is optional there is a "_none" option.
+    $this->assertCount(2, $disk2_attribute_info['values']);
+    $this->assertTrue(isset($disk2_attribute_info['values']['_none']));
+  }
+
+  /**
+   * Tests the getAttributeInfo method.
+   */
+  public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() {
+    $product = Product::create([
+      'type' => 'default',
+      'title' => $this->randomMachineName(),
+      'stores' => [$this->store],
+      'variations' => [],
+    ]);
+    $attribute_values_matrix = [
+      ['4gb', '2tb', '2tb'],
+      ['8gb', '1tb', '2tb'],
+      ['8gb', '2tb', '1tb'],
+    ];
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = ProductVariation::create([
+        'type' => 'default',
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_ram' => $this->ramAttributes[$value[0]],
+        'attribute_disk1' => $this->disk1Attributes[$value[1]],
+        'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL,
+      ]);
+      $variation->save();
+      $variations[] = $variation;
+      $product->addVariation($variation);
+    }
+    $product->save();
+
+    // Test from initial variation.
+    $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations);
+
+    $ram_attribute_info = $attribute_info['attribute_ram'];
+    $this->assertEquals('select', $ram_attribute_info['element_type']);
+    $this->assertEquals(1, $ram_attribute_info['required']);
+    $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.');
+    $this->assertCount(2, $ram_attribute_info['values']);
+
+    $disk1_attribute_info = $attribute_info['attribute_disk1'];
+    $this->assertEquals('select', $disk1_attribute_info['element_type']);
+    $this->assertEquals(1, $disk1_attribute_info['required']);
+    $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(1, $disk1_attribute_info['values']);
+
+    $disk2_attribute_info = $attribute_info['attribute_disk2'];
+    $this->assertEquals('select', $disk2_attribute_info['element_type']);
+    $this->assertEquals(1, $disk2_attribute_info['required']);
+    $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(1, $disk2_attribute_info['values']);
+    $this->assertTrue(in_array('2TB', $disk2_attribute_info['values']), 'Only the one valid Disk 2 option is available.');
+
+    // Test 8GB 1TB 2TB.
+    $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations);
+
+    $ram_attribute_info = $attribute_info['attribute_ram'];
+    $this->assertEquals('select', $ram_attribute_info['element_type']);
+    $this->assertEquals(1, $ram_attribute_info['required']);
+    $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.');
+    $this->assertCount(2, $ram_attribute_info['values']);
+
+    $disk1_attribute_info = $attribute_info['attribute_disk1'];
+    $this->assertEquals('select', $disk1_attribute_info['element_type']);
+    $this->assertEquals(1, $disk1_attribute_info['required']);
+    $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(2, $disk1_attribute_info['values']);
+
+    $disk2_attribute_info = $attribute_info['attribute_disk2'];
+    $this->assertEquals('select', $disk2_attribute_info['element_type']);
+    $this->assertEquals(1, $disk2_attribute_info['required']);
+    $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    // There should only be one Disk 2 option, since the other 8GB RAM option
+    // has a Disk 1 value of 2TB.
+    $this->assertCount(1, $disk2_attribute_info['values']);
+
+    // Test 8GB 2TB 1TB.
+    $attribute_info = $this->mapper->getAttributeInfo($variations[2], $variations);
+
+    $ram_attribute_info = $attribute_info['attribute_ram'];
+    $this->assertEquals('select', $ram_attribute_info['element_type']);
+    $this->assertEquals(1, $ram_attribute_info['required']);
+    $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.');
+    $this->assertCount(2, $ram_attribute_info['values']);
+
+    $disk1_attribute_info = $attribute_info['attribute_disk1'];
+    $this->assertEquals('select', $disk1_attribute_info['element_type']);
+    $this->assertEquals(1, $disk1_attribute_info['required']);
+    $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    $this->assertCount(2, $disk1_attribute_info['values']);
+
+    $disk2_attribute_info = $attribute_info['attribute_disk2'];
+    $this->assertEquals('select', $disk2_attribute_info['element_type']);
+    $this->assertEquals(1, $disk2_attribute_info['required']);
+    $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.');
+    // There should only be one Disk 2 option, since the other 8GB RAM option
+    // has a Disk 1 value of 2TB.
+    $this->assertCount(1, $disk2_attribute_info['values'], print_r($disk2_attribute_info['values'], TRUE));
+  }
+
+  /**
+   * Tests having three attributes and six variations.
+   *
+   * @group debug
+   *
+   * @link https://www.drupal.org/project/commerce/issues/2707721
+   */
+  public function testThreeAttributesSixVariations() {
+    $variation_type = ProductVariationType::load('default');
+
+    $pack = $this->createAttributeSet($variation_type, 'pack', [
+      'one' => '1',
+      'twenty' => '20',
+      'hundred' => '100',
+      'twohundred' => '200',
+    ]);
+
+    $product = Product::create([
+      'type' => 'default',
+      'title' => $this->randomMachineName(),
+      'stores' => [$this->store],
+      'variations' => [],
+    ]);
+    $product->save();
+
+    // The Size attribute needs a lighter weight than Color for this scenario.
+    // @todo This is an undocumented item, where the order of the attributes on
+    // the form display correlate to how they display in the widget / returned
+    // values.
+    $form_display = commerce_get_entity_display('commerce_product_variation', $variation_type->id(), 'form');
+    $form_display->setComponent('attribute_size', ['weight' => 0] + $form_display->getComponent('attribute_size'));
+    $form_display->setComponent('attribute_color', ['weight' => 1] + $form_display->getComponent('attribute_color'));
+    $form_display->setComponent('attribute_pack', ['weight' => 2] + $form_display->getComponent('attribute_pack'));
+    $form_display->save();
+
+    $attribute_values_matrix = [
+      ['small', 'black', 'one'],
+      ['small', 'blue', 'twenty'],
+      ['medium', 'green', 'hundred'],
+      ['medium', 'red', 'twohundred'],
+      ['large', 'white', 'hundred'],
+      ['large', 'yellow', 'twenty'],
+    ];
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = ProductVariation::create([
+        'type' => 'default',
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_size' => $this->sizeAttributes[$value[0]],
+        'attribute_color' => $this->colorAttributes[$value[1]],
+        'attribute_pack' => $pack[$value[2]],
+      ]);
+      $variation->save();
+      $variations[] = $variation;
+      $product->addVariation($variation);
+    }
+    $product->save();
+
+    /** @var \Drupal\commerce_product\Entity\ProductVariation[] $variations */
+    $variations = $product->getVariations();
+    $resolved_variation = $this->mapper->getVariation($product->getVariations());
+    $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id());
+    $this->assertEquals('Small', $resolved_variation->getAttributeValue('attribute_size')->label());
+    $this->assertEquals('Black', $resolved_variation->getAttributeValue('attribute_color')->label());
+    $this->assertEquals('1', $resolved_variation->getAttributeValue('attribute_pack')->label());
+
+    // Verify available attribute selections.
+    $attribute_info = $this->mapper->getAttributeInfo($resolved_variation, $product->getVariations());
+    $size_attribute_info = $attribute_info['attribute_size'];
+    $this->assertCount(3, $size_attribute_info['values']);
+    $color_attribute_info = $attribute_info['attribute_color'];
+    $this->assertCount(2, $color_attribute_info['values']);
+    $this->assertTrue(in_array('Black', $color_attribute_info['values']));
+    $this->assertTrue(in_array('Blue', $color_attribute_info['values']));
+    $pack_attribute_info = $attribute_info['attribute_pack'];
+    $this->assertCount(1, $pack_attribute_info['values']);
+    $this->assertTrue(in_array('1', $pack_attribute_info['values']));
+    // The resolved variation is Small -> Black -> 1, cannot choose 20 for the
+    // pack size, since that is Small -> Blue -> 20.
+    $this->assertFalse(in_array('20', $pack_attribute_info['values']));
+
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_size' => $this->sizeAttributes['small']->id(),
+      'attribute_color' => $this->colorAttributes['blue']->id(),
+    ]);
+    $this->assertEquals($variations[1]->id(), $resolved_variation->id());
+
+    // Medium only has Green & Red as color, so selecting this size should
+    // cause the color to reset.
+    $resolved_variation = $this->mapper->getVariation($variations, [
+      'attribute_size' => $this->sizeAttributes['medium']->id(),
+      'attribute_color' => $this->colorAttributes['blue']->id(),
+    ]);
+    $this->assertEquals($variations[2]->id(), $resolved_variation->id());
+    $this->assertEquals('Medium', $resolved_variation->getAttributeValue('attribute_size')->label());
+    $this->assertEquals('Green', $resolved_variation->getAttributeValue('attribute_color')->label());
+    $this->assertEquals('100', $resolved_variation->getAttributeValue('attribute_pack')->label());
+
+    // Verify available attribute selections.
+    $attribute_info = $this->mapper->getAttributeInfo($resolved_variation, $product->getVariations());
+    $size_attribute_info = $attribute_info['attribute_size'];
+    $this->assertCount(3, $size_attribute_info['values'], print_r($size_attribute_info['values'], TRUE));
+    $color_attribute_info = $attribute_info['attribute_color'];
+    $this->assertCount(2, $color_attribute_info['values']);
+    $this->assertTrue(in_array('Green', $color_attribute_info['values']));
+    $this->assertTrue(in_array('Red', $color_attribute_info['values']));
+    $pack_attribute_info = $attribute_info['attribute_pack'];
+    $this->assertCount(1, $pack_attribute_info['values']);
+    // The resolved variation is Medium -> Green -> 100, cannot choose 200 for
+    // the pack size, since that is Medium -> Red -> 200.
+    $this->assertTrue(in_array('100', $pack_attribute_info['values']));
+    $this->assertFalse(in_array('200', $pack_attribute_info['values']));
+  }
+
+  /**
+   * Generates a three by two scenario.
+   *
+   * This generates a product and variations in 3x2 scenario. There are three
+   * sizes and two colors. Missing one color option.
+   *
+   * [ RS, RM, RL ]
+   * [ BS, BM, X  ]
+   *
+   * @return \Drupal\commerce_product\Entity\ProductInterface
+   *   The product.
+   */
+  protected function generateThreeByTwoScenario() {
+    $product = Product::create([
+      'type' => 'default',
+      'title' => $this->randomMachineName(),
+      'stores' => [$this->store],
+      'variations' => [],
+    ]);
+    $attribute_values_matrix = [
+      ['red', 'small'],
+      ['red', 'medium'],
+      ['red', 'large'],
+      ['blue', 'small'],
+      ['blue', 'medium'],
+    ];
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = ProductVariation::create([
+        'type' => 'default',
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_color' => $this->colorAttributes[$value[0]],
+        'attribute_size' => $this->sizeAttributes[$value[1]],
+      ]);
+      $variation->save();
+      $variations[] = $variation;
+      $product->addVariation($variation);
+    }
+    $product->save();
+
+    return $product;
+  }
+
+  /**
+   * Generates a three by two (optional) secenario.
+   *
+   * This generates a product and variations in 3x2 scenario.
+   *
+   * https://www.drupal.org/project/commerce/issues/2730643#comment-11216983
+   *
+   * [ 8GBx1TB,    X        , X ]
+   * [    X   , 16GBx1TB    , X ]
+   * [    X   , 16GBx1TBx1TB, X ]
+   *
+   * @return \Drupal\commerce_product\Entity\ProductInterface
+   *   The product.
+   */
+  protected function generateThreeByTwoOptionalScenario() {
+    $product = Product::create([
+      'type' => 'default',
+      'title' => $this->randomMachineName(),
+      'stores' => [$this->store],
+      'variations' => [],
+    ]);
+    $attribute_values_matrix = [
+      ['8gb', '1tb', ''],
+      ['16gb', '1tb', ''],
+      ['16gb', '1tb', '1tb'],
+    ];
+    $variations = [];
+    foreach ($attribute_values_matrix as $key => $value) {
+      $variation = ProductVariation::create([
+        'type' => 'default',
+        'sku' => $this->randomMachineName(),
+        'price' => [
+          'number' => 999,
+          'currency_code' => 'USD',
+        ],
+        'attribute_ram' => $this->ramAttributes[$value[0]],
+        'attribute_disk1' => $this->disk1Attributes[$value[1]],
+        'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL,
+      ]);
+      $variation->save();
+      $variations[] = $variation;
+      $product->addVariation($variation);
+    }
+    $product->save();
+
+    return $product;
+  }
+
+  /**
+   * Creates an attribute field and set of attribute values.
+   *
+   * @param \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type
+   *   The variation type.
+   * @param string $name
+   *   The attribute field name.
+   * @param array $options
+   *   Associative array of key name values. [red => Red].
+   *
+   * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface[]
+   *   Array of attribute entities.
+   */
+  protected function createAttributeSet(ProductVariationTypeInterface $variation_type, $name, array $options) {
+    $attribute = ProductAttribute::create([
+      'id' => $name,
+      'label' => ucfirst($name),
+    ]);
+    $attribute->save();
+    $this->attributeFieldManager->createField($attribute, $variation_type->id());
+
+    $attribute_set = [];
+    foreach ($options as $key => $value) {
+      $attribute_set[$key] = $this->createAttributeValue($name, $value);
+    }
+
+    return $attribute_set;
+  }
+
+  /**
+   * Creates an attribute value.
+   *
+   * @param string $attribute
+   *   The attribute ID.
+   * @param string $name
+   *   The attribute value name.
+   *
+   * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface
+   *   The attribute value entity.
+   */
+  protected function createAttributeValue($attribute, $name) {
+    $attribute_value = ProductAttributeValue::create([
+      'attribute' => $attribute,
+      'name' => $name,
+    ]);
+    $attribute_value->save();
+
+    return $attribute_value;
+  }
+
+}
