import io

import pytest
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from PIL import Image

from tests import models
from tests.utils import deep_tuple_to_list


def prep_image(fname):
    """Return an SimpleUploadedFile."""
    img_f = io.BytesIO()
    img = Image.new(mode="RGB", size=(1, 1), color="red")
    img.save(img_f, format="jpeg")
    return SimpleUploadedFile(fname, img_f.getvalue())


def test_create(mutation):
    result = mutation(
        '{ fruit: createFruit(data: { name: "strawberry" }) { id name } }',
    )
    assert not result.errors
    assert result.data["fruit"] == {"id": "1", "name": "strawberry"}
    assert list(models.Fruit.objects.values("id", "name")) == [
        {"id": 1, "name": "strawberry"},
    ]


def test_create_with_optional_file(mutation):
    fname = "test_create_with_optional_file.png"
    upload = prep_image(fname)
    result = mutation(
        """\
        CreateFruit($picture: Upload!) {
          createFruit(data: { name: "strawberry", picture: $picture }) {
            id
            name
            picture {
              name
            }
          }
        }
        """,
        variable_values={"picture": upload},
    )

    assert not result.errors
    assert result.data["createFruit"] == {
        "id": "1",
        "name": "strawberry",
        "picture": {"name": f".tmp_upload/{fname}"},
    }


def test_create_with_optional_file_when_not_setting_it(mutation):
    result = mutation(
        """\
        CreateFruit($picture: Upload) {
          createFruit(data: { name: "strawberry", picture: $picture }) {
            id
            name
            picture {
              name
            }
          }
        }
        """,
        variable_values={"picture": None},
    )

    assert not result.errors
    assert result.data["createFruit"] == {
        "id": "1",
        "name": "strawberry",
        "picture": None,
    }


def test_update_with_optional_file_when_unsetting_it(mutation):
    fname = "test_update_with_optional_file.png"
    upload = prep_image(fname)
    fruit = models.Fruit.objects.create(name="strawberry", picture=upload)

    result = mutation(
        """\
        UpdateFruit($id: ID! $picture: Upload) {
          updateFruits(
            filters: { id: { exact: $id } }
            data: { picture: $picture }
          ) {
            id
            name
            picture {
              name
            }
          }
        }
        """,
        variable_values={"id": fruit.pk, "picture": None},
    )

    assert not result.errors
    assert result.data["updateFruits"] == [
        {
            "id": str(fruit.pk),
            "name": "strawberry",
            "picture": None,
        }
    ]


def test_with_required_file_fails(mutation):
    # The query input will not have the required field listed
    # as we want to test the failback of the django-model full_clean
    # method on the create to trigger validation errors.
    result = mutation(
        """\
        createTomatoWithRequiredPicture {
          createTomatoWithRequiredPicture(data: {name: "strawberry"}) {
            id
            name
            picture {
              name
            }
          }
        }
        """,
        variable_values={},
    )

    assert result.errors is not None
    assert "'This field cannot be blank" in str(result.errors)


@pytest.mark.asyncio
@pytest.mark.django_db(transaction=True)
async def test_create_async(mutation):
    result = await mutation(
        '{ fruit: createFruit(data: { name: "strawberry" }) { name } }',
    )
    assert not result.errors
    assert result.data["fruit"] == {"name": "strawberry"}


def test_create_many(mutation):
    result = mutation(
        '{ fruits: createFruits(data: [{ name: "strawberry" },'
        ' { name: "raspberry" }]) { id name } }',
    )
    assert not result.errors
    assert result.data["fruits"] == [
        {"id": "1", "name": "strawberry"},
        {"id": "2", "name": "raspberry"},
    ]
    assert list(models.Fruit.objects.values("id", "name")) == [
        {"id": 1, "name": "strawberry"},
        {"id": 2, "name": "raspberry"},
    ]


def test_update(mutation, fruits):
    result = mutation(
        '{ fruits: updateFruits(data: { name: "orange" }, filters: {}) { id name } }'
    )
    assert not result.errors
    assert result.data["fruits"] == [
        {"id": "1", "name": "orange"},
        {"id": "2", "name": "orange"},
        {"id": "3", "name": "orange"},
    ]
    assert list(models.Fruit.objects.values("id", "name")) == [
        {"id": 1, "name": "orange"},
        {"id": 2, "name": "orange"},
        {"id": 3, "name": "orange"},
    ]


