from typing import Union
from unittest import mock

import graphene
import pytest
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.template.defaultfilters import slugify
from graphene.utils.str_converters import to_camel_case

from saleor.core.taxes import zero_money
from saleor.graphql.core.utils import snake_to_camel_case
from saleor.graphql.product.enums import AttributeTypeEnum, AttributeValueType
from saleor.graphql.product.filters import filter_attributes_by_product_types
from saleor.graphql.product.mutations.attributes import validate_value_is_unique
from saleor.graphql.product.types.attributes import resolve_attribute_value_type
from saleor.product import AttributeInputType
from saleor.product.error_codes import ProductErrorCode
from saleor.product.models import (
    Attribute,
    AttributeProduct,
    AttributeValue,
    AttributeVariant,
    Category,
    Collection,
    Product,
    ProductType,
    ProductVariant,
)
from saleor.product.utils.attributes import associate_attribute_values_to_instance
from tests.api.utils import get_graphql_content


def test_validate_value_is_unique(color_attribute):
    value = color_attribute.values.first()

    # a new value but with existing slug should raise an error
    with pytest.raises(ValidationError):
        validate_value_is_unique(color_attribute, AttributeValue(slug=value.slug))

    # a new value with a new slug should pass
    validate_value_is_unique(
        color_attribute, AttributeValue(slug="spanish-inquisition")
    )

    # value that already belongs to the attribute shouldn't be taken into account
    validate_value_is_unique(color_attribute, value)


def test_get_single_attribute_by_pk(user_api_client, color_attribute_without_values):
    attribute_gql_id = graphene.Node.to_global_id(
        "Attribute", color_attribute_without_values.id
    )
    query = """
    query($id: ID!) {
        attribute(id: $id) {
            id
            slug
        }
    }
    """
    content = get_graphql_content(
        user_api_client.post_graphql(query, {"id": attribute_gql_id})
    )

    assert content["data"]["attribute"], "Should have found an attribute"
    assert content["data"]["attribute"]["id"] == attribute_gql_id
    assert content["data"]["attribute"]["slug"] == color_attribute_without_values.slug


QUERY_ATTRIBUTES = """
    query {
        attributes(first: 20) {
            edges {
                node {
                    id
                    name
                    slug
                    values {
                        id
                        name
                        slug
                    }
                }
            }
        }
    }
"""


def test_attributes_query(user_api_client, product):
    attributes = Attribute.objects
    query = QUERY_ATTRIBUTES
    response = user_api_client.post_graphql(query)
    content = get_graphql_content(response)
    attributes_data = content["data"]["attributes"]["edges"]
    assert attributes_data
    assert len(attributes_data) == attributes.count()


def test_attributes_query_hidden_attribute(user_api_client, product, color_attribute):
    query = QUERY_ATTRIBUTES

    # hide the attribute
    color_attribute.visible_in_storefront = False
    color_attribute.save(update_fields=["visible_in_storefront"])

    attribute_count = Attribute.objects.get_visible_to_user(
        user_api_client.user
    ).count()
    assert attribute_count == 1

    response = user_api_client.post_graphql(query)
    content = get_graphql_content(response)
    attributes_data = content["data"]["attributes"]["edges"]
    assert len(attributes_data) == attribute_count


def test_attributes_query_hidden_attribute_as_staff_user(
    staff_api_client, product, color_attribute, permission_manage_products
):
    query = QUERY_ATTRIBUTES

    # hide the attribute
    color_attribute.visible_in_storefront = False
    color_attribute.save(update_fields=["visible_in_storefront"])

    attribute_count = Attribute.objects.all().count()

    # The user doesn't have the permission yet to manage products,
    # the user shouldn't be able to see the hidden attributes
    assert Attribute.objects.get_visible_to_user(staff_api_client.user).count() == 1

    # The user should now be able to see the attributes
    staff_api_client.user.user_permissions.add(permission_manage_products)

    response = staff_api_client.post_graphql(query)
    content = get_graphql_content(response)
    attributes_data = content["data"]["attributes"]["edges"]
    assert len(attributes_data) == attribute_count


QUERY_PRODUCT_AND_VARIANTS_ATTRIBUTES = """
    {
      products(first: 1) {
        edges {
          node {
            attributes {
              attribute {
                slug
              }
              values {
                slug
              }
              value {
                slug
              }
            }
            variants {
              attributes {
                attribute {
                  slug
                }
                values {
                  slug
                }
                value {
                  slug
                }
              }
            }
          }
        }
      }
    }
"""


@pytest.mark.parametrize("is_staff", (False, True))
def test_resolve_attributes_with_hidden(
    user_api_client,
    product,
    color_attribute,
    size_attribute,
    staff_user,
    is_staff,
    permission_manage_products,
):
    """Ensure non-staff users don't see hidden attributes, and staff users having
    the 'manage product' permission can.
    """
    query = QUERY_PRODUCT_AND_VARIANTS_ATTRIBUTES
    api_client = user_api_client

    variant = product.variants.first()

    product_attribute = color_attribute
    variant_attribute = size_attribute

    expected_product_attribute_count = product.attributes.count() - 1
    expected_variant_attribute_count = variant.attributes.count() - 1

    if is_staff:
        api_client.user = staff_user
        expected_product_attribute_count += 1
        expected_variant_attribute_count += 1
        staff_user.user_permissions.add(permission_manage_products)

    # Hide one product and variant attribute from the storefront
    for attribute in (product_attribute, variant_attribute):
        attribute.visible_in_storefront = False
        attribute.save(update_fields=["visible_in_storefront"])

    product = get_graphql_content(api_client.post_graphql(query))["data"]["products"][
        "edges"
    ][0]["node"]

    assert len(product["attributes"]) == expected_product_attribute_count
    assert len(product["variants"][0]["attributes"]) == expected_variant_attribute_count


def test_resolve_attribute_values(user_api_client, product, staff_user):
    """Ensure the attribute values are properly resolved."""
    query = QUERY_PRODUCT_AND_VARIANTS_ATTRIBUTES
    api_client = user_api_client

    variant = product.variants.first()

    assert product.attributes.count() == 1
    assert variant.attributes.count() == 1

    product_attribute_values = list(
        product.attributes.first().values.values_list("slug", flat=True)
    )
    variant_attribute_values = list(
        variant.attributes.first().values.values_list("slug", flat=True)
    )

    assert len(product_attribute_values) == 1
    assert len(variant_attribute_values) == 1

    product = get_graphql_content(api_client.post_graphql(query))["data"]["products"][
        "edges"
    ][0]["node"]

    product_attributes = product["attributes"]
    variant_attributes = product["variants"][0]["attributes"]

    assert len(product_attributes) == len(product_attribute_values)
    assert len(variant_attributes) == len(variant_attribute_values)

    assert product_attributes[0]["attribute"]["slug"] == "color"
    assert product_attributes[0]["values"][0]["slug"] == product_attribute_values[0]
    assert product_attributes[0]["value"]["slug"] == product_attribute_values[0]

    assert variant_attributes[0]["attribute"]["slug"] == "size"
    assert variant_attributes[0]["values"][0]["slug"] == variant_attribute_values[0]
    assert variant_attributes[0]["value"]["slug"] == variant_attribute_values[0]


def test_resolve_attribute_values_non_assigned_to_node(
    user_api_client, product, staff_user
):
    """Ensure the attribute values are properly resolved when an attribute is part
    of the product type but not of the node (product/variant), thus no values should be
    resolved.
    """
    query = QUERY_PRODUCT_AND_VARIANTS_ATTRIBUTES
    api_client = user_api_client

    variant = product.variants.first()
    product_type = product.product_type

    # Create dummy attributes
    unassigned_product_attribute = Attribute.objects.create(name="P", slug="product")
    unassigned_variant_attribute = Attribute.objects.create(name="V", slug="variant")

    # Create a value for each dummy attribute to ensure they are not returned
    # by the product or variant as they are not associated to them
    AttributeValue.objects.bulk_create(
        [
            AttributeValue(slug="a", name="A", attribute=unassigned_product_attribute),
            AttributeValue(slug="b", name="B", attribute=unassigned_product_attribute),
        ]
    )

    # Assign the dummy attributes to the product type and push them at the top
    # through a sort_order=0 as the other attributes have sort_order=null
    AttributeProduct.objects.create(
        attribute=unassigned_product_attribute, product_type=product_type, sort_order=0
    )
    AttributeVariant.objects.create(
        attribute=unassigned_variant_attribute, product_type=product_type, sort_order=0
    )

    assert product.attributes.count() == 1
    assert variant.attributes.count() == 1

    product = get_graphql_content(api_client.post_graphql(query))["data"]["products"][
        "edges"
    ][0]["node"]

    product_attributes = product["attributes"]
    variant_attributes = product["variants"][0]["attributes"]

    assert len(product_attributes) == 2, "Non-assigned attr from the PT may be missing"
    assert len(variant_attributes) == 2, "Non-assigned attr from the PT may be missing"

    assert product_attributes[0]["attribute"]["slug"] == "product"
    assert product_attributes[0]["values"] == []
    assert variant_attributes[0]["value"] is None

    assert variant_attributes[0]["attribute"]["slug"] == "variant"
    assert variant_attributes[0]["values"] == []
    assert variant_attributes[0]["value"] is None


def test_attributes_filter_by_product_type_with_empty_value():
    """Ensure passing an empty or null value is ignored and the queryset is simply
    returned without any modification.
    """

    qs = Attribute.objects.all()

    assert filter_attributes_by_product_types(qs, "...", "") is qs
    assert filter_attributes_by_product_types(qs, "...", None) is qs


def test_attributes_filter_by_product_type_with_unsupported_field():
    """Ensure using an unknown field to filter attributes by raises a NotImplemented
    exception.
    """

    qs = Attribute.objects.all()

    with pytest.raises(NotImplementedError) as exc:
        filter_attributes_by_product_types(qs, "in_space", "a-value")

    assert exc.value.args == ("Filtering by in_space is unsupported",)


