import datetime
import os
import tempfile
import uuid

from django.core import validators
from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage
from django.db import models

temp_storage_dir = tempfile.mkdtemp()
temp_storage = FileSystemStorage(temp_storage_dir)


class Person(models.Model):
    name = models.CharField(max_length=100)


class Category(models.Model):
    name = models.CharField(max_length=20)
    slug = models.SlugField(max_length=20)
    url = models.CharField("The URL", max_length=40)

    class Meta:
        ordering = ("pk",)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.__str__()


class WriterManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(archived=False)


class Writer(models.Model):
    name = models.CharField(max_length=50, help_text="Use both first and last names.")
    archived = models.BooleanField(default=False, editable=False)

    objects = WriterManager()

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class Article(models.Model):
    ARTICLE_STATUS = (
        (1, "Draft"),
        (2, "Pending"),
        (3, "Live"),
    )
    headline = models.CharField(max_length=50)
    slug = models.SlugField()
    pub_date = models.DateField()
    created = models.DateField(editable=False)
    writer = models.ForeignKey(Writer, models.CASCADE)
    article = models.TextField()
    categories = models.ManyToManyField(Category, blank=True)
    status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True)

    def save(self, *args, **kwargs):
        if not self.id:
            self.created = datetime.date.today()
        return super().save(*args, **kwargs)

    def __str__(self):
        return self.headline


class ImprovedArticle(models.Model):
    article = models.OneToOneField(Article, models.CASCADE)


class ImprovedArticleWithParentLink(models.Model):
    article = models.OneToOneField(Article, models.CASCADE, parent_link=True)


class BetterWriter(Writer):
    score = models.IntegerField()


class Publication(models.Model):
    title = models.CharField(max_length=30)
    date_published = models.DateField()

    def __str__(self):
        return self.title


def default_mode():
    return "di"


def default_category():
    return 3


class PublicationDefaults(models.Model):
    MODE_CHOICES = (("di", "direct"), ("de", "delayed"))
    CATEGORY_CHOICES = ((1, "Games"), (2, "Comics"), (3, "Novel"))
    title = models.CharField(max_length=30)
    date_published = models.DateField(default=datetime.date.today)
    datetime_published = models.DateTimeField(default=datetime.datetime(2000, 1, 1))
    mode = models.CharField(max_length=2, choices=MODE_CHOICES, default=default_mode)
    category = models.IntegerField(choices=CATEGORY_CHOICES, default=default_category)
    active = models.BooleanField(default=True)
    file = models.FileField(default="default.txt")


class Author(models.Model):
    publication = models.OneToOneField(
        Publication, models.SET_NULL, null=True, blank=True
    )
    full_name = models.CharField(max_length=255)


class Author1(models.Model):
    publication = models.OneToOneField(Publication, models.CASCADE, null=False)
    full_name = models.CharField(max_length=255)


class WriterProfile(models.Model):
    writer = models.OneToOneField(Writer, models.CASCADE, primary_key=True)
    age = models.PositiveIntegerField()

    def __str__(self):
        return "%s is %s" % (self.writer, self.age)


class Document(models.Model):
    myfile = models.FileField(storage=temp_storage, upload_to="unused", blank=True)


class TextFile(models.Model):
    description = models.CharField(max_length=20)
    file = models.FileField(storage=temp_storage, upload_to="tests", max_length=15)

    def __str__(self):
        return self.description


class CustomFileField(models.FileField):
    def save_form_data(self, instance, data):
        been_here = getattr(self, "been_saved", False)
        assert not been_here, "save_form_data called more than once"
        setattr(self, "been_saved", True)


class CustomFF(models.Model):
    f = CustomFileField(upload_to="unused", blank=True)


class FilePathModel(models.Model):
    path = models.FilePathField(
        path=os.path.dirname(__file__), match="models.py", blank=True
    )


try:
    from PIL import Image  # NOQA: detect if Pillow is installed

    test_images = True

    class ImageFile(models.Model):
        def custom_upload_path(self, filename):
            path = self.path or "tests"
            return "%s/%s" % (path, filename)

        description = models.CharField(max_length=20)

        # Deliberately put the image field *after* the width/height fields to
        # trigger the bug in #10404 with width/height not getting assigned.
        width = models.IntegerField(editable=False)
        height = models.IntegerField(editable=False)
        image = models.ImageField(
            storage=temp_storage,
            upload_to=custom_upload_path,
            width_field="width",
            height_field="height",
        )
        path = models.CharField(max_length=16, blank=True, default="")

        def __str__(self):
            return self.description

    class OptionalImageFile(models.Model):
        def custom_upload_path(self, filename):
            path = self.path or "tests"
            return "%s/%s" % (path, filename)

        description = models.CharField(max_length=20)
        image = models.ImageField(
            storage=temp_storage,
            upload_to=custom_upload_path,
            width_field="width",
            height_field="height",
            blank=True,
            null=True,
        )
        width = models.IntegerField(editable=False, null=True)
        height = models.IntegerField(editable=False, null=True)
        path = models.CharField(max_length=16, blank=True, default="")

        def __str__(self):
            return self.description

    class NoExtensionImageFile(models.Model):
        def upload_to(self, filename):
            return "tests/no_extension"

        description = models.CharField(max_length=20)
        image = models.ImageField(storage=temp_storage, upload_to=upload_to)

        def __str__(self):
            return self.description

