from __future__ import annotations

import json
import re

import pytest
from django import forms
from django.core import exceptions, serializers
from django.db import models
from django.db.migrations.writer import MigrationWriter
from django.db.models import Q
from django.test import SimpleTestCase, TestCase
from django.test.utils import isolate_apps

from django_mysql.forms import SimpleListField
from django_mysql.models import ListTextField
from tests.testapp.models import BigCharListModel, BigIntListModel


class TestSaveLoad(TestCase):
    def test_char_easy(self):
        s = BigCharListModel.objects.create(field=["comfy", "big"])
        assert s.field == ["comfy", "big"]
        s = BigCharListModel.objects.get(id=s.id)
        assert s.field == ["comfy", "big"]

        s.field.append("round")
        s.save()
        assert s.field == ["comfy", "big", "round"]
        s = BigCharListModel.objects.get(id=s.id)
        assert s.field == ["comfy", "big", "round"]

    def test_char_string_direct(self):
        s = BigCharListModel.objects.create(field="big,bad")
        s = BigCharListModel.objects.get(id=s.id)
        assert s.field == ["big", "bad"]

    def test_is_a_list_immediately(self):
        s = BigCharListModel()
        assert s.field == []
        s.field.append("bold")
        s.field.append("brave")
        s.save()
        assert s.field == ["bold", "brave"]
        s = BigCharListModel.objects.get(id=s.id)
        assert s.field == ["bold", "brave"]

    def test_empty(self):
        s = BigCharListModel.objects.create()
        assert s.field == []
        s = BigCharListModel.objects.get(id=s.id)
        assert s.field == []

    def test_char_cant_create_lists_with_empty_string(self):
        with pytest.raises(ValueError):
            BigCharListModel.objects.create(field=[""])

    def test_char_cant_create_sets_with_commas(self):
        with pytest.raises(ValueError):
            BigCharListModel.objects.create(field=["co,mma", "contained"])

    def test_char_basic_lookup(self):
        mymodel = BigCharListModel.objects.create()
        empty = BigCharListModel.objects.filter(field="")

        assert empty.count() == 1
        assert empty[0] == mymodel

        mymodel.delete()

        assert empty.count() == 0

    def test_char_lookup_contains(self):
        self.check_char_lookup("contains")

    def test_char_lookup_icontains(self):
        self.check_char_lookup("icontains")

    def check_char_lookup(self, lookup):
        lname = "field__" + lookup
        mymodel = BigCharListModel.objects.create(field=["mouldy", "rotten"])

        mouldy = BigCharListModel.objects.filter(**{lname: "mouldy"})
        assert mouldy.count() == 1
        assert mouldy[0] == mymodel

        rotten = BigCharListModel.objects.filter(**{lname: "rotten"})
        assert rotten.count() == 1
        assert rotten[0] == mymodel

        clean = BigCharListModel.objects.filter(**{lname: "clean"})
        assert clean.count() == 0

        with pytest.raises(ValueError):
            list(BigCharListModel.objects.filter(**{lname: ["a", "b"]}))

        both = BigCharListModel.objects.filter(
            Q(**{lname: "mouldy"}) & Q(**{lname: "rotten"})
        )
        assert both.count() == 1
        assert both[0] == mymodel

        either = BigCharListModel.objects.filter(
            Q(**{lname: "mouldy"}) | Q(**{lname: "clean"})
        )
        assert either.count() == 1

        not_clean = BigCharListModel.objects.exclude(**{lname: "clean"})
        assert not_clean.count() == 1

        not_mouldy = BigCharListModel.objects.exclude(**{lname: "mouldy"})
        assert not_mouldy.count() == 0

    def test_char_len_lookup_empty(self):
        mymodel = BigCharListModel.objects.create(field=[])

        empty = BigCharListModel.objects.filter(field__len=0)
        assert empty.count() == 1
        assert empty[0] == mymodel

        one = BigCharListModel.objects.filter(field__len=1)
        assert one.count() == 0

        one_or_more = BigCharListModel.objects.filter(field__len__gte=0)
        assert one_or_more.count() == 1

    def test_char_len_lookup(self):
        mymodel = BigCharListModel.objects.create(field=["red", "expensive"])

        empty = BigCharListModel.objects.filter(field__len=0)
        assert empty.count() == 0

        one_or_more = BigCharListModel.objects.filter(field__len__gte=1)
        assert one_or_more.count() == 1
        assert one_or_more[0] == mymodel

        two = BigCharListModel.objects.filter(field__len=2)
        assert two.count() == 1
        assert two[0] == mymodel

        three = BigCharListModel.objects.filter(field__len=3)
        assert three.count() == 0

    def test_char_position_lookup(self):
        mymodel = BigCharListModel.objects.create(field=["red", "blue"])

        blue0 = BigCharListModel.objects.filter(field__0="blue")
        assert blue0.count() == 0

        red0 = BigCharListModel.objects.filter(field__0="red")
        assert list(red0) == [mymodel]

        red0_red1 = BigCharListModel.objects.filter(field__0="red", field__1="red")
        assert red0_red1.count() == 0

        red0_blue1 = BigCharListModel.objects.filter(field__0="red", field__1="blue")
        assert list(red0_blue1) == [mymodel]

        red0_or_blue0 = BigCharListModel.objects.filter(
            Q(field__0="red") | Q(field__0="blue")
        )
        assert list(red0_or_blue0) == [mymodel]

    def test_int_easy(self):
        mymodel = BigIntListModel.objects.create(field=[1, 2])
        assert mymodel.field == [1, 2]
        mymodel = BigIntListModel.objects.get(id=mymodel.id)
        assert mymodel.field == [1, 2]

    def test_int_contains_lookup(self):
        onetwo = BigIntListModel.objects.create(field=[1, 2])

        ones = BigIntListModel.objects.filter(field__contains=1)
        assert ones.count() == 1
        assert ones[0] == onetwo

        twos = BigIntListModel.objects.filter(field__contains=2)
        assert twos.count() == 1
        assert twos[0] == onetwo

        threes = BigIntListModel.objects.filter(field__contains=3)
        assert threes.count() == 0

        with pytest.raises(ValueError):
            list(BigIntListModel.objects.filter(field__contains=[1, 2]))

        ones_and_twos = BigIntListModel.objects.filter(
            Q(field__contains=1) & Q(field__contains=2)
        )
        assert ones_and_twos.count() == 1
        assert ones_and_twos[0] == onetwo

        ones_and_threes = BigIntListModel.objects.filter(
            Q(field__contains=1) & Q(field__contains=3)
        )
        assert ones_and_threes.count() == 0

        ones_or_threes = BigIntListModel.objects.filter(
            Q(field__contains=1) | Q(field__contains=3)
        )
        assert ones_or_threes.count() == 1

        no_three = BigIntListModel.objects.exclude(field__contains=3)
        assert no_three.count() == 1

        no_one = BigIntListModel.objects.exclude(field__contains=1)
        assert no_one.count() == 0

    def test_int_position_lookup(self):
        onetwo = BigIntListModel.objects.create(field=[1, 2])

        one0 = BigIntListModel.objects.filter(field__0=1)
        assert list(one0) == [onetwo]

        two0 = BigIntListModel.objects.filter(field__0=2)
        assert two0.count() == 0

        one0two1 = BigIntListModel.objects.filter(field__0=1, field__1=2)
        assert list(one0two1) == [onetwo]