def test_update_m2m_with_validation_error(mutation, fruit):
    result = mutation(
        '{ fruits: updateFruits(data: { types: [{ name: "rotten"} ] }, filters: {}) { id types {'
        " name } }}",
    )
    assert result.errors
    assert result.errors[0].message == "{'name': ['We do not allow rotten fruits.']}"


def test_update_m2m_with_new_different_objects(mutation, fruit):
    result = mutation(
        '{ fruits: updateFruits(data: { types: [{name: "apple"}, {name: "strawberry"}]}, filters: {}) { id types { id name }}}'
    )
    assert not result.errors
    assert result.data["fruits"][0]["types"] == [
        {"id": "1", "name": "apple"},
        {"id": "2", "name": "strawberry"},
    ]

    result = mutation(
        '{ fruits: updateFruits(data: { types: [{id: "1", name: "apple updated"}, {name: "raspberry"}]}, filters: {}) { id types { id name }}}'
    )

    assert result.data["fruits"][0]["types"] == [
        {"id": "1", "name": "apple updated"},
        {"id": "3", "name": "raspberry"},
    ]


def test_update_m2m_with_duplicates(mutation, fruit):
    result = mutation(
        '{ fruits: updateFruits(data: { types: [{name: "apple"}, {name: "apple"}]}, filters: {}) { id types { id name }}}'
    )
    assert not result.errors
    assert result.data["fruits"][0]["types"] == [
        {"id": "1", "name": "apple"},
        {"id": "2", "name": "apple"},
    ]


def test_update_lazy_object(mutation, fruit):
    result = mutation(
        '{ fruit: updateLazyFruit(data: { name: "orange" }) { id name } }',
    )
    assert not result.errors
    assert result.data["fruit"] == {"id": "1", "name": "orange"}


def test_update_with_filters(mutation, fruits):
    result = mutation(
        '{ fruits: updateFruits(data: { name: "orange" },'
        " filters: { id: { inList: [1, 2] } } ) { id name } }",
    )
    assert not result.errors
    assert result.data["fruits"] == [
        {"id": "1", "name": "orange"},
        {"id": "2", "name": "orange"},
    ]
    assert list(models.Fruit.objects.values("id", "name")) == [
        {"id": 1, "name": "orange"},
        {"id": 2, "name": "orange"},
        {"id": 3, "name": "banana"},
    ]


def test_delete(mutation, fruits):
    result = mutation("{ fruits: deleteFruits(filters: {}) { id name } }")
    assert not result.errors
    assert result.data["fruits"] == [
        {"id": "1", "name": "strawberry"},
        {"id": "2", "name": "raspberry"},
        {"id": "3", "name": "banana"},
    ]
    assert list(models.Fruit.objects.values("id", "name")) == []


def test_delete_lazy_object(mutation, fruit):
    result = mutation("{ fruit: deleteLazyFruit { id name } }")
    assert not result.errors
    assert result.data["fruit"] == {"id": "1", "name": "Strawberry"}
    assert list(models.Fruit.objects.values("id", "name")) == []


def test_delete_with_filters(mutation, fruits):
    result = mutation(
        '{ fruits: deleteFruits(filters: { name: { contains: "berry" } }) { id'
        " name } }",
    )
    assert not result.errors
    assert result.data["fruits"] == [
        {"id": "1", "name": "strawberry"},
        {"id": "2", "name": "raspberry"},
    ]
    assert list(models.Fruit.objects.values("id", "name")) == [
        {"id": 3, "name": "banana"},
    ]