except ImportError:
    test_images = False


class Homepage(models.Model):
    url = models.URLField()


class Product(models.Model):
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.slug


class Price(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField()

    class Meta:
        unique_together = (("price", "quantity"),)

    def __str__(self):
        return "%s for %s" % (self.quantity, self.price)


class Triple(models.Model):
    left = models.IntegerField()
    middle = models.IntegerField()
    right = models.IntegerField()

    class Meta:
        unique_together = (("left", "middle"), ("middle", "right"))


class ArticleStatus(models.Model):
    ARTICLE_STATUS_CHAR = (
        ("d", "Draft"),
        ("p", "Pending"),
        ("l", "Live"),
    )
    status = models.CharField(
        max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True
    )


class Inventory(models.Model):
    barcode = models.PositiveIntegerField(unique=True)
    parent = models.ForeignKey(
        "self", models.SET_NULL, to_field="barcode", blank=True, null=True
    )
    name = models.CharField(blank=False, max_length=20)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.__str__()


class Book(models.Model):
    title = models.CharField(max_length=40)
    author = models.ForeignKey(Writer, models.SET_NULL, blank=True, null=True)
    special_id = models.IntegerField(blank=True, null=True, unique=True)

    class Meta:
        unique_together = ("title", "author")


class BookXtra(models.Model):
    isbn = models.CharField(max_length=16, unique=True)
    suffix1 = models.IntegerField(blank=True, default=0)
    suffix2 = models.IntegerField(blank=True, default=0)

    class Meta:
        unique_together = ("suffix1", "suffix2")
        abstract = True


class DerivedBook(Book, BookXtra):
    pass


class ExplicitPK(models.Model):
    key = models.CharField(max_length=20, primary_key=True)
    desc = models.CharField(max_length=20, blank=True, unique=True)

    class Meta:
        unique_together = ("key", "desc")

    def __str__(self):
        return self.key


class Post(models.Model):
    title = models.CharField(max_length=50, unique_for_date="posted", blank=True)
    slug = models.CharField(max_length=50, unique_for_year="posted", blank=True)
    subtitle = models.CharField(max_length=50, unique_for_month="posted", blank=True)
    posted = models.DateField()

    def __str__(self):
        return self.title


class DateTimePost(models.Model):
    title = models.CharField(max_length=50, unique_for_date="posted", blank=True)
    slug = models.CharField(max_length=50, unique_for_year="posted", blank=True)
    subtitle = models.CharField(max_length=50, unique_for_month="posted", blank=True)
    posted = models.DateTimeField(editable=False)

    def __str__(self):
        return self.title


class DerivedPost(Post):
    pass


class BigInt(models.Model):
    biggie = models.BigIntegerField()

    def __str__(self):
        return str(self.biggie)


class MarkupField(models.CharField):
    def __init__(self, *args, **kwargs):
        kwargs["max_length"] = 20
        super().__init__(*args, **kwargs)

    def formfield(self, **kwargs):
        # don't allow this field to be used in form (real use-case might be
        # that you know the markup will always be X, but it is among an app
        # that allows the user to say it could be something else)
        # regressed at r10062
        return None


class CustomFieldForExclusionModel(models.Model):
    name = models.CharField(max_length=10)
    markup = MarkupField()


class FlexibleDatePost(models.Model):
    title = models.CharField(max_length=50, unique_for_date="posted", blank=True)
    slug = models.CharField(max_length=50, unique_for_year="posted", blank=True)
    subtitle = models.CharField(max_length=50, unique_for_month="posted", blank=True)
    posted = models.DateField(blank=True, null=True)


class Color(models.Model):
    name = models.CharField(max_length=50)

    def __iter__(self):
        yield from range(5)

    def __str__(self):
        return self.name


class ColorfulItem(models.Model):
    name = models.CharField(max_length=50)
    colors = models.ManyToManyField(Color)


class CustomErrorMessage(models.Model):
    name1 = models.CharField(
        max_length=50,
        validators=[validators.validate_slug],
        error_messages={"invalid": "Model custom error message."},
    )
    name2 = models.CharField(
        max_length=50,
        validators=[validators.validate_slug],
        error_messages={"invalid": "Model custom error message."},
    )

    def clean(self):
        if self.name1 == "FORBIDDEN_VALUE":
            raise ValidationError(
                {"name1": [ValidationError("Model.clean() error messages.")]}
            )
        elif self.name1 == "FORBIDDEN_VALUE2":
            raise ValidationError(
                {"name1": "Model.clean() error messages (simpler syntax)."}
            )
        elif self.name1 == "GLOBAL_ERROR":
            raise ValidationError("Global error message.")


def today_callable_dict():
    return {"last_action__gte": datetime.datetime.today()}


def today_callable_q():
    return models.Q(last_action__gte=datetime.datetime.today())


class Character(models.Model):
    username = models.CharField(max_length=100)
    last_action = models.DateTimeField()

    def __str__(self):
        return self.username


class StumpJoke(models.Model):
    most_recently_fooled = models.ForeignKey(
        Character,
        models.CASCADE,
        limit_choices_to=today_callable_dict,
        related_name="jokes",
    )
    has_fooled_today = models.ManyToManyField(
        Character,
        limit_choices_to=today_callable_q,
        related_name="jokes_today",
    )
    funny = models.BooleanField(default=False)


# Model for #13776
class Student(models.Model):
    character = models.ForeignKey(Character, models.CASCADE)
    study = models.CharField(max_length=30)


# Model for #639
class Photo(models.Model):
    title = models.CharField(max_length=30)
    image = models.FileField(storage=temp_storage, upload_to="tests")

    # Support code for the tests; this keeps track of how many times save()
    # gets called on each instance.
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._savecount = 0

    def save(self, force_insert=False, force_update=False):
        super().save(force_insert=force_insert, force_update=force_update)
        self._savecount += 1


class UUIDPK(models.Model):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=30)