class TestValidation(SimpleTestCase):
    def test_max_length(self):
        field = ListTextField(models.CharField(max_length=32), size=3, max_length=32)

        field.clean({"a", "b", "c"}, None)

        with pytest.raises(exceptions.ValidationError) as excinfo:
            field.clean({"a", "b", "c", "d"}, None)
        assert (
            excinfo.value.messages[0]
            == "List contains 4 items, it should contain no more than 3."
        )


@isolate_apps("tests.testapp")
class TestCheck(SimpleTestCase):
    def test_field_checks(self):
        class Invalid(models.Model):
            field = ListTextField(models.CharField(), max_length=32)

        errors = Invalid.check()
        assert len(errors) == 1
        assert errors[0].id == "django_mysql.E004"
        assert "Base field for list has errors" in errors[0].msg
        assert "max_length" in errors[0].msg

    def test_invalid_base_fields(self):
        class Invalid(models.Model):
            field = ListTextField(
                models.ForeignKey("testapp.Author", on_delete=models.CASCADE),
                max_length=32,
            )

        errors = Invalid.check()
        assert len(errors) == 1
        assert errors[0].id == "django_mysql.E005"
        assert "Base field for list must be" in errors[0].msg


class ListTextFieldSubclass(ListTextField):
    """
    Used below, has a different path for deconstruct()
    """