@pytest.mark.skipif(
    not settings.GEOS_IMPORTED,
    reason="Test requires GEOS to be imported and properly configured",
)
@pytest.mark.django_db(transaction=True)
def test_create_geo(mutation):
    from tests.models import GeosFieldsModel

    # Test for point
    point = (0.0, 1.0)
    result = mutation(
        f"{{ geofield: createGeoField(data: {{ point: {list(point)} }} ) {{ id }} }}",
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).point.tuple
        == point
    )

    # Test for lineString
    line_string = ((0.0, 0.0), (1.0, 1.0))
    result = mutation(
        f"""
        {{ geofield: createGeoField(data: {{ lineString:
            {deep_tuple_to_list(line_string)}
         }}) {{ id }} }}
        """,
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).line_string.tuple
        == line_string
    )

    # Test for polygon
    polygon = (
        ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)),
        ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)),
    )
    result = mutation(
        f"""
        {{ geofield: createGeoField(data: {{
            polygon: {deep_tuple_to_list(polygon)}
        }}) {{ id }} }}
        """,
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).polygon.tuple
        == polygon
    )

    # Test for multi_point
    multi_point = ((0.0, 0.0), (-1.0, -1.0), (1.0, 1.0))
    result = mutation(
        f"""
        {{ geofield: createGeoField(data: {{ multiPoint:
            {deep_tuple_to_list(multi_point)}
        }}) {{ id }} }}
        """,
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(id=result.data["geofield"]["id"]).multi_point.tuple
        == multi_point
    )

    # Test for multiLineString
    multi_line_string = (
        ((0.0, 0.0), (1.0, 1.0)),
        ((1.0, 1.0), (-1.0, -1.0)),
        ((2.0, 2.0), (-2.0, -2.0)),
    )
    result = mutation(
        f"""
        {{ geofield: createGeoField(data: {{ multiLineString:
            {deep_tuple_to_list(multi_line_string)}
        }}) {{ id }} }}
        """,
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(
            id=result.data["geofield"]["id"],
        ).multi_line_string.tuple
        == multi_line_string
    )

    # Test for multiPolygon
    multi_polygon = (
        (
            ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)),
            ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)),
        ),
        (
            ((-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)),
            ((-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0)),
        ),
    )
    result = mutation(
        f"""
        {{ geofield: createGeoField(data: {{ multiPolygon:
            {deep_tuple_to_list(multi_polygon)}
        }}) {{ id }} }}
        """,
    )
    assert not result.errors
    assert (
        GeosFieldsModel.objects.get(
            id=result.data["geofield"]["id"],
        ).multi_polygon.tuple
        == multi_polygon
    )


@pytest.mark.skipif(
    not settings.GEOS_IMPORTED,
    reason="Test requires GEOS to be imported and properly configured",
)
@pytest.mark.django_db(transaction=True)
def test_update_geo(mutation):
    from tests.models import GeosFieldsModel

    geofield_obj = GeosFieldsModel.objects.create()

    assert geofield_obj.point is None
    assert geofield_obj.line_string is None
    assert geofield_obj.polygon is None
    assert geofield_obj.multi_point is None
    assert geofield_obj.multi_line_string is None
    assert geofield_obj.multi_polygon is None

    # Test for point
    point = [0.0, 1.0]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            point: {point}
            }}) {{
                id
            }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.point is not None
    assert deep_tuple_to_list(geofield_obj.point.tuple) == point

    # Test for lineString
    line_string = [[0.0, 0.0], [1.0, 1.0]]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            lineString: {line_string}
            }}) {{
                id
            }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.line_string is not None
    assert deep_tuple_to_list(geofield_obj.line_string.tuple) == line_string

    # Test for polygon
    polygon = [
        [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]],
        [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]],
    ]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            polygon: {polygon}
            }}) {{
                id
        }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.polygon is not None
    assert deep_tuple_to_list(geofield_obj.polygon.tuple) == polygon

    # Test for multi_point
    multi_point = [[0.0, 0.0], [-1.0, -1.0], [1.0, 1.0]]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            multiPoint: {multi_point}
            }}) {{
                id
            }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.multi_point is not None
    assert deep_tuple_to_list(geofield_obj.multi_point.tuple) == multi_point

    # Test for multiLineString
    multi_line_string = [
        [[0.0, 0.0], [1.0, 1.0]],
        [[1.0, 1.0], [-1.0, -1.0]],
        [[2.0, 2.0], [-2.0, -2.0]],
    ]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            multiLineString: {multi_line_string}
            }}) {{
                id
            }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.multi_line_string is not None
    assert deep_tuple_to_list(geofield_obj.multi_line_string.tuple) == multi_line_string

    # Test for multiPolygon
    multi_polygon = [
        [
            [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]],
            [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]],
        ],
        [
            [[-1.0, -1.0], [-1.0, 1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0]],
            [[-2.0, -2.0], [-2.0, 2.0], [2.0, 2.0], [2.0, -2.0], [-2.0, -2.0]],
        ],
    ]
    result = mutation(
        f"""
        {{ geofield: updateGeoFields(data: {{
            multiPolygon: {multi_polygon}
            }}) {{
                id
            }} }}
        """,
    )
    assert not result.errors
    geofield_obj.refresh_from_db()
    assert geofield_obj.multi_polygon is not None
    assert deep_tuple_to_list(geofield_obj.multi_polygon.tuple) == multi_polygon

    # Test everything not overwritten
    geofield_obj.refresh_from_db()
    assert geofield_obj.point is not None
    assert geofield_obj.line_string is not None
    assert geofield_obj.polygon is not None
    assert geofield_obj.multi_point is not None
    assert geofield_obj.multi_line_string is not None
    assert geofield_obj.multi_polygon is not None
    assert deep_tuple_to_list(geofield_obj.point.tuple) == point
    assert deep_tuple_to_list(geofield_obj.line_string.tuple) == line_string
    assert deep_tuple_to_list(geofield_obj.polygon.tuple) == polygon
    assert deep_tuple_to_list(geofield_obj.multi_point.tuple) == multi_point
    assert deep_tuple_to_list(geofield_obj.multi_line_string.tuple) == multi_line_string
    assert deep_tuple_to_list(geofield_obj.multi_polygon.tuple) == multi_polygon