# Models for #24706
class StrictAssignmentFieldSpecific(models.Model):
    title = models.CharField(max_length=30)
    _should_error = False

    def __setattr__(self, key, value):
        if self._should_error is True:
            raise ValidationError(message={key: "Cannot set attribute"}, code="invalid")
        super().__setattr__(key, value)


class StrictAssignmentAll(models.Model):
    title = models.CharField(max_length=30)
    _should_error = False

    def __setattr__(self, key, value):
        if self._should_error is True:
            raise ValidationError(message="Cannot set attribute", code="invalid")
        super().__setattr__(key, value)


# A model with ForeignKey(blank=False, null=True)
class Award(models.Model):
    name = models.CharField(max_length=30)
    character = models.ForeignKey(Character, models.SET_NULL, blank=False, null=True)


class NullableUniqueCharFieldModel(models.Model):
    codename = models.CharField(max_length=50, blank=True, null=True, unique=True)
    email = models.EmailField(blank=True, null=True)
    slug = models.SlugField(blank=True, null=True)
    url = models.URLField(blank=True, null=True)


class Number(models.Model):
    value = models.IntegerField()


class NumbersToDice(models.Model):
    number = models.ForeignKey("Number", on_delete=models.CASCADE)
    die = models.ForeignKey("Dice", on_delete=models.CASCADE)


class Dice(models.Model):
    numbers = models.ManyToManyField(
        Number,
        through=NumbersToDice,
        limit_choices_to=models.Q(value__gte=1),
    )


class ConstraintsModel(models.Model):
    name = models.CharField(max_length=100)
    category = models.CharField(max_length=50, default="uncategorized")
    price = models.DecimalField(max_digits=10, decimal_places=2, default=0)

    class Meta:
        required_db_features = {"supports_table_check_constraints"}
        constraints = [
            models.UniqueConstraint(
                "name",
                "category",
                name="unique_name_category",
                violation_error_message="This product already exists.",
            ),
            models.CheckConstraint(
                condition=models.Q(price__gt=0),
                name="price_gte_zero",
                violation_error_message="Price must be greater than zero.",
            ),
        ]


class AttnameConstraintsModel(models.Model):
    left = models.ForeignKey(
        "self", related_name="+", null=True, on_delete=models.SET_NULL
    )
    right = models.ForeignKey(
        "self", related_name="+", null=True, on_delete=models.SET_NULL
    )

    class Meta:
        required_db_features = {"supports_table_check_constraints"}
        constraints = [
            models.CheckConstraint(
                name="%(app_label)s_%(class)s_left_not_right",
                # right_id here is the ForeignKey's attname, not name.
                condition=~models.Q(left=models.F("right_id")),
            ),
        ]