class TestDeconstruct(TestCase):
    def test_deconstruct(self):
        field = ListTextField(models.IntegerField(), max_length=32)
        name, path, args, kwargs = field.deconstruct()
        new = ListTextField(*args, **kwargs)
        assert new.base_field.__class__ == field.base_field.__class__

    def test_deconstruct_with_size(self):
        field = ListTextField(models.IntegerField(), size=3, max_length=32)
        name, path, args, kwargs = field.deconstruct()
        new = ListTextField(*args, **kwargs)
        assert new.size == field.size

    def test_deconstruct_args(self):
        field = ListTextField(models.CharField(max_length=5), max_length=32)
        name, path, args, kwargs = field.deconstruct()
        new = ListTextField(*args, **kwargs)
        assert new.base_field.max_length == field.base_field.max_length

    def test_bad_import_deconstruct(self):
        from django_mysql.models.fields import ListTextField as LTField

        field = LTField(models.IntegerField())
        name, path, args, kwargs = field.deconstruct()
        assert path == "django_mysql.models.ListTextField"

    def test_bad_import2_deconstruct(self):
        from django_mysql.models.fields.lists import ListTextField as LTField

        field = LTField(models.IntegerField())
        name, path, args, kwargs = field.deconstruct()
        assert path == "django_mysql.models.ListTextField"

    def test_subclass_deconstruct(self):
        field = ListTextFieldSubclass(models.IntegerField())
        name, path, args, kwargs = field.deconstruct()
        assert path == "tests.testapp.test_listtextfield.ListTextFieldSubclass"


class TestMigrationWriter(TestCase):
    def test_makemigrations(self):
        field = ListTextField(models.CharField(max_length=5), max_length=32)
        statement, imports = MigrationWriter.serialize(field)

        # The order of the output max_length/size statements varies by
        # python version, hence a little regexp to match them
        assert re.compile(
            r"""^django_mysql\.models\.ListTextField\(
                models\.CharField\(max_length=5\),\ # space here
                (
                    max_length=32,\ size=None|
                    size=None,\ max_length=32
                )
                \)$
            """,
            re.VERBOSE,
        ).match(statement)

    def test_makemigrations_with_size(self):
        field = ListTextField(models.CharField(max_length=5), max_length=32, size=5)
        statement, imports = MigrationWriter.serialize(field)

        # The order of the output max_length/size statements varies by
        # python version, hence a little regexp to match them
        assert re.compile(
            r"""^django_mysql\.models\.ListTextField\(
                models\.CharField\(max_length=5\),\ # space here
                (
                    max_length=32,\ size=5|
                    size=5,\ max_length=32
                )
                \)$
            """,
            re.VERBOSE,
        ).match(statement)


class TestSerialization(SimpleTestCase):
    def test_dumping(self):
        instance = BigCharListModel(field=["big", "comfy"])
        data = json.loads(serializers.serialize("json", [instance]))[0]
        field = data["fields"]["field"]
        assert sorted(field.split(",")) == ["big", "comfy"]

    def test_loading(self):
        test_data = """
            [{"fields": {"field": "big,leather,comfy"},
             "model": "testapp.BigCharListModel", "pk": null}]
        """
        objs = list(serializers.deserialize("json", test_data))
        instance = objs[0].object
        assert instance.field == ["big", "leather", "comfy"]

    def test_dumping_loading_empty(self):
        instance = BigCharListModel(field=[])
        data = serializers.serialize("json", [instance])
        objs = list(serializers.deserialize("json", data))
        instance = objs[0].object
        assert instance.field == []


class TestDescription(SimpleTestCase):
    def test_char(self):
        field = ListTextField(models.CharField(max_length=5), max_length=32)
        assert field.description == "List of String (up to %(max_length)s)"

    def test_int(self):
        field = ListTextField(models.IntegerField(), max_length=32)
        assert field.description == "List of Integer"


class TestFormField(SimpleTestCase):
    def test_model_field_formfield(self):
        model_field = ListTextField(models.CharField(max_length=27))
        form_field = model_field.formfield()
        assert isinstance(form_field, SimpleListField)
        assert isinstance(form_field.base_field, forms.CharField)
        assert form_field.base_field.max_length == 27

    def test_model_field_formfield_size(self):
        model_field = ListTextField(models.IntegerField(), size=4)
        form_field = model_field.formfield()
        assert isinstance(form_field, SimpleListField)
        assert form_field.max_length == 4