def test_parse_input_unwraps_some():
    from enum import Enum
    from typing import Any

    from strawberry import Some

    from strawberry_django.mutations.resolvers import parse_input

    class Color(Enum):
        RED = "red"
        GREEN = "green"

    info: Any = None
    assert parse_input(info, Some("hello")) == "hello"
    assert parse_input(info, Some(None)) is None
    assert parse_input(info, Some(Some("nested"))) == "nested"
    assert parse_input(info, Some(Color.RED)) == "red"
    assert parse_input(info, [Some("a"), Some("b")]) == ["a", "b"]
    assert parse_input(info, {"key": Some("value")}) == {"key": "value"}


def test_create_with_fk_id_field(db):
    """FK _id fields (e.g. color_id) in input types should work with mutations.create()."""
    import strawberry

    import strawberry_django
    from strawberry_django import mutations

    @strawberry_django.input(models.Fruit)
    class FruitWithFkIdInput:
        name: strawberry.auto
        color_id: strawberry.auto

    @strawberry_django.type(models.Fruit)
    class FruitOutput:
        id: strawberry.auto
        name: strawberry.auto
        color_id: strawberry.auto

    @strawberry.type
    class Mutation:
        create_fruit: FruitOutput = mutations.create(FruitWithFkIdInput)

    @strawberry.type
    class Query:
        @strawberry.field
        def noop(self) -> str:
            return ""

    schema = strawberry.Schema(query=Query, mutation=Mutation)

    color = models.Color.objects.create(name="Red")

    result = schema.execute_sync(
        f'mutation {{ createFruit(data: {{ name: "strawberry", colorId: {color.pk} }}) {{ id name colorId }} }}',
    )
    assert not result.errors
    assert result.data == {
        "createFruit": {
            "id": str(models.Fruit.objects.get().pk),
            "name": "strawberry",
            "colorId": str(color.pk),
        },
    }
    fruit = models.Fruit.objects.get()
    assert fruit.color_id == color.pk


def test_update_with_fk_id_field(db):
    """FK _id fields should work with mutations.update()."""
    import strawberry

    import strawberry_django
    from strawberry_django import mutations

    @strawberry_django.input(models.Fruit)
    class FruitUpdateInput:
        id: strawberry.auto
        color_id: strawberry.auto

    @strawberry_django.type(models.Fruit)
    class FruitOutput:
        id: strawberry.auto
        name: strawberry.auto
        color_id: strawberry.auto

    @strawberry.type
    class Mutation:
        update_fruit: FruitOutput = mutations.update(FruitUpdateInput)

    @strawberry.type
    class Query:
        @strawberry.field
        def noop(self) -> str:
            return ""

    schema = strawberry.Schema(query=Query, mutation=Mutation)

    red = models.Color.objects.create(name="Red")
    blue = models.Color.objects.create(name="Blue")
    fruit = models.Fruit.objects.create(name="strawberry", color=red)

    result = schema.execute_sync(
        "mutation($data: FruitUpdateInput!) { updateFruit(data: $data) { id name colorId } }",
        variable_values={"data": {"id": fruit.pk, "colorId": blue.pk}},
    )
    assert not result.errors
    assert result.data == {
        "updateFruit": {
            "id": str(fruit.pk),
            "name": "strawberry",
            "colorId": str(blue.pk),
        },
    }
    fruit.refresh_from_db()
    assert fruit.color_id == blue.pk