def test_attributes_filter_by_non_existing_category_id():
    """Ensure using a non-existing category ID returns an empty query set."""

    category_id = graphene.Node.to_global_id("Category", -1)
    mocked_qs = mock.MagicMock()
    qs = filter_attributes_by_product_types(mocked_qs, "in_category", category_id)
    assert qs == mocked_qs.none.return_value


@pytest.mark.parametrize("test_deprecated_filter", [True, False])
@pytest.mark.parametrize("tested_field", ["inCategory", "inCollection"])
def test_attributes_in_collection_query(
    user_api_client,
    product_type,
    category,
    collection,
    collection_with_products,
    test_deprecated_filter,
    tested_field,
):
    if "Collection" in tested_field:
        filtered_by_node_id = graphene.Node.to_global_id("Collection", collection.pk)
    elif "Category" in tested_field:
        filtered_by_node_id = graphene.Node.to_global_id("Category", category.pk)
    else:
        raise AssertionError(tested_field)
    expected_qs = Attribute.objects.filter(
        Q(attributeproduct__product_type_id=product_type.pk)
        | Q(attributevariant__product_type_id=product_type.pk)
    )

    # Create another product type and attribute that shouldn't get matched
    other_category = Category.objects.create(name="Other Category", slug="other-cat")
    other_attribute = Attribute.objects.create(name="Other", slug="other")
    other_product_type = ProductType.objects.create(
        name="Other type", has_variants=True, is_shipping_required=True
    )
    other_product_type.product_attributes.add(other_attribute)
    other_product = Product.objects.create(
        name=f"Another Product",
        product_type=other_product_type,
        category=other_category,
        price=zero_money(),
        is_published=True,
    )

    # Create another collection with products but shouldn't get matched
    # as we don't look for this other collection
    other_collection = Collection.objects.create(
        name="Other Collection",
        slug="other-collection",
        is_published=True,
        description="Description",
    )
    other_collection.products.add(other_product)

    query = """
    query($nodeID: ID!) {
        attributes(first: 20, %(filter_input)s) {
            edges {
                node {
                    id
                    name
                    slug
                }
            }
        }
    }
    """

    if test_deprecated_filter:
        query = query % {"filter_input": f"{tested_field}: $nodeID"}
    else:
        query = query % {"filter_input": "filter: { %s: $nodeID }" % tested_field}

    variables = {"nodeID": filtered_by_node_id}
    content = get_graphql_content(user_api_client.post_graphql(query, variables))
    attributes_data = content["data"]["attributes"]["edges"]

    flat_attributes_data = [attr["node"]["slug"] for attr in attributes_data]
    expected_flat_attributes_data = list(expected_qs.values_list("slug", flat=True))

    assert flat_attributes_data == expected_flat_attributes_data


CREATE_ATTRIBUTES_QUERY = """
    mutation createAttribute($name: String!, $values: [AttributeValueCreateInput]) {
        attributeCreate(input: {name: $name, values: $values}) {
            errors {
                field
                message
            }
            productErrors {
                field
                message
                code
            }
            attribute {
                name
                slug
                values {
                    name
                    slug
                }
                productTypes(first: 10) {
                    edges {
                        node {
                            id
                        }
                    }
                }
            }
        }
    }
"""


def test_create_attribute_and_attribute_values(
    staff_api_client, permission_manage_products
):
    query = CREATE_ATTRIBUTES_QUERY

    attribute_name = "Example name"
    name = "Value name"
    variables = {"name": attribute_name, "values": [{"name": name}]}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    assert not content["data"]["attributeCreate"]["errors"]
    data = content["data"]["attributeCreate"]

    # Check if the attribute was correctly created
    assert data["attribute"]["name"] == attribute_name
    assert data["attribute"]["slug"] == slugify(
        attribute_name
    ), "The default slug should be the slugified name"
    assert (
        data["attribute"]["productTypes"]["edges"] == []
    ), "The attribute should not have been assigned to a product type"

    # Check if the attribute values were correctly created
    assert len(data["attribute"]["values"]) == 1
    assert data["attribute"]["values"][0]["name"] == name
    assert data["attribute"]["values"][0]["slug"] == slugify(name)


@pytest.mark.parametrize(
    "input_slug, expected_slug, expected_error",
    (
        ("my-slug", "my-slug", []),
        (None, "my-name", []),
        (
            "",
            None,
            [{"field": "slug", "message": "The attribute's slug cannot be blank."}],
        ),
    ),
)
def test_create_attribute_with_given_slug(
    staff_api_client,
    permission_manage_products,
    input_slug,
    expected_slug,
    expected_error,
):
    staff_api_client.user.user_permissions.add(permission_manage_products)
    query = """
        mutation createAttribute(
            $name: String!, $slug: String) {
        attributeCreate(input: {name: $name, slug: $slug}) {
            errors {
                field
                message
            }
            attribute {
                slug
            }
        }
    }
    """

    attribute_name = "My Name"
    variables = {"name": attribute_name, "slug": input_slug}
    content = get_graphql_content(staff_api_client.post_graphql(query, variables))

    # Check if the error is as expected: null or something else
    assert content["data"]["attributeCreate"]["errors"] == expected_error

    # Check if the slug was correctly set if no error was expected
    if expected_error is None:
        assert content["data"]["attributeCreate"]["attribute"]["slug"] == expected_slug


@pytest.mark.parametrize(
    "name_1, name_2, error_msg, error_code",
    (
        (
            "Red color",
            "Red color",
            "Provided values are not unique.",
            ProductErrorCode.UNIQUE,
        ),
        (
            "Red color",
            "red color",
            "Provided values are not unique.",
            ProductErrorCode.UNIQUE,
        ),
    ),
)
def test_create_attribute_and_attribute_values_errors(
    staff_api_client,
    name_1,
    name_2,
    error_msg,
    error_code,
    permission_manage_products,
    product_type,
):
    query = CREATE_ATTRIBUTES_QUERY
    variables = {"name": "Example name", "values": [{"name": name_1}, {"name": name_2}]}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    errors = content["data"]["attributeCreate"]["errors"]
    assert errors
    assert errors[0]["field"] == "values"
    assert errors[0]["message"] == error_msg

    product_errors = content["data"]["attributeCreate"]["productErrors"]
    assert product_errors[0]["code"] == error_code.name


UPDATE_ATTRIBUTE_QUERY = """
    mutation updateAttribute(
        $id: ID!, $name: String!, $addValues: [AttributeValueCreateInput]!,
        $removeValues: [ID]!) {
    attributeUpdate(
            id: $id,
            input: {
                name: $name, addValues: $addValues,
                removeValues: $removeValues}) {
        errors {
            field
            message
        }
        productErrors {
            field
            message
            code
        }
        attribute {
            name
            slug
            values {
                name
                slug
            }
            productTypes(first: 10) {
                edges {
                    node {
                        id
                    }
                }
            }
        }
    }
}
"""


def test_update_attribute_name(
    staff_api_client, color_attribute, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_QUERY
    attribute = color_attribute
    name = "Wings name"
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    variables = {"name": name, "id": node_id, "addValues": [], "removeValues": []}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    attribute.refresh_from_db()
    data = content["data"]["attributeUpdate"]
    assert data["attribute"]["name"] == name == attribute.name
    assert data["attribute"]["productTypes"]["edges"] == []


def test_update_attribute_remove_and_add_values(
    staff_api_client, color_attribute, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_QUERY
    attribute = color_attribute
    name = "Wings name"
    attribute_value_name = "Red Color"
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    attribute_value_id = attribute.values.first().id
    value_id = graphene.Node.to_global_id("AttributeValue", attribute_value_id)
    variables = {
        "name": name,
        "id": node_id,
        "addValues": [{"name": attribute_value_name}],
        "removeValues": [value_id],
    }
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    attribute.refresh_from_db()
    data = content["data"]["attributeUpdate"]
    assert not data["errors"]
    assert data["attribute"]["name"] == name == attribute.name
    assert not attribute.values.filter(pk=attribute_value_id).exists()
    assert attribute.values.filter(name=attribute_value_name).exists()


def test_update_empty_attribute_and_add_values(
    staff_api_client, color_attribute_without_values, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_QUERY
    attribute = color_attribute_without_values
    name = "Wings name"
    attribute_value_name = "Yellow Color"
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    variables = {
        "name": name,
        "id": node_id,
        "addValues": [{"name": attribute_value_name}],
        "removeValues": [],
    }
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    get_graphql_content(response)
    attribute.refresh_from_db()
    assert attribute.values.count() == 1
    assert attribute.values.filter(name=attribute_value_name).exists()


@pytest.mark.parametrize(
    "name_1, name_2, error_msg, error_code",
    (
        (
            "Red color",
            "Red color",
            "Provided values are not unique.",
            ProductErrorCode.UNIQUE,
        ),
        (
            "Red color",
            "red color",
            "Provided values are not unique.",
            ProductErrorCode.UNIQUE,
        ),
    ),
)
def test_update_attribute_and_add_attribute_values_errors(
    staff_api_client,
    name_1,
    name_2,
    error_msg,
    error_code,
    color_attribute,
    permission_manage_products,
):
    query = UPDATE_ATTRIBUTE_QUERY
    attribute = color_attribute
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    variables = {
        "name": "Example name",
        "id": node_id,
        "removeValues": [],
        "addValues": [{"name": name_1}, {"name": name_2}],
    }
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    errors = content["data"]["attributeUpdate"]["errors"]
    assert errors
    assert errors[0]["field"] == "addValues"
    assert errors[0]["message"] == error_msg

    product_errors = content["data"]["attributeUpdate"]["productErrors"]
    assert product_errors[0]["code"] == error_code.name


def test_update_attribute_and_remove_others_attribute_value(
    staff_api_client, color_attribute, size_attribute, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_QUERY
    attribute = color_attribute
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    size_attribute = size_attribute.values.first()
    attr_id = graphene.Node.to_global_id("AttributeValue", size_attribute.pk)
    variables = {
        "name": "Example name",
        "id": node_id,
        "slug": "example-slug",
        "addValues": [],
        "removeValues": [attr_id],
    }
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    errors = content["data"]["attributeUpdate"]["errors"]
    assert errors
    assert errors[0]["field"] == "removeValues"
    err_msg = "Value %s does not belong to this attribute." % str(size_attribute)
    assert errors[0]["message"] == err_msg

    product_errors = content["data"]["attributeUpdate"]["productErrors"]
    assert product_errors[0]["code"] == ProductErrorCode.INVALID.name


def test_delete_attribute(
    staff_api_client, color_attribute, permission_manage_products, product_type
):
    attribute = color_attribute
    query = """
    mutation deleteAttribute($id: ID!) {
        attributeDelete(id: $id) {
            errors {
                field
                message
            }
            attribute {
                id
            }
        }
    }
    """
    node_id = graphene.Node.to_global_id("Attribute", attribute.id)
    variables = {"id": node_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeDelete"]
    assert data["attribute"]["id"] == variables["id"]
    with pytest.raises(attribute._meta.model.DoesNotExist):
        attribute.refresh_from_db()


CREATE_ATTRIBUTE_VALUE_QUERY = """
    mutation createAttributeValue(
        $attributeId: ID!, $name: String!) {
    attributeValueCreate(
        attribute: $attributeId, input: {name: $name}) {
        productErrors {
            field
            message
            code
        }
        attribute {
            values {
                name
            }
        }
        attributeValue {
            name
            type
            slug
        }
    }
}
"""


def test_create_attribute_value(
    staff_api_client, color_attribute, permission_manage_products
):
    attribute = color_attribute
    query = CREATE_ATTRIBUTE_VALUE_QUERY
    attribute_id = graphene.Node.to_global_id("Attribute", attribute.id)
    name = "test name"
    variables = {"name": name, "attributeId": attribute_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeValueCreate"]
    assert not data["productErrors"]

    attr_data = data["attributeValue"]
    assert attr_data["name"] == name
    assert attr_data["slug"] == slugify(name)
    assert attr_data["type"] == "STRING"
    assert name in [value["name"] for value in data["attribute"]["values"]]


def test_create_attribute_value_not_unique_name(
    staff_api_client, color_attribute, permission_manage_products
):
    attribute = color_attribute
    query = CREATE_ATTRIBUTE_VALUE_QUERY
    attribute_id = graphene.Node.to_global_id("Attribute", attribute.id)
    value_name = attribute.values.first().name
    variables = {"name": value_name, "attributeId": attribute_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeValueCreate"]
    assert data["productErrors"]
    assert data["productErrors"][0]["code"] == ProductErrorCode.ALREADY_EXISTS.name
    assert data["productErrors"][0]["field"] == "name"


def test_create_attribute_value_capitalized_name(
    staff_api_client, color_attribute, permission_manage_products
):
    attribute = color_attribute
    query = CREATE_ATTRIBUTE_VALUE_QUERY
    attribute_id = graphene.Node.to_global_id("Attribute", attribute.id)
    value_name = attribute.values.first().name
    variables = {"name": value_name.upper(), "attributeId": attribute_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeValueCreate"]
    assert data["productErrors"]
    assert data["productErrors"][0]["code"] == ProductErrorCode.ALREADY_EXISTS.name
    assert data["productErrors"][0]["field"] == "name"


UPDATE_ATTRIBUTE_VALUE_QUERY = """
mutation updateChoice(
        $id: ID!, $name: String!) {
    attributeValueUpdate(
    id: $id, input: {name: $name}) {
        errors {
            field
            message
        }
        attributeValue {
            name
            slug
        }
        attribute {
            values {
                name
            }
        }
    }
}
"""


def test_update_attribute_value(
    staff_api_client, pink_attribute_value, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_VALUE_QUERY
    value = pink_attribute_value
    node_id = graphene.Node.to_global_id("AttributeValue", value.id)
    name = "Crimson name"
    variables = {"name": name, "id": node_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeValueUpdate"]
    value.refresh_from_db()
    assert data["attributeValue"]["name"] == name == value.name
    assert data["attributeValue"]["slug"] == slugify(name)
    assert name in [value["name"] for value in data["attribute"]["values"]]


def test_update_attribute_value_name_not_unique(
    staff_api_client, pink_attribute_value, permission_manage_products
):
    query = UPDATE_ATTRIBUTE_VALUE_QUERY
    value = pink_attribute_value.attribute.values.create(
        name="Example Name", slug="example-name", value="#RED"
    )
    node_id = graphene.Node.to_global_id("AttributeValue", value.id)
    variables = {"name": pink_attribute_value.name, "id": node_id}
    response = staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    content = get_graphql_content(response)
    data = content["data"]["attributeValueUpdate"]
    assert data["errors"]
    assert data["errors"][0]["message"]
    assert data["errors"][0]["field"] == "name"


def test_delete_attribute_value(
    staff_api_client, color_attribute, pink_attribute_value, permission_manage_products
):
    value = color_attribute.values.get(name="Red")
    query = """
    mutation updateChoice($id: ID!) {
        attributeValueDelete(id: $id) {
            attributeValue {
                name
                slug
            }
        }
    }
    """
    node_id = graphene.Node.to_global_id("AttributeValue", value.id)
    variables = {"id": node_id}
    staff_api_client.post_graphql(
        query, variables, permissions=[permission_manage_products]
    )
    with pytest.raises(value._meta.model.DoesNotExist):
        value.refresh_from_db()


@pytest.mark.parametrize(
    "raw_value, expected_type",
    [
        ("#0000", AttributeValueType.COLOR),
        ("#FF69B4", AttributeValueType.COLOR),
        ("rgb(255, 0, 0)", AttributeValueType.COLOR),
        ("hsl(0, 100%, 50%)", AttributeValueType.COLOR),
        ("hsla(120,  60%, 70%, 0.3)", AttributeValueType.COLOR),
        ("rgba(100%, 255, 0, 0)", AttributeValueType.COLOR),
        ("http://example.com", AttributeValueType.URL),
        ("https://example.com", AttributeValueType.URL),
        ("ftp://example.com", AttributeValueType.URL),
        ("example.com", AttributeValueType.STRING),
        ("Foo", AttributeValueType.STRING),
        ("linear-gradient(red, yellow)", AttributeValueType.GRADIENT),
        ("radial-gradient(#0000, yellow)", AttributeValueType.GRADIENT),
    ],
)
def test_resolve_attribute_value_type(raw_value, expected_type):
    assert resolve_attribute_value_type(raw_value) == expected_type


def test_resolve_assigned_attribute_without_values(api_client, product_type, product):
    """Ensure the attributes assigned to a product type are resolved even if
    the product doesn't provide any value for it or is not directly associated to it.
    """
    # Retrieve the product's variant
    variant = product.variants.get()

    # Remove all attributes and values from the product and its variant
    product.attributesrelated.clear()
    variant.attributesrelated.clear()

    # Retrieve the product and variant's attributes
    products = get_graphql_content(
        api_client.post_graphql(
            """
        {
          products(first: 10) {
            edges {
              node {
                attributes {
                  attribute {
                    slug
                  }
                  values {
                    name
                  }
                }
                variants {
                  attributes {
                    attribute {
                      slug
                    }
                    values {
                      name
                    }
                  }
                }
              }
            }
          }
        }
    """
        )
    )["data"]["products"]["edges"]

    # Ensure we are only working on one product and variant, the ones we are testing
    assert len(products) == 1
    assert len(products[0]["node"]["variants"]) == 1

    # Retrieve the nodes data
    product = products[0]["node"]
    variant = product["variants"][0]

    # Ensure the product attributes values are all None
    assert len(product["attributes"]) == 1
    assert product["attributes"][0]["attribute"]["slug"] == "color"
    assert product["attributes"][0]["values"] == []

    # Ensure the variant attributes values are all None
    assert variant["attributes"][0]["attribute"]["slug"] == "size"
    assert variant["attributes"][0]["values"] == []


ASSIGN_ATTR_QUERY = """
    mutation assign($productTypeId: ID!, $operations: [AttributeAssignInput]!) {
      attributeAssign(productTypeId: $productTypeId, operations: $operations) {
        errors {
          field
          message
        }
        productType {
          id
          productAttributes {
            id
          }
          variantAttributes {
            id
          }
        }
      }
    }
"""


def test_assign_attributes_to_product_type(
    staff_api_client, permission_manage_products, attribute_list
):
    product_type = ProductType.objects.create(name="Default Type", has_variants=True)
    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    query = ASSIGN_ATTR_QUERY
    operations = []
    variables = {"productTypeId": product_type_global_id, "operations": operations}

    product_attributes_ids = {attr.pk for attr in attribute_list[:2]}
    variant_attributes_ids = {attr.pk for attr in attribute_list[2:]}

    for attr_id in product_attributes_ids:
        operations.append(
            {"type": "PRODUCT", "id": graphene.Node.to_global_id("Attribute", attr_id)}
        )

    for attr_id in variant_attributes_ids:
        operations.append(
            {"type": "VARIANT", "id": graphene.Node.to_global_id("Attribute", attr_id)}
        )

    content = get_graphql_content(
        staff_api_client.post_graphql(
            query, variables, permissions=[permission_manage_products]
        )
    )["data"]["attributeAssign"]
    assert not content["errors"], "Should have succeeded"

    assert content["productType"]["id"] == product_type_global_id
    assert len(content["productType"]["productAttributes"]) == len(
        product_attributes_ids
    )
    assert len(content["productType"]["variantAttributes"]) == len(
        variant_attributes_ids
    )

    found_product_attrs_ids = {
        int(graphene.Node.from_global_id(attr["id"])[1])
        for attr in content["productType"]["productAttributes"]
    }
    found_variant_attrs_ids = {
        int(graphene.Node.from_global_id(attr["id"])[1])
        for attr in content["productType"]["variantAttributes"]
    }

    assert found_product_attrs_ids == product_attributes_ids
    assert found_variant_attrs_ids == variant_attributes_ids


def test_assign_variant_attribute_to_product_type_with_disabled_variants(
    staff_api_client,
    permission_manage_products,
    product_type_without_variant,
    color_attribute_without_values,
):
    """The assignAttribute mutation should raise an error when trying
    to add an attribute as a variant attribute when
    the product type doesn't support variants"""

    product_type = product_type_without_variant
    attribute = color_attribute_without_values
    staff_api_client.user.user_permissions.add(permission_manage_products)

    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    query = ASSIGN_ATTR_QUERY
    operations = [
        {"type": "VARIANT", "id": graphene.Node.to_global_id("Attribute", attribute.pk)}
    ]
    variables = {"productTypeId": product_type_global_id, "operations": operations}

    content = get_graphql_content(staff_api_client.post_graphql(query, variables))[
        "data"
    ]["attributeAssign"]
    assert content["errors"] == [
        {
            "field": "operations",
            "message": "Variants are disabled in this product type.",
        }
    ]


def test_assign_variant_attribute_having_unsupported_input_type(
    staff_api_client, permission_manage_products, product_type, size_attribute
):
    """The assignAttribute mutation should raise an error when trying
    to use an attribute as a variant attribute when
    the attribute's input type doesn't support variants"""

    attribute = size_attribute
    attribute.input_type = AttributeInputType.MULTISELECT
    attribute.save(update_fields=["input_type"])
    product_type.variant_attributes.clear()

    staff_api_client.user.user_permissions.add(permission_manage_products)

    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    query = ASSIGN_ATTR_QUERY
    operations = [
        {"type": "VARIANT", "id": graphene.Node.to_global_id("Attribute", attribute.pk)}
    ]
    variables = {"productTypeId": product_type_global_id, "operations": operations}

    content = get_graphql_content(staff_api_client.post_graphql(query, variables))[
        "data"
    ]["attributeAssign"]
    assert content["errors"] == [
        {
            "field": "operations",
            "message": (
                "Attributes having for input types ['multiselect'] cannot be assigned "
                "as variant attributes"
            ),
        }
    ]


@pytest.mark.parametrize(
    "product_type_attribute_type, gql_attribute_type",
    (
        (AttributeTypeEnum.PRODUCT, AttributeTypeEnum.VARIANT),
        (AttributeTypeEnum.VARIANT, AttributeTypeEnum.PRODUCT),
        (AttributeTypeEnum.PRODUCT, AttributeTypeEnum.PRODUCT),
        (AttributeTypeEnum.VARIANT, AttributeTypeEnum.VARIANT),
    ),
)
def test_assign_attribute_to_product_type_having_already_that_attribute(
    staff_api_client,
    permission_manage_products,
    color_attribute_without_values,
    product_type_attribute_type,
    gql_attribute_type,
):
    """The assignAttribute mutation should raise an error when trying
    to add an attribute already contained in the product type."""

    product_type = ProductType.objects.create(name="Type")
    attribute = color_attribute_without_values
    staff_api_client.user.user_permissions.add(permission_manage_products)

    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    if product_type_attribute_type == AttributeTypeEnum.PRODUCT:
        product_type.product_attributes.add(attribute)
    elif product_type_attribute_type == AttributeTypeEnum.VARIANT:
        product_type.variant_attributes.add(attribute)
    else:
        raise ValueError(f"Unknown: {product_type}")

    query = ASSIGN_ATTR_QUERY
    operations = [
        {
            "type": gql_attribute_type.value,
            "id": graphene.Node.to_global_id("Attribute", attribute.pk),
        }
    ]
    variables = {"productTypeId": product_type_global_id, "operations": operations}

    content = get_graphql_content(staff_api_client.post_graphql(query, variables))[
        "data"
    ]["attributeAssign"]
    assert content["errors"] == [
        {
            "field": "operations",
            "message": "Color (color) have already been assigned to this product type.",
        }
    ]


UNASSIGN_ATTR_QUERY = """
    mutation unAssignAttribute(
      $productTypeId: ID!, $attributeIds: [ID]!
    ) {
      attributeUnassign(productTypeId: $productTypeId, attributeIds: $attributeIds) {
        errors {
          field
          message
        }
        productType {
          id
          variantAttributes {
            id
          }
          productAttributes {
            id
          }
        }
      }
    }
"""


def test_unassign_attributes_from_product_type(
    staff_api_client, permission_manage_products, attribute_list
):
    product_type = ProductType.objects.create(name="Type")
    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    variant_attribute, *product_attributes = attribute_list
    product_type.product_attributes.add(*product_attributes)
    product_type.variant_attributes.add(variant_attribute)

    remaining_attribute_global_id = graphene.Node.to_global_id(
        "Attribute", product_attributes[1].pk
    )

    query = UNASSIGN_ATTR_QUERY
    variables = {
        "productTypeId": product_type_global_id,
        "attributeIds": [
            graphene.Node.to_global_id("Attribute", product_attributes[0].pk)
        ],
    }

    content = get_graphql_content(
        staff_api_client.post_graphql(
            query, variables, permissions=[permission_manage_products]
        )
    )["data"]["attributeUnassign"]
    assert not content["errors"]

    assert content["productType"]["id"] == product_type_global_id
    assert len(content["productType"]["productAttributes"]) == 1
    assert len(content["productType"]["variantAttributes"]) == 1

    assert (
        content["productType"]["productAttributes"][0]["id"]
        == remaining_attribute_global_id
    )


def test_unassign_attributes_not_in_product_type(
    staff_api_client, permission_manage_products, color_attribute_without_values
):
    """The unAssignAttribute mutation should not raise any error when trying
    to remove an attribute that is not/no longer in the product type."""

    staff_api_client.user.user_permissions.add(permission_manage_products)

    product_type = ProductType.objects.create(name="Type")
    product_type_global_id = graphene.Node.to_global_id("ProductType", product_type.pk)

    query = UNASSIGN_ATTR_QUERY
    variables = {
        "productTypeId": product_type_global_id,
        "attributeIds": [
            graphene.Node.to_global_id("Attribute", color_attribute_without_values.pk)
        ],
    }

    content = get_graphql_content(staff_api_client.post_graphql(query, variables))[
        "data"
    ]["attributeUnassign"]
    assert not content["errors"]

    assert content["productType"]["id"] == product_type_global_id
    assert len(content["productType"]["productAttributes"]) == 0
    assert len(content["productType"]["variantAttributes"]) == 0


def test_retrieve_product_attributes_input_type(
    staff_api_client, product, permission_manage_products
):
    query = """
        {
          products(first: 10) {
            edges {
              node {
                attributes {
                  values {
                    type
                    inputType
                  }
                }
              }
            }
          }
        }
    """

    found_products = get_graphql_content(
        staff_api_client.post_graphql(query, permissions=[permission_manage_products])
    )["data"]["products"]["edges"]
    assert len(found_products) == 1

    for gql_attr in found_products[0]["node"]["attributes"]:
        assert len(gql_attr["values"]) == 1
        assert gql_attr["values"][0]["type"] == "STRING"
        assert gql_attr["values"][0]["inputType"] == "DROPDOWN"


@pytest.mark.parametrize(
    "attribute, expected_value",
    (
        ("filterable_in_storefront", True),
        ("filterable_in_dashboard", True),
        ("visible_in_storefront", True),
        ("available_in_grid", True),
        ("value_required", False),
        ("storefront_search_position", 0),
    ),
)
def test_retrieving_the_restricted_attributes_restricted(
    staff_api_client,
    color_attribute,
    permission_manage_products,
    attribute,
    expected_value,
):
    """Checks if the attributes are restricted and if their default value
    is the expected one."""

    attribute = to_camel_case(attribute)
    query = (
        """
        {
          attributes(first: 10) {
            edges {
              node {
                %s
              }
            }
          }
        }
    """
        % attribute
    )

    found_attributes = get_graphql_content(
        staff_api_client.post_graphql(query, permissions=[permission_manage_products])
    )["data"]["attributes"]["edges"]

    assert len(found_attributes) == 1
    assert found_attributes[0]["node"][attribute] == expected_value


ATTRIBUTES_RESORT_QUERY = """
    mutation ProductTypeReorderAttributes(
      $productTypeId: ID!
      $moves: [ReorderInput]!
      $type: AttributeTypeEnum!
    ) {
      productTypeReorderAttributes(
        productTypeId: $productTypeId
        moves: $moves
        type: $type
      ) {
        productType {
          id
          variantAttributes {
            id
            slug
          }
          productAttributes {
            id
          }
        }

        errors {
          field
          message
        }
      }
    }
"""


def test_sort_attributes_within_product_type_invalid_product_type(
    staff_api_client, permission_manage_products
):
    """Try to reorder an invalid product type (invalid ID)."""

    product_type_id = graphene.Node.to_global_id("ProductType", -1)
    attribute_id = graphene.Node.to_global_id("Attribute", -1)

    variables = {
        "type": "VARIANT",
        "productTypeId": product_type_id,
        "moves": [{"id": attribute_id, "sortOrder": 1}],
    }

    content = get_graphql_content(
        staff_api_client.post_graphql(
            ATTRIBUTES_RESORT_QUERY, variables, permissions=[permission_manage_products]
        )
    )["data"]["productTypeReorderAttributes"]

    assert content["errors"] == [
        {
            "field": "productTypeId",
            "message": f"Couldn't resolve to a product type: {product_type_id}",
        }
    ]


def test_sort_attributes_within_product_type_invalid_id(
    staff_api_client, permission_manage_products, color_attribute
):
    """Try to reorder an attribute not associated to the given product type."""

    product_type = ProductType.objects.create(name="Dummy Type")
    product_type_id = graphene.Node.to_global_id("ProductType", product_type.id)

    attribute_id = graphene.Node.to_global_id("Attribute", color_attribute.id)

    variables = {
        "type": "VARIANT",
        "productTypeId": product_type_id,
        "moves": [{"id": attribute_id, "sortOrder": 1}],
    }

    content = get_graphql_content(
        staff_api_client.post_graphql(
            ATTRIBUTES_RESORT_QUERY, variables, permissions=[permission_manage_products]
        )
    )["data"]["productTypeReorderAttributes"]

    assert content["errors"] == [
        {
            "field": "moves",
            "message": f"Couldn't resolve to an attribute: {attribute_id}",
        }
    ]


@pytest.mark.parametrize(
    "attribute_type, relation_field, backref_field",
    (
        ("VARIANT", "variant_attributes", "attributevariant"),
        ("PRODUCT", "product_attributes", "attributeproduct"),
    ),
)
def test_sort_attributes_within_product_type(
    staff_api_client,
    attribute_list,
    permission_manage_products,
    attribute_type,
    relation_field,
    backref_field,
):
    attributes = attribute_list
    assert len(attributes) == 3

    staff_api_client.user.user_permissions.add(permission_manage_products)

    product_type = ProductType.objects.create(name="Dummy Type")
    product_type_id = graphene.Node.to_global_id("ProductType", product_type.id)
    m2m_attributes = getattr(product_type, relation_field)
    m2m_attributes.set(attributes)

    sort_method = getattr(m2m_attributes, f"{relation_field}_sorted")
    attributes = list(sort_method())

    assert len(attributes) == 3

    variables = {
        "type": attribute_type,
        "productTypeId": product_type_id,
        "moves": [
            {
                "id": graphene.Node.to_global_id("Attribute", attributes[0].pk),
                "sortOrder": +1,
            },
            {
                "id": graphene.Node.to_global_id("Attribute", attributes[2].pk),
                "sortOrder": -1,
            },
        ],
    }

    expected_order = [attributes[1].pk, attributes[2].pk, attributes[0].pk]

    content = get_graphql_content(
        staff_api_client.post_graphql(ATTRIBUTES_RESORT_QUERY, variables)
    )["data"]["productTypeReorderAttributes"]
    assert not content["errors"]

    assert (
        content["productType"]["id"] == product_type_id
    ), "Did not return the correct product type"

    gql_attributes = content["productType"][snake_to_camel_case(relation_field)]
    assert len(gql_attributes) == len(expected_order)

    for attr, expected_pk in zip(gql_attributes, expected_order):
        gql_type, gql_attr_id = graphene.Node.from_global_id(attr["id"])
        assert gql_type == "Attribute"
        assert int(gql_attr_id) == expected_pk


ATTRIBUTE_VALUES_RESORT_QUERY = """
    mutation attributeReorderValues($attributeId: ID!, $moves: [ReorderInput]!) {
      attributeReorderValues(attributeId: $attributeId, moves: $moves) {
        attribute {
          id
          values {
            id
          }
        }

        errors {
          field
          message
        }
      }
    }
"""


def test_sort_values_within_attribute_invalid_product_type(
    staff_api_client, permission_manage_products
):
    """Try to reorder an invalid attribute (invalid ID)."""

    attribute_id = graphene.Node.to_global_id("Attribute", -1)
    value_id = graphene.Node.to_global_id("AttributeValue", -1)

    variables = {
        "attributeId": attribute_id,
        "moves": [{"id": value_id, "sortOrder": 1}],
    }

    content = get_graphql_content(
        staff_api_client.post_graphql(
            ATTRIBUTE_VALUES_RESORT_QUERY,
            variables,
            permissions=[permission_manage_products],
        )
    )["data"]["attributeReorderValues"]

    assert content["errors"] == [
        {
            "field": "attributeId",
            "message": f"Couldn't resolve to an attribute: {attribute_id}",
        }
    ]


def test_sort_values_within_attribute_invalid_id(
    staff_api_client, permission_manage_products, color_attribute
):
    """Try to reorder a value not associated to the given attribute."""

    attribute_id = graphene.Node.to_global_id("Attribute", color_attribute.id)
    value_id = graphene.Node.to_global_id("AttributeValue", -1)

    variables = {
        "type": "VARIANT",
        "attributeId": attribute_id,
        "moves": [{"id": value_id, "sortOrder": 1}],
    }

    content = get_graphql_content(
        staff_api_client.post_graphql(
            ATTRIBUTE_VALUES_RESORT_QUERY,
            variables,
            permissions=[permission_manage_products],
        )
    )["data"]["attributeReorderValues"]

    assert content["errors"] == [
        {
            "field": "moves",
            "message": f"Couldn't resolve to an attribute value: {value_id}",
        }
    ]


def test_sort_values_within_attribute(
    staff_api_client, color_attribute, permission_manage_products
):
    attribute = color_attribute
    AttributeValue.objects.create(attribute=attribute, name="Green", slug="green")
    values = list(attribute.values.all())
    assert len(values) == 3

    staff_api_client.user.user_permissions.add(permission_manage_products)

    attribute_id = graphene.Node.to_global_id("Attribute", attribute.id)
    m2m_values = attribute.values
    m2m_values.set(values)

    assert values == sorted(
        values, key=lambda o: o.sort_order if o.sort_order is not None else o.pk
    ), "The values are not properly ordered"

    variables = {
        "attributeId": attribute_id,
        "moves": [
            {
                "id": graphene.Node.to_global_id("AttributeValue", values[0].pk),
                "sortOrder": +1,
            },
            {
                "id": graphene.Node.to_global_id("AttributeValue", values[2].pk),
                "sortOrder": -1,
            },
        ],
    }

    expected_order = [values[1].pk, values[2].pk, values[0].pk]

    content = get_graphql_content(
        staff_api_client.post_graphql(ATTRIBUTE_VALUES_RESORT_QUERY, variables)
    )["data"]["attributeReorderValues"]
    assert not content["errors"]

    assert content["attribute"]["id"] == attribute_id

    gql_values = content["attribute"]["values"]
    assert len(gql_values) == len(expected_order)

    actual_order = []

    for attr, expected_pk in zip(gql_values, expected_order):
        gql_type, gql_attr_id = graphene.Node.from_global_id(attr["id"])
        assert gql_type == "AttributeValue"
        actual_order.append(int(gql_attr_id))

    assert actual_order == expected_order


ATTRIBUTES_FILTER_QUERY = """
    query($filters: AttributeFilterInput!) {
      attributes(first: 10, filter: $filters) {
        edges {
          node {
            name
            slug
          }
        }
      }
    }
"""


def test_search_attributes(api_client, color_attribute, size_attribute):
    variables = {"filters": {"search": "color"}}

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_FILTER_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 1
    assert attributes[0]["node"]["slug"] == "color"


def test_filter_attributes_if_filterable_in_dashboard(
    api_client, color_attribute, size_attribute
):
    color_attribute.filterable_in_dashboard = False
    color_attribute.save(update_fields=["filterable_in_dashboard"])

    variables = {"filters": {"filterableInDashboard": True}}

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_FILTER_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 1
    assert attributes[0]["node"]["slug"] == "size"


def test_filter_attributes_if_available_in_grid(
    api_client, color_attribute, size_attribute
):
    color_attribute.available_in_grid = False
    color_attribute.save(update_fields=["available_in_grid"])

    variables = {"filters": {"availableInGrid": True}}

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_FILTER_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 1
    assert attributes[0]["node"]["slug"] == "size"


def test_filter_attributes_by_global_id_list(api_client, attribute_list):
    global_ids = [
        graphene.Node.to_global_id("Attribute", attribute.pk)
        for attribute in attribute_list[:2]
    ]
    variables = {"filters": {"ids": global_ids}}

    expected_slugs = sorted([attribute_list[0].slug, attribute_list[1].slug])

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_FILTER_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 2
    received_slugs = sorted(
        [attributes[0]["node"]["slug"], attributes[1]["node"]["slug"]]
    )

    assert received_slugs == expected_slugs


ATTRIBUTES_SORT_QUERY = """
    query($sortBy: AttributeSortingInput) {
      attributes(first: 10, sortBy: $sortBy) {
        edges {
          node {
            slug
          }
        }
      }
    }
"""


def test_sort_attributes_by_slug(api_client):
    Attribute.objects.bulk_create(
        [
            Attribute(name="MyAttribute", slug="b"),
            Attribute(name="MyAttribute", slug="a"),
        ]
    )

    variables = {"sortBy": {"field": "SLUG", "direction": "ASC"}}

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_SORT_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 2
    assert attributes[0]["node"]["slug"] == "a"
    assert attributes[1]["node"]["slug"] == "b"


@pytest.mark.parametrize(
    "sort_field, m2m_model",
    (
        ("DASHBOARD_VARIANT_POSITION", AttributeVariant),
        ("DASHBOARD_PRODUCT_POSITION", AttributeProduct),
    ),
)
def test_sort_attributes_by_position_in_product_type(
    api_client,
    color_attribute,
    size_attribute,
    sort_field: str,
    m2m_model: Union[AttributeVariant, AttributeProduct],
):
    """Sorts attributes for dashboard custom ordering inside a given product type."""

    product_type = ProductType.objects.create(name="My Product Type")
    m2m_model.objects.create(
        product_type=product_type, attribute=color_attribute, sort_order=0
    )
    m2m_model.objects.create(
        product_type=product_type, attribute=size_attribute, sort_order=1
    )

    variables = {"sortBy": {"field": sort_field, "direction": "DESC"}}

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_SORT_QUERY, variables)
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 2
    assert attributes[0]["node"]["slug"] == "size"
    assert attributes[1]["node"]["slug"] == "color"


def test_sort_attributes_by_default_sorting(api_client):
    """Don't provide any sorting, this should sort by name by default."""
    Attribute.objects.bulk_create(
        [Attribute(name="A", slug="b"), Attribute(name="B", slug="a")]
    )

    attributes = get_graphql_content(
        api_client.post_graphql(ATTRIBUTES_SORT_QUERY, {})
    )["data"]["attributes"]["edges"]

    assert len(attributes) == 2
    assert attributes[0]["node"]["slug"] == "b"
    assert attributes[1]["node"]["slug"] == "a"


@pytest.mark.parametrize("is_variant", (True, False))
def test_attributes_of_products_are_sorted(
    staff_api_client, product, color_attribute, is_variant
):
    """Ensures the attributes of products and variants are sorted."""

    variant = product.variants.first()

    if is_variant:
        query = """
            query($id: ID!) {
              productVariant(id: $id) {
                attributes {
                  attribute {
                    id
                  }
                }
              }
            }
        """
    else:
        query = """
            query($id: ID!) {
              product(id: $id) {
                attributes {
                  attribute {
                    id
                  }
                }
              }
            }
        """

    # Create a dummy attribute with a higher ID
    # This will allow us to make sure it is always the last attribute
    # when sorted by ID. Thus, we are sure the query is actually passing the test.
    other_attribute = Attribute.objects.create(name="Other", slug="other")

    # Add the attribute to the product type
    if is_variant:
        product.product_type.variant_attributes.set([color_attribute, other_attribute])
    else:
        product.product_type.product_attributes.set([color_attribute, other_attribute])

    # Retrieve the M2M object for the attribute vs the product type
    if is_variant:
        m2m_rel_other_attr = other_attribute.attributevariant.last()
    else:
        m2m_rel_other_attr = other_attribute.attributeproduct.last()

    # Push the last attribute to the top and let the others to None
    m2m_rel_other_attr.sort_order = 0
    m2m_rel_other_attr.save(update_fields=["sort_order"])

    # Assign attributes to the product
    node = variant if is_variant else product  # type: Union[Product, ProductVariant]
    node.attributesrelated.clear()
    associate_attribute_values_to_instance(
        node, color_attribute, color_attribute.values.first()
    )

    # Sort the database attributes by their sort order and ID (when None)
    expected_order = [other_attribute.pk, color_attribute.pk]

    # Make the node ID
    if is_variant:
        node_id = graphene.Node.to_global_id("ProductVariant", variant.pk)
    else:
        node_id = graphene.Node.to_global_id("Product", product.pk)

    # Retrieve the attributes
    data = get_graphql_content(staff_api_client.post_graphql(query, {"id": node_id}))[
        "data"
    ]
    attributes = data["productVariant" if is_variant else "product"]["attributes"]
    actual_order = [
        int(graphene.Node.from_global_id(attr["attribute"]["id"])[1])
        for attr in attributes
    ]

    # Compare the received data against our expectations
    assert actual_order == expected_order

[((179, 1, 179, 51), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(179, 25, 179, 35): '"""is_staff"""', (179, 37, 179, 50): '(False, True)'}, {}), "('is_staff', (False, True))", False, 'import pytest\n'), ((351, 1, 351, 65), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(351, 25, 351, 49): '"""test_deprecated_filter"""', (351, 51, 351, 64): '[True, False]'}, {}), "('test_deprecated_filter', [True, False])", False, 'import pytest\n'), ((352, 1, 352, 72), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(352, 25, 352, 39): '"""tested_field"""', (352, 41, 352, 71): "['inCategory', 'inCollection']"}, {}), "('tested_field', ['inCategory', 'inCollection'])", False, 'import pytest\n'), ((489, 1, 500, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(490, 4, 490, 47): '"""input_slug, expected_slug, expected_error"""', (491, 4, 499, 5): '((\'my-slug\', \'my-slug\', []), (None, \'my-name\', []), (\'\', None, [{\'field\':\n    \'slug\', \'message\': "The attribute\'s slug cannot be blank."}]))'}, {}), '(\'input_slug, expected_slug, expected_error\', ((\n    \'my-slug\', \'my-slug\', []), (None, \'my-name\', []), (\'\', None, [{\'field\':\n    \'slug\', \'message\': "The attribute\'s slug cannot be blank."}])))', False, 'import pytest\n'), ((536, 1, 552, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(537, 4, 537, 43): '"""name_1, name_2, error_msg, error_code"""', (538, 4, 551, 5): "(('Red color', 'Red color', 'Provided values are not unique.',\n    ProductErrorCode.UNIQUE), ('Red color', 'red color',\n    'Provided values are not unique.', ProductErrorCode.UNIQUE))"}, {}), "('name_1, name_2, error_msg, error_code', ((\n    'Red color', 'Red color', 'Provided values are not unique.',\n    ProductErrorCode.UNIQUE), ('Red color', 'red color',\n    'Provided values are not unique.', ProductErrorCode.UNIQUE)))", False, 'import pytest\n'), ((684, 1, 700, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(685, 4, 685, 43): '"""name_1, name_2, error_msg, error_code"""', (686, 4, 699, 5): "(('Red color', 'Red color', 'Provided values are not unique.',\n    ProductErrorCode.UNIQUE), ('Red color', 'red color',\n    'Provided values are not unique.', ProductErrorCode.UNIQUE))"}, {}), "('name_1, name_2, error_msg, error_code', ((\n    'Red color', 'Red color', 'Provided values are not unique.',\n    ProductErrorCode.UNIQUE), ('Red color', 'red color',\n    'Provided values are not unique.', ProductErrorCode.UNIQUE)))", False, 'import pytest\n'), ((957, 1, 974, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(958, 4, 958, 30): '"""raw_value, expected_type"""', (959, 4, 973, 5): "[('#0000', AttributeValueType.COLOR), ('#FF69B4', AttributeValueType.COLOR),\n    ('rgb(255, 0, 0)', AttributeValueType.COLOR), ('hsl(0, 100%, 50%)',\n    AttributeValueType.COLOR), ('hsla(120,  60%, 70%, 0.3)',\n    AttributeValueType.COLOR), ('rgba(100%, 255, 0, 0)', AttributeValueType\n    .COLOR), ('http://example.com', AttributeValueType.URL), (\n    'https://example.com', AttributeValueType.URL), ('ftp://example.com',\n    AttributeValueType.URL), ('example.com', AttributeValueType.STRING), (\n    'Foo', AttributeValueType.STRING), ('linear-gradient(red, yellow)',\n    AttributeValueType.GRADIENT), ('radial-gradient(#0000, yellow)',\n    AttributeValueType.GRADIENT)]"}, {}), "('raw_value, expected_type', [('#0000',\n    AttributeValueType.COLOR), ('#FF69B4', AttributeValueType.COLOR), (\n    'rgb(255, 0, 0)', AttributeValueType.COLOR), ('hsl(0, 100%, 50%)',\n    AttributeValueType.COLOR), ('hsla(120,  60%, 70%, 0.3)',\n    AttributeValueType.COLOR), ('rgba(100%, 255, 0, 0)', AttributeValueType\n    .COLOR), ('http://example.com', AttributeValueType.URL), (\n    'https://example.com', AttributeValueType.URL), ('ftp://example.com',\n    AttributeValueType.URL), ('example.com', AttributeValueType.STRING), (\n    'Foo', AttributeValueType.STRING), ('linear-gradient(red, yellow)',\n    AttributeValueType.GRADIENT), ('radial-gradient(#0000, yellow)',\n    AttributeValueType.GRADIENT)])", False, 'import pytest\n'), ((1183, 1, 1191, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(1184, 4, 1184, 53): '"""product_type_attribute_type, gql_attribute_type"""', (1185, 4, 1190, 5): '((AttributeTypeEnum.PRODUCT, AttributeTypeEnum.VARIANT), (AttributeTypeEnum\n    .VARIANT, AttributeTypeEnum.PRODUCT), (AttributeTypeEnum.PRODUCT,\n    AttributeTypeEnum.PRODUCT), (AttributeTypeEnum.VARIANT,\n    AttributeTypeEnum.VARIANT))'}, {}), "('product_type_attribute_type, gql_attribute_type',\n    ((AttributeTypeEnum.PRODUCT, AttributeTypeEnum.VARIANT), (\n    AttributeTypeEnum.VARIANT, AttributeTypeEnum.PRODUCT), (\n    AttributeTypeEnum.PRODUCT, AttributeTypeEnum.PRODUCT), (\n    AttributeTypeEnum.VARIANT, AttributeTypeEnum.VARIANT)))", False, 'import pytest\n'), ((1357, 1, 1367, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(1358, 4, 1358, 31): '"""attribute, expected_value"""', (1359, 4, 1366, 5): "(('filterable_in_storefront', True), ('filterable_in_dashboard', True), (\n    'visible_in_storefront', True), ('available_in_grid', True), (\n    'value_required', False), ('storefront_search_position', 0))"}, {}), "('attribute, expected_value', ((\n    'filterable_in_storefront', True), ('filterable_in_dashboard', True), (\n    'visible_in_storefront', True), ('available_in_grid', True), (\n    'value_required', False), ('storefront_search_position', 0)))", False, 'import pytest\n'), ((1491, 1, 1497, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(1492, 4, 1492, 51): '"""attribute_type, relation_field, backref_field"""', (1493, 4, 1496, 5): "(('VARIANT', 'variant_attributes', 'attributevariant'), ('PRODUCT',\n    'product_attributes', 'attributeproduct'))"}, {}), "('attribute_type, relation_field, backref_field', ((\n    'VARIANT', 'variant_attributes', 'attributevariant'), ('PRODUCT',\n    'product_attributes', 'attributeproduct')))", False, 'import pytest\n'), ((1798, 1, 1804, 1), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(1799, 4, 1799, 27): '"""sort_field, m2m_model"""', (1800, 4, 1803, 5): "(('DASHBOARD_VARIANT_POSITION', AttributeVariant), (\n    'DASHBOARD_PRODUCT_POSITION', AttributeProduct))"}, {}), "('sort_field, m2m_model', ((\n    'DASHBOARD_VARIANT_POSITION', AttributeVariant), (\n    'DASHBOARD_PRODUCT_POSITION', AttributeProduct)))", False, 'import pytest\n'), ((1848, 1, 1848, 53), 'pytest.mark.parametrize', 'pytest.mark.parametrize', ({(1848, 25, 1848, 37): '"""is_variant"""', (1848, 39, 1848, 52): '(True, False)'}, {}), "('is_variant', (True, False))", False, 'import pytest\n'), ((47, 4, 47, 52), 'saleor.graphql.product.mutations.attributes.validate_value_is_unique', 'validate_value_is_unique', ({(47, 29, 47, 44): 'color_attribute', (47, 46, 47, 51): 'value'}, {}), '(color_attribute, value)', False, 'from saleor.graphql.product.mutations.attributes import validate_value_is_unique\n'), ((51, 23, 53, 5), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(52, 8, 52, 19): '"""Attribute"""', (52, 21, 52, 54): 'color_attribute_without_values.id'}, {}), "('Attribute', color_attribute_without_values.id)", False, 'import graphene\n'), ((95, 14, 95, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(95, 34, 95, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((114, 14, 114, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(114, 34, 114, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((138, 14, 138, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(138, 34, 138, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((275, 35, 275, 85), 'saleor.product.models.Attribute.objects.create', 'Attribute.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((276, 35, 276, 85), 'saleor.product.models.Attribute.objects.create', 'Attribute.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((289, 4, 291, 5), 'saleor.product.models.AttributeProduct.objects.create', 'AttributeProduct.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((292, 4, 294, 5), 'saleor.product.models.AttributeVariant.objects.create', 'AttributeVariant.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((323, 9, 323, 32), 'saleor.product.models.Attribute.objects.all', 'Attribute.objects.all', ({}, {}), '()', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((334, 9, 334, 32), 'saleor.product.models.Attribute.objects.all', 'Attribute.objects.all', ({}, {}), '()', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((345, 18, 345, 60), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(345, 45, 345, 55): '"""Category"""', (345, 57, 345, 59): '-1'}, {}), "('Category', -1)", False, 'import graphene\n'), ((346, 16, 346, 32), 'unittest.mock.MagicMock', 'mock.MagicMock', ({}, {}), '()', False, 'from unittest import mock\n'), ((347, 9, 347, 82), 'saleor.graphql.product.filters.filter_attributes_by_product_types', 'filter_attributes_by_product_types', ({(347, 44, 347, 53): 'mocked_qs', (347, 55, 347, 68): '"""in_category"""', (347, 70, 347, 81): 'category_id'}, {}), "(mocked_qs, 'in_category', category_id)", False, 'from saleor.graphql.product.filters import filter_attributes_by_product_types\n'), ((374, 21, 374, 85), 'saleor.product.models.Category.objects.create', 'Category.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((375, 22, 375, 74), 'saleor.product.models.Attribute.objects.create', 'Attribute.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((376, 25, 378, 5), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((390, 23, 395, 5), 'saleor.product.models.Collection.objects.create', 'Collection.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((470, 14, 470, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(470, 34, 470, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((567, 14, 567, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(567, 34, 567, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((621, 14, 621, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(621, 41, 621, 52): '"""Attribute"""', (621, 54, 621, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((626, 14, 626, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(626, 34, 626, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((640, 14, 640, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(640, 41, 640, 52): '"""Attribute"""', (640, 54, 640, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((642, 15, 642, 79), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(642, 42, 642, 58): '"""AttributeValue"""', (642, 60, 642, 78): 'attribute_value_id'}, {}), "('AttributeValue', attribute_value_id)", False, 'import graphene\n'), ((652, 14, 652, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(652, 34, 652, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((668, 14, 668, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(668, 41, 668, 52): '"""Attribute"""', (668, 54, 668, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((678, 4, 678, 33), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(678, 24, 678, 32): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((712, 14, 712, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(712, 41, 712, 52): '"""Attribute"""', (712, 54, 712, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((722, 14, 722, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(722, 34, 722, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((737, 14, 737, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(737, 41, 737, 52): '"""Attribute"""', (737, 54, 737, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((739, 14, 739, 77), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(739, 41, 739, 57): '"""AttributeValue"""', (739, 59, 739, 76): 'size_attribute.pk'}, {}), "('AttributeValue', size_attribute.pk)", False, 'import graphene\n'), ((750, 14, 750, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(750, 34, 750, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((778, 14, 778, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(778, 41, 778, 52): '"""Attribute"""', (778, 54, 778, 66): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((783, 14, 783, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(783, 34, 783, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((820, 19, 820, 72), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(820, 46, 820, 57): '"""Attribute"""', (820, 59, 820, 71): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((826, 14, 826, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(826, 34, 826, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((842, 19, 842, 72), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(842, 46, 842, 57): '"""Attribute"""', (842, 59, 842, 71): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((848, 14, 848, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(848, 34, 848, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((860, 19, 860, 72), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(860, 46, 860, 57): '"""Attribute"""', (860, 59, 860, 71): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((866, 14, 866, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(866, 34, 866, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((901, 14, 901, 68), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(901, 41, 901, 57): '"""AttributeValue"""', (901, 59, 901, 67): 'value.id'}, {}), "('AttributeValue', value.id)", False, 'import graphene\n'), ((907, 14, 907, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(907, 34, 907, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((922, 14, 922, 68), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(922, 41, 922, 57): '"""AttributeValue"""', (922, 59, 922, 67): 'value.id'}, {}), "('AttributeValue', value.id)", False, 'import graphene\n'), ((927, 14, 927, 43), 'tests.api.utils.get_graphql_content', 'get_graphql_content', ({(927, 34, 927, 42): 'response'}, {}), '(response)', False, 'from tests.api.utils import get_graphql_content\n'), ((948, 14, 948, 68), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(948, 41, 948, 57): '"""AttributeValue"""', (948, 59, 948, 67): 'value.id'}, {}), "('AttributeValue', value.id)", False, 'import graphene\n'), ((1066, 19, 1066, 85), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1067, 29, 1067, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1067, 56, 1067, 69): '"""ProductType"""', (1067, 71, 1067, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1128, 29, 1128, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1128, 56, 1128, 69): '"""ProductType"""', (1128, 71, 1128, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1161, 29, 1161, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1161, 56, 1161, 69): '"""ProductType"""', (1161, 71, 1161, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1202, 19, 1202, 58), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1206, 29, 1206, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1206, 56, 1206, 69): '"""ProductType"""', (1206, 71, 1206, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1261, 19, 1261, 58), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1262, 29, 1262, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1262, 56, 1262, 69): '"""ProductType"""', (1262, 71, 1262, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1268, 36, 1270, 5), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1269, 8, 1269, 19): '"""Attribute"""', (1269, 21, 1269, 45): 'product_attributes[1].pk'}, {}), "('Attribute', product_attributes[1].pk)", False, 'import graphene\n'), ((1305, 19, 1305, 58), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1306, 29, 1306, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1306, 56, 1306, 69): '"""ProductType"""', (1306, 71, 1306, 86): 'product_type.pk'}, {}), "('ProductType', product_type.pk)", False, 'import graphene\n'), ((1378, 16, 1378, 40), 'graphene.utils.str_converters.to_camel_case', 'to_camel_case', ({(1378, 30, 1378, 39): 'attribute'}, {}), '(attribute)', False, 'from graphene.utils.str_converters import to_camel_case\n'), ((1438, 22, 1438, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1438, 49, 1438, 62): '"""ProductType"""', (1438, 64, 1438, 66): '-1'}, {}), "('ProductType', -1)", False, 'import graphene\n'), ((1439, 19, 1439, 62), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1439, 46, 1439, 57): '"""Attribute"""', (1439, 59, 1439, 61): '-1'}, {}), "('Attribute', -1)", False, 'import graphene\n'), ((1466, 19, 1466, 64), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1467, 22, 1467, 80), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1467, 49, 1467, 62): '"""ProductType"""', (1467, 64, 1467, 79): 'product_type.id'}, {}), "('ProductType', product_type.id)", False, 'import graphene\n'), ((1469, 19, 1469, 78), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1469, 46, 1469, 57): '"""Attribute"""', (1469, 59, 1469, 77): 'color_attribute.id'}, {}), "('Attribute', color_attribute.id)", False, 'import graphene\n'), ((1511, 19, 1511, 64), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1512, 22, 1512, 80), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1512, 49, 1512, 62): '"""ProductType"""', (1512, 64, 1512, 79): 'product_type.id'}, {}), "('ProductType', product_type.id)", False, 'import graphene\n'), ((1580, 19, 1580, 62), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1580, 46, 1580, 57): '"""Attribute"""', (1580, 59, 1580, 61): '-1'}, {}), "('Attribute', -1)", False, 'import graphene\n'), ((1581, 15, 1581, 63), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1581, 42, 1581, 58): '"""AttributeValue"""', (1581, 60, 1581, 62): '-1'}, {}), "('AttributeValue', -1)", False, 'import graphene\n'), ((1609, 19, 1609, 78), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1609, 46, 1609, 57): '"""Attribute"""', (1609, 59, 1609, 77): 'color_attribute.id'}, {}), "('Attribute', color_attribute.id)", False, 'import graphene\n'), ((1610, 15, 1610, 63), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1610, 42, 1610, 58): '"""AttributeValue"""', (1610, 60, 1610, 62): '-1'}, {}), "('AttributeValue', -1)", False, 'import graphene\n'), ((1638, 4, 1638, 82), 'saleor.product.models.AttributeValue.objects.create', 'AttributeValue.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1644, 19, 1644, 72), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1644, 46, 1644, 57): '"""Attribute"""', (1644, 59, 1644, 71): 'attribute.id'}, {}), "('Attribute', attribute.id)", False, 'import graphene\n'), ((1814, 19, 1814, 69), 'saleor.product.models.ProductType.objects.create', 'ProductType.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1884, 22, 1884, 74), 'saleor.product.models.Attribute.objects.create', 'Attribute.objects.create', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((38, 9, 38, 39), 'pytest.raises', 'pytest.raises', ({(38, 23, 38, 38): 'ValidationError'}, {}), '(ValidationError)', False, 'import pytest\n'), ((43, 25, 43, 67), 'saleor.product.models.AttributeValue', 'AttributeValue', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((325, 11, 325, 60), 'saleor.graphql.product.filters.filter_attributes_by_product_types', 'filter_attributes_by_product_types', ({(325, 46, 325, 48): 'qs', (325, 50, 325, 55): '"""..."""', (325, 57, 325, 59): '""""""'}, {}), "(qs, '...', '')", False, 'from saleor.graphql.product.filters import filter_attributes_by_product_types\n'), ((326, 11, 326, 62), 'saleor.graphql.product.filters.filter_attributes_by_product_types', 'filter_attributes_by_product_types', ({(326, 46, 326, 48): 'qs', (326, 50, 326, 55): '"""..."""', (326, 57, 326, 61): 'None'}, {}), "(qs, '...', None)", False, 'from saleor.graphql.product.filters import filter_attributes_by_product_types\n'), ((336, 9, 336, 43), 'pytest.raises', 'pytest.raises', ({(336, 23, 336, 42): 'NotImplementedError'}, {}), '(NotImplementedError)', False, 'import pytest\n'), ((337, 8, 337, 69), 'saleor.graphql.product.filters.filter_attributes_by_product_types', 'filter_attributes_by_product_types', ({(337, 43, 337, 45): 'qs', (337, 47, 337, 57): '"""in_space"""', (337, 59, 337, 68): '"""a-value"""'}, {}), "(qs, 'in_space', 'a-value')", False, 'from saleor.graphql.product.filters import filter_attributes_by_product_types\n'), ((363, 30, 363, 85), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(363, 57, 363, 69): '"""Collection"""', (363, 71, 363, 84): 'collection.pk'}, {}), "('Collection', collection.pk)", False, 'import graphene\n'), ((476, 40, 478, 5), 'django.template.defaultfilters.slugify', 'slugify', ({(477, 8, 477, 22): 'attribute_name'}, {}), '(attribute_name)', False, 'from django.template.defaultfilters import slugify\n'), ((486, 53, 486, 66), 'django.template.defaultfilters.slugify', 'slugify', ({(486, 61, 486, 65): 'name'}, {}), '(name)', False, 'from django.template.defaultfilters import slugify\n'), ((786, 9, 786, 58), 'pytest.raises', 'pytest.raises', ({(786, 23, 786, 57): 'attribute._meta.model.DoesNotExist'}, {}), '(attribute._meta.model.DoesNotExist)', False, 'import pytest\n'), ((832, 32, 832, 45), 'django.template.defaultfilters.slugify', 'slugify', ({(832, 40, 832, 44): 'name'}, {}), '(name)', False, 'from django.template.defaultfilters import slugify\n'), ((911, 45, 911, 58), 'django.template.defaultfilters.slugify', 'slugify', ({(911, 53, 911, 57): 'name'}, {}), '(name)', False, 'from django.template.defaultfilters import slugify\n'), ((953, 9, 953, 54), 'pytest.raises', 'pytest.raises', ({(953, 23, 953, 53): 'value._meta.model.DoesNotExist'}, {}), '(value._meta.model.DoesNotExist)', False, 'import pytest\n'), ((976, 11, 976, 50), 'saleor.graphql.product.types.attributes.resolve_attribute_value_type', 'resolve_attribute_value_type', ({(976, 40, 976, 49): 'raw_value'}, {}), '(raw_value)', False, 'from saleor.graphql.product.types.attributes import resolve_attribute_value_type\n'), ((1551, 32, 1551, 72), 'graphene.Node.from_global_id', 'graphene.Node.from_global_id', ({(1551, 61, 1551, 71): "attr['id']"}, {}), "(attr['id'])", False, 'import graphene\n'), ((1681, 32, 1681, 72), 'graphene.Node.from_global_id', 'graphene.Node.from_global_id', ({(1681, 61, 1681, 71): "attr['id']"}, {}), "(attr['id'])", False, 'import graphene\n'), ((1747, 8, 1747, 61), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1747, 35, 1747, 46): '"""Attribute"""', (1747, 48, 1747, 60): 'attribute.pk'}, {}), "('Attribute', attribute.pk)", False, 'import graphene\n'), ((1914, 18, 1914, 74), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1914, 45, 1914, 61): '"""ProductVariant"""', (1914, 63, 1914, 73): 'variant.pk'}, {}), "('ProductVariant', variant.pk)", False, 'import graphene\n'), ((1916, 18, 1916, 67), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1916, 45, 1916, 54): '"""Product"""', (1916, 56, 1916, 66): 'product.pk'}, {}), "('Product', product.pk)", False, 'import graphene\n'), ((39, 50, 39, 81), 'saleor.product.models.AttributeValue', 'AttributeValue', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((108, 22, 110, 5), 'saleor.product.models.Attribute.objects.get_visible_to_user', 'Attribute.objects.get_visible_to_user', ({(109, 8, 109, 28): 'user_api_client.user'}, {}), '(user_api_client.user)', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((128, 22, 128, 45), 'saleor.product.models.Attribute.objects.all', 'Attribute.objects.all', ({}, {}), '()', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((282, 12, 282, 86), 'saleor.product.models.AttributeValue', 'AttributeValue', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((283, 12, 283, 86), 'saleor.product.models.AttributeValue', 'AttributeValue', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((365, 30, 365, 81), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(365, 57, 365, 67): '"""Category"""', (365, 69, 365, 80): 'category.pk'}, {}), "('Category', category.pk)", False, 'import graphene\n'), ((369, 8, 369, 60), 'django.db.models.Q', 'Q', (), '', False, 'from django.db.models import Q\n'), ((370, 10, 370, 62), 'django.db.models.Q', 'Q', (), '', False, 'from django.db.models import Q\n'), ((384, 14, 384, 26), 'saleor.core.taxes.zero_money', 'zero_money', ({}, {}), '()', False, 'from saleor.core.taxes import zero_money\n'), ((1132, 34, 1132, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1132, 61, 1132, 72): '"""Attribute"""', (1132, 74, 1132, 86): 'attribute.pk'}, {}), "('Attribute', attribute.pk)", False, 'import graphene\n'), ((1165, 34, 1165, 87), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1165, 61, 1165, 72): '"""Attribute"""', (1165, 74, 1165, 86): 'attribute.pk'}, {}), "('Attribute', attribute.pk)", False, 'import graphene\n'), ((1219, 18, 1219, 71), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1219, 45, 1219, 56): '"""Attribute"""', (1219, 58, 1219, 70): 'attribute.pk'}, {}), "('Attribute', attribute.pk)", False, 'import graphene\n'), ((1276, 12, 1276, 77), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1276, 39, 1276, 50): '"""Attribute"""', (1276, 52, 1276, 76): 'product_attributes[0].pk'}, {}), "('Attribute', product_attributes[0].pk)", False, 'import graphene\n'), ((1312, 12, 1312, 86), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1312, 39, 1312, 50): '"""Attribute"""', (1312, 52, 1312, 85): 'color_attribute_without_values.pk'}, {}), "('Attribute', color_attribute_without_values.pk)", False, 'import graphene\n'), ((1547, 44, 1547, 79), 'saleor.graphql.core.utils.snake_to_camel_case', 'snake_to_camel_case', ({(1547, 64, 1547, 78): 'relation_field'}, {}), '(relation_field)', False, 'from saleor.graphql.core.utils import snake_to_camel_case\n'), ((1782, 12, 1782, 51), 'saleor.product.models.Attribute', 'Attribute', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1783, 12, 1783, 51), 'saleor.product.models.Attribute', 'Attribute', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1836, 9, 1836, 38), 'saleor.product.models.Attribute', 'Attribute', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1836, 40, 1836, 69), 'saleor.product.models.Attribute', 'Attribute', (), '', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((132, 11, 132, 71), 'saleor.product.models.Attribute.objects.get_visible_to_user', 'Attribute.objects.get_visible_to_user', ({(132, 49, 132, 70): 'staff_api_client.user'}, {}), '(staff_api_client.user)', False, 'from saleor.product.models import Attribute, AttributeProduct, AttributeValue, AttributeVariant, Category, Collection, Product, ProductType, ProductVariant\n'), ((1078, 38, 1078, 86), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1078, 65, 1078, 76): '"""Attribute"""', (1078, 78, 1078, 85): 'attr_id'}, {}), "('Attribute', attr_id)", False, 'import graphene\n'), ((1083, 38, 1083, 86), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1083, 65, 1083, 76): '"""Attribute"""', (1083, 78, 1083, 85): 'attr_id'}, {}), "('Attribute', attr_id)", False, 'import graphene\n'), ((1102, 12, 1102, 52), 'graphene.Node.from_global_id', 'graphene.Node.from_global_id', ({(1102, 41, 1102, 51): "attr['id']"}, {}), "(attr['id'])", False, 'import graphene\n'), ((1106, 12, 1106, 52), 'graphene.Node.from_global_id', 'graphene.Node.from_global_id', ({(1106, 41, 1106, 51): "attr['id']"}, {}), "(attr['id'])", False, 'import graphene\n'), ((1526, 22, 1526, 79), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1526, 49, 1526, 60): '"""Attribute"""', (1526, 62, 1526, 78): 'attributes[0].pk'}, {}), "('Attribute', attributes[0].pk)", False, 'import graphene\n'), ((1530, 22, 1530, 79), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1530, 49, 1530, 60): '"""Attribute"""', (1530, 62, 1530, 78): 'attributes[2].pk'}, {}), "('Attribute', attributes[2].pk)", False, 'import graphene\n'), ((1656, 22, 1656, 80), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1656, 49, 1656, 65): '"""AttributeValue"""', (1656, 67, 1656, 79): 'values[0].pk'}, {}), "('AttributeValue', values[0].pk)", False, 'import graphene\n'), ((1660, 22, 1660, 80), 'graphene.Node.to_global_id', 'graphene.Node.to_global_id', ({(1660, 49, 1660, 65): '"""AttributeValue"""', (1660, 67, 1660, 79): 'values[2].pk'}, {}), "('AttributeValue', values[2].pk)", False, 'import graphene\n'), ((1924, 12, 1924, 65), 'graphene.Node.from_global_id', 'graphene.Node.from_global_id', ({(1924, 41, 1924, 64): "attr['attribute']['id']"}, {}), "(attr['attribute']['id'])", False, 'import graphene\n')]