from django.db import migrations, models
from django.db.migrations import operations
from django.db.migrations.optimizer import MigrationOptimizer
from django.db.models.functions import Abs

from .models import EmptyManager, UnicodeModel
from .test_base import OptimizerTestBase


class OptimizerTests(OptimizerTestBase):
    """
    Tests the migration optimizer.
    """

    def test_none_app_label(self):
        optimizer = MigrationOptimizer()
        with self.assertRaisesMessage(TypeError, "app_label must be a str"):
            optimizer.optimize([], None)

    def test_single(self):
        """
        The optimizer does nothing on a single operation,
        and that it does it in just one pass.
        """
        self.assertOptimizesTo(
            [migrations.DeleteModel("Foo")],
            [migrations.DeleteModel("Foo")],
            exact=1,
        )

    def test_create_delete_model(self):
        """
        CreateModel and DeleteModel should collapse into nothing.
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.DeleteModel("Foo"),
            ],
            [],
        )

    def test_create_rename_model(self):
        """
        CreateModel should absorb RenameModels.
        """
        managers = [("objects", EmptyManager())]
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[("name", models.CharField(max_length=255))],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
                migrations.RenameModel("Foo", "Bar"),
            ],
            [
                migrations.CreateModel(
                    "Bar",
                    [("name", models.CharField(max_length=255))],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                )
            ],
        )

    def test_rename_model_self(self):
        """
        RenameModels should absorb themselves.
        """
        self.assertOptimizesTo(
            [
                migrations.RenameModel("Foo", "Baa"),
                migrations.RenameModel("Baa", "Bar"),
            ],
            [
                migrations.RenameModel("Foo", "Bar"),
            ],
        )

    def test_create_alter_model_options(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel("Foo", fields=[]),
                migrations.AlterModelOptions(
                    name="Foo", options={"verbose_name_plural": "Foozes"}
                ),
            ],
            [
                migrations.CreateModel(
                    "Foo", fields=[], options={"verbose_name_plural": "Foozes"}
                ),
            ],
        )

    def test_create_alter_model_managers(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel("Foo", fields=[]),
                migrations.AlterModelManagers(
                    name="Foo",
                    managers=[
                        ("objects", models.Manager()),
                        ("things", models.Manager()),
                    ],
                ),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    fields=[],
                    managers=[
                        ("objects", models.Manager()),
                        ("things", models.Manager()),
                    ],
                ),
            ],
        )

    def test_create_alter_model_table(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel("Foo", fields=[]),
                migrations.AlterModelTable(
                    name="foo",
                    table="foo",
                ),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    fields=[],
                    options={
                        "db_table": "foo",
                    },
                ),
            ],
        )

    def test_create_alter_model_table_comment(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel("Foo", fields=[]),
                migrations.AlterModelTableComment(
                    name="foo",
                    table_comment="A lovely table.",
                ),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    fields=[],
                    options={
                        "db_table_comment": "A lovely table.",
                    },
                ),
            ],
        )

    def test_create_model_and_remove_model_options(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "MyModel",
                    fields=[],
                    options={"verbose_name": "My Model"},
                ),
                migrations.AlterModelOptions("MyModel", options={}),
            ],
            [migrations.CreateModel("MyModel", fields=[])],
        )
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "MyModel",
                    fields=[],
                    options={
                        "verbose_name": "My Model",
                        "verbose_name_plural": "My Model plural",
                    },
                ),
                migrations.AlterModelOptions(
                    "MyModel",
                    options={"verbose_name": "My Model"},
                ),
            ],
            [
                migrations.CreateModel(
                    "MyModel",
                    fields=[],
                    options={"verbose_name": "My Model"},
                ),
            ],
        )

    def _test_create_alter_foo_delete_model(self, alter_foo):
        """
        CreateModel, AlterModelTable, AlterUniqueTogether/AlterIndexTogether/
        AlterOrderWithRespectTo, and DeleteModel should collapse into nothing.
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.AlterModelTable("Foo", "woohoo"),
                alter_foo,
                migrations.DeleteModel("Foo"),
            ],
            [],
        )

    def test_create_alter_unique_delete_model(self):
        self._test_create_alter_foo_delete_model(
            migrations.AlterUniqueTogether("Foo", [["a", "b"]])
        )

    def test_create_alter_index_delete_model(self):
        self._test_create_alter_foo_delete_model(
            migrations.AlterIndexTogether("Foo", [["a", "b"]])
        )

    def test_create_alter_owrt_delete_model(self):
        self._test_create_alter_foo_delete_model(
            migrations.AlterOrderWithRespectTo("Foo", "a")
        )

    def _test_alter_alter(self, alter_foo, alter_bar):
        """
        Two AlterUniqueTogether/AlterIndexTogether/AlterOrderWithRespectTo
        /AlterField should collapse into the second.
        """
        self.assertOptimizesTo(
            [
                alter_foo,
                alter_bar,
            ],
            [
                alter_bar,
            ],
        )

    def test_alter_alter_table_model(self):
        self._test_alter_alter(
            migrations.AlterModelTable("Foo", "a"),
            migrations.AlterModelTable("Foo", "b"),
        )

    def test_alter_alter_unique_model(self):
        self._test_alter_alter(
            migrations.AlterUniqueTogether("Foo", [["a", "b"]]),
            migrations.AlterUniqueTogether("Foo", [["a", "c"]]),
        )

    def test_alter_alter_index_model(self):
        self._test_alter_alter(
            migrations.AlterIndexTogether("Foo", [["a", "b"]]),
            migrations.AlterIndexTogether("Foo", [["a", "c"]]),
        )

    def test_alter_alter_owrt_model(self):
        self._test_alter_alter(
            migrations.AlterOrderWithRespectTo("Foo", "a"),
            migrations.AlterOrderWithRespectTo("Foo", "b"),
        )

    def test_alter_alter_field(self):
        self._test_alter_alter(
            migrations.AlterField("Foo", "name", models.IntegerField()),
            migrations.AlterField("Foo", "name", models.IntegerField(help_text="help")),
        )

    def test_optimize_through_create(self):
        """
        We should be able to optimize away create/delete through a create or
        delete of a different model, but only if the create operation does not
        mention the model at all.
        """
        # These should work
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
                migrations.DeleteModel("Foo"),
            ],
            [
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
            ],
        )
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
                migrations.DeleteModel("Bar"),
                migrations.DeleteModel("Foo"),
            ],
            [],
        )
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
                migrations.DeleteModel("Foo"),
                migrations.DeleteModel("Bar"),
            ],
            [],
        )
        # Operations should be optimized if the FK references a model from the
        # other app.
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]
                ),
                migrations.DeleteModel("Foo"),
            ],
            [
                migrations.CreateModel(
                    "Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]
                ),
            ],
            app_label="otherapp",
        )
        # But it shouldn't work if a FK references a model with the same
        # app_label.
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("other", models.ForeignKey("Foo", models.CASCADE))]
                ),
                migrations.DeleteModel("Foo"),
            ],
        )
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("other", models.ForeignKey("testapp.Foo", models.CASCADE))]
                ),
                migrations.DeleteModel("Foo"),
            ],
            app_label="testapp",
        )
        # This should not work - bases should block it
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("size", models.IntegerField())], bases=("Foo",)
                ),
                migrations.DeleteModel("Foo"),
            ],
        )
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)
                ),
                migrations.DeleteModel("Foo"),
            ],
            app_label="testapp",
        )
        # The same operations should be optimized if app_label and none of
        # bases belong to that app.
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)
                ),
                migrations.DeleteModel("Foo"),
            ],
            [
                migrations.CreateModel(
                    "Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)
                ),
            ],
            app_label="otherapp",
        )
        # But it shouldn't work if some of bases belongs to the specified app.
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar", [("size", models.IntegerField())], bases=("testapp.Foo",)
                ),
                migrations.DeleteModel("Foo"),
            ],
            app_label="testapp",
        )

        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Book", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Person", [("name", models.CharField(max_length=255))]
                ),
                migrations.AddField(
                    "book",
                    "author",
                    models.ForeignKey("test_app.Person", models.CASCADE),
                ),
                migrations.CreateModel(
                    "Review",
                    [("book", models.ForeignKey("test_app.Book", models.CASCADE))],
                ),
                migrations.CreateModel(
                    "Reviewer", [("name", models.CharField(max_length=255))]
                ),
                migrations.AddField(
                    "review",
                    "reviewer",
                    models.ForeignKey("test_app.Reviewer", models.CASCADE),
                ),
                migrations.RemoveField("book", "author"),
                migrations.DeleteModel("Person"),
            ],
            [
                migrations.CreateModel(
                    "Book", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Reviewer", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Review",
                    [
                        ("book", models.ForeignKey("test_app.Book", models.CASCADE)),
                        (
                            "reviewer",
                            models.ForeignKey("test_app.Reviewer", models.CASCADE),
                        ),
                    ],
                ),
            ],
            app_label="test_app",
        )

    def test_create_model_add_field(self):
        """
        AddField should optimize into CreateModel.
        """
        managers = [("objects", EmptyManager())]
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[("name", models.CharField(max_length=255))],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
                migrations.AddField("Foo", "age", models.IntegerField()),
            ],
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[
                        ("name", models.CharField(max_length=255)),
                        ("age", models.IntegerField()),
                    ],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
            ],
        )

    def test_create_model_reordering(self):
        """
        AddField optimizes into CreateModel if it's a FK to a model that's
        between them (and there's no FK in the other direction), by changing
        the order of the CreateModel operations.
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Link", [("url", models.TextField())]),
                migrations.AddField(
                    "Foo", "link", models.ForeignKey("migrations.Link", models.CASCADE)
                ),
            ],
            [
                migrations.CreateModel("Link", [("url", models.TextField())]),
                migrations.CreateModel(
                    "Foo",
                    [
                        ("name", models.CharField(max_length=255)),
                        ("link", models.ForeignKey("migrations.Link", models.CASCADE)),
                    ],
                ),
            ],
        )

    def test_create_model_reordering_circular_fk(self):
        """
        CreateModel reordering behavior doesn't result in an infinite loop if
        there are FKs in both directions.
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel("Bar", [("url", models.TextField())]),
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.AddField(
                    "Bar", "foo_fk", models.ForeignKey("migrations.Foo", models.CASCADE)
                ),
                migrations.AddField(
                    "Foo", "bar_fk", models.ForeignKey("migrations.Bar", models.CASCADE)
                ),
            ],
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "Bar",
                    [
                        ("url", models.TextField()),
                        ("foo_fk", models.ForeignKey("migrations.Foo", models.CASCADE)),
                    ],
                ),
                migrations.AddField(
                    "Foo", "bar_fk", models.ForeignKey("migrations.Bar", models.CASCADE)
                ),
            ],
        )

    def test_create_model_no_reordering_for_unrelated_fk(self):
        """
        CreateModel order remains unchanged if the later AddField operation
        isn't a FK between them.
        """
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Link", [("url", models.TextField())]),
                migrations.AddField(
                    "Other",
                    "link",
                    models.ForeignKey("migrations.Link", models.CASCADE),
                ),
            ],
        )

    def test_create_model_no_reordering_of_inherited_model(self):
        """
        A CreateModel that inherits from another isn't reordered to avoid
        moving it earlier than its parent CreateModel operation.
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Other", [("foo", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "ParentModel", [("bar", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "ChildModel",
                    [("baz", models.CharField(max_length=255))],
                    bases=("migrations.parentmodel",),
                ),
                migrations.AddField(
                    "Other",
                    "fk",
                    models.ForeignKey("migrations.ChildModel", models.CASCADE),
                ),
            ],
            [
                migrations.CreateModel(
                    "ParentModel", [("bar", models.CharField(max_length=255))]
                ),
                migrations.CreateModel(
                    "ChildModel",
                    [("baz", models.CharField(max_length=255))],
                    bases=("migrations.parentmodel",),
                ),
                migrations.CreateModel(
                    "Other",
                    [
                        ("foo", models.CharField(max_length=255)),
                        (
                            "fk",
                            models.ForeignKey("migrations.ChildModel", models.CASCADE),
                        ),
                    ],
                ),
            ],
        )

    def test_create_model_add_field_not_through_m2m_through(self):
        """
        AddField should NOT optimize into CreateModel if it's an M2M using a
        through that's created between them.
        """
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel("Employee", []),
                migrations.CreateModel("Employer", []),
                migrations.CreateModel(
                    "Employment",
                    [
                        (
                            "employee",
                            models.ForeignKey("migrations.Employee", models.CASCADE),
                        ),
                        (
                            "employment",
                            models.ForeignKey("migrations.Employer", models.CASCADE),
                        ),
                    ],
                ),
                migrations.AddField(
                    "Employer",
                    "employees",
                    models.ManyToManyField(
                        "migrations.Employee",
                        through="migrations.Employment",
                    ),
                ),
            ],
        )

    def test_create_model_alter_field(self):
        """
        AlterField should optimize into CreateModel.
        """
        managers = [("objects", EmptyManager())]
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[("name", models.CharField(max_length=255))],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
                migrations.AlterField("Foo", "name", models.IntegerField()),
            ],
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[
                        ("name", models.IntegerField()),
                    ],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
            ],
        )

    def test_create_model_rename_field(self):
        """
        RenameField should optimize into CreateModel.
        """
        managers = [("objects", EmptyManager())]
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[("name", models.CharField(max_length=255))],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
                migrations.RenameField("Foo", "name", "title"),
            ],
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[
                        ("title", models.CharField(max_length=255)),
                    ],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
            ],
        )

    def test_add_field_rename_field(self):
        """
        RenameField should optimize into AddField
        """
        self.assertOptimizesTo(
            [
                migrations.AddField("Foo", "name", models.CharField(max_length=255)),
                migrations.RenameField("Foo", "name", "title"),
            ],
            [
                migrations.AddField("Foo", "title", models.CharField(max_length=255)),
            ],
        )

    def test_alter_field_rename_field(self):
        """
        RenameField should optimize to the other side of AlterField,
        and into itself.
        """
        self.assertOptimizesTo(
            [
                migrations.AlterField("Foo", "name", models.CharField(max_length=255)),
                migrations.RenameField("Foo", "name", "title"),
                migrations.RenameField("Foo", "title", "nom"),
            ],
            [
                migrations.RenameField("Foo", "name", "nom"),
                migrations.AlterField("Foo", "nom", models.CharField(max_length=255)),
            ],
        )

    def test_swapping_fields_names(self):
        self.assertDoesNotOptimize(
            [
                migrations.CreateModel(
                    "MyModel",
                    [
                        ("field_a", models.IntegerField()),
                        ("field_b", models.IntegerField()),
                    ],
                ),
                migrations.RunPython(migrations.RunPython.noop),
                migrations.RenameField("MyModel", "field_a", "field_c"),
                migrations.RenameField("MyModel", "field_b", "field_a"),
                migrations.RenameField("MyModel", "field_c", "field_b"),
            ],
        )

    def test_create_model_remove_field(self):
        """
        RemoveField should optimize into CreateModel.
        """
        managers = [("objects", EmptyManager())]
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[
                        ("name", models.CharField(max_length=255)),
                        ("age", models.IntegerField()),
                    ],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
                migrations.RemoveField("Foo", "age"),
            ],
            [
                migrations.CreateModel(
                    name="Foo",
                    fields=[
                        ("name", models.CharField(max_length=255)),
                    ],
                    options={"verbose_name": "Foo"},
                    bases=(UnicodeModel,),
                    managers=managers,
                ),
            ],
        )

    def test_add_field_alter_field(self):
        """
        AlterField should optimize into AddField.
        """
        self.assertOptimizesTo(
            [
                migrations.AddField("Foo", "age", models.IntegerField()),
                migrations.AlterField("Foo", "age", models.FloatField(default=2.4)),
            ],
            [
                migrations.AddField(
                    "Foo", name="age", field=models.FloatField(default=2.4)
                ),
            ],
        )

    def test_add_field_delete_field(self):
        """
        RemoveField should cancel AddField
        """
        self.assertOptimizesTo(
            [
                migrations.AddField("Foo", "age", models.IntegerField()),
                migrations.RemoveField("Foo", "age"),
            ],
            [],
        )

    def test_alter_field_delete_field(self):
        """
        RemoveField should absorb AlterField
        """
        self.assertOptimizesTo(
            [
                migrations.AlterField("Foo", "age", models.IntegerField()),
                migrations.RemoveField("Foo", "age"),
            ],
            [
                migrations.RemoveField("Foo", "age"),
            ],
        )

    def _test_create_alter_foo_field(self, alter):
        """
        CreateModel, AlterFooTogether/AlterOrderWithRespectTo followed by an
        add/alter/rename field should optimize to CreateModel with options.
        """
        option_value = getattr(alter, alter.option_name)
        options = {alter.option_name: option_value}

        # AddField
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.AddField("Foo", "c", models.IntegerField()),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                    options=options,
                ),
            ],
        )

        # AlterField
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.AlterField("Foo", "b", models.CharField(max_length=255)),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.CharField(max_length=255)),
                    ],
                    options=options,
                ),
            ],
        )

        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.AlterField("Foo", "c", models.CharField(max_length=255)),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("c", models.CharField(max_length=255)),
                    ],
                    options=options,
                ),
            ],
        )

        # RenameField
        if isinstance(option_value, str):
            renamed_options = {alter.option_name: "c"}
        else:
            renamed_options = {
                alter.option_name: {
                    tuple("c" if value == "b" else value for value in item)
                    for item in option_value
                }
            }
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.RenameField("Foo", "b", "c"),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                    options=renamed_options,
                ),
            ],
        )

        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.RenameField("Foo", "b", "x"),
                migrations.RenameField("Foo", "x", "c"),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                    options=renamed_options,
                ),
            ],
        )

        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.RenameField("Foo", "c", "d"),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("d", models.IntegerField()),
                    ],
                    options=options,
                ),
            ],
        )

        # RemoveField
        if isinstance(option_value, str):
            removed_options = None
        else:
            removed_options = {
                alter.option_name: {
                    tuple(value for value in item if value != "b")
                    for item in option_value
                }
            }
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.RemoveField("Foo", "b"),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                    ],
                    options=removed_options,
                ),
            ],
        )

        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                        ("c", models.IntegerField()),
                    ],
                ),
                alter,
                migrations.RemoveField("Foo", "c"),
            ],
            [
                migrations.CreateModel(
                    "Foo",
                    [
                        ("a", models.IntegerField()),
                        ("b", models.IntegerField()),
                    ],
                    options=options,
                ),
            ],
        )

    def test_create_alter_unique_field(self):
        self._test_create_alter_foo_field(
            migrations.AlterUniqueTogether("Foo", [["a", "b"]])
        )

    def test_create_alter_index_field(self):
        self._test_create_alter_foo_field(
            migrations.AlterIndexTogether("Foo", [["a", "b"]])
        )

    def test_create_alter_owrt_field(self):
        self._test_create_alter_foo_field(
            migrations.AlterOrderWithRespectTo("Foo", "b")
        )

    def test_optimize_through_fields(self):
        """
        field-level through checking is working. This should manage to collapse
        model Foo to nonexistence, and model Bar to a single IntegerField
        called "width".
        """
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
                migrations.AddField("Foo", "age", models.IntegerField()),
                migrations.AddField("Bar", "width", models.IntegerField()),
                migrations.AlterField("Foo", "age", models.IntegerField()),
                migrations.RenameField("Bar", "size", "dimensions"),
                migrations.RemoveField("Foo", "age"),
                migrations.RenameModel("Foo", "Phou"),
                migrations.RemoveField("Bar", "dimensions"),
                migrations.RenameModel("Phou", "Fou"),
                migrations.DeleteModel("Fou"),
            ],
            [
                migrations.CreateModel("Bar", [("width", models.IntegerField())]),
            ],
        )

    def test_optimize_elidable_operation(self):
        elidable_operation = operations.base.Operation()
        elidable_operation.elidable = True
        self.assertOptimizesTo(
            [
                elidable_operation,
                migrations.CreateModel(
                    "Foo", [("name", models.CharField(max_length=255))]
                ),
                elidable_operation,
                migrations.CreateModel("Bar", [("size", models.IntegerField())]),
                elidable_operation,
                migrations.RenameModel("Foo", "Phou"),
                migrations.DeleteModel("Bar"),
                elidable_operation,
            ],
            [
                migrations.CreateModel(
                    "Phou", [("name", models.CharField(max_length=255))]
                ),
            ],
        )

    def test_rename_index(self):
        self.assertOptimizesTo(
            [
                migrations.RenameIndex(
                    "Pony", new_name="mid_name", old_fields=("weight", "pink")
                ),
                migrations.RenameIndex(
                    "Pony", new_name="new_name", old_name="mid_name"
                ),
            ],
            [
                migrations.RenameIndex(
                    "Pony", new_name="new_name", old_fields=("weight", "pink")
                ),
            ],
        )
        self.assertOptimizesTo(
            [
                migrations.RenameIndex(
                    "Pony", new_name="mid_name", old_name="old_name"
                ),
                migrations.RenameIndex(
                    "Pony", new_name="new_name", old_name="mid_name"
                ),
            ],
            [migrations.RenameIndex("Pony", new_name="new_name", old_name="old_name")],
        )
        self.assertDoesNotOptimize(
            [
                migrations.RenameIndex(
                    "Pony", new_name="mid_name", old_name="old_name"
                ),
                migrations.RenameIndex(
                    "Pony", new_name="new_name", old_fields=("weight", "pink")
                ),
            ]
        )

    def test_add_rename_index(self):
        tests = [
            models.Index(fields=["weight", "pink"], name="mid_name"),
            models.Index(Abs("weight"), name="mid_name"),
            models.Index(
                Abs("weight"), name="mid_name", condition=models.Q(weight__gt=0)
            ),
        ]
        for index in tests:
            with self.subTest(index=index):
                renamed_index = index.clone()
                renamed_index.name = "new_name"
                self.assertOptimizesTo(
                    [
                        migrations.AddIndex("Pony", index),
                        migrations.RenameIndex(
                            "Pony", new_name="new_name", old_name="mid_name"
                        ),
                    ],
                    [
                        migrations.AddIndex("Pony", renamed_index),
                    ],
                )
                self.assertDoesNotOptimize(
                    [
                        migrations.AddIndex("Pony", index),
                        migrations.RenameIndex(
                            "Pony", new_name="new_name", old_name="other_name"
                        ),
                    ],
                )

    def test_add_remove_index(self):
        self.assertOptimizesTo(
            [
                migrations.AddIndex(
                    "Pony",
                    models.Index(
                        fields=["weight", "pink"], name="idx_pony_weight_pink"
                    ),
                ),
                migrations.RemoveIndex("Pony", "idx_pony_weight_pink"),
            ],
            [],
        )

    def test_add_remove_constraint(self):
        gt_constraint = models.CheckConstraint(
            condition=models.Q(pink__gt=2), name="constraint_pony_pink_gt_2"
        )
        self.assertOptimizesTo(
            [
                migrations.AddConstraint("Pony", gt_constraint),
                migrations.RemoveConstraint("Pony", gt_constraint.name),
            ],
            [],
        )
        self.assertDoesNotOptimize(
            [
                migrations.AddConstraint("Pony", gt_constraint),
                migrations.RemoveConstraint("Pony", "other_name"),
            ],
        )

    def test_multiple_alter_constraints(self):
        gt_constraint_violation_msg_added = models.CheckConstraint(
            condition=models.Q(pink__gt=2),
            name="pink_gt_2",
            violation_error_message="ERROR",
        )
        gt_constraint_violation_msg_altered = models.CheckConstraint(
            condition=models.Q(pink__gt=2),
            name="pink_gt_2",
            violation_error_message="error",
        )
        self.assertOptimizesTo(
            [
                migrations.AlterConstraint(
                    "Pony", "pink_gt_2", gt_constraint_violation_msg_added
                ),
                migrations.AlterConstraint(
                    "Pony", "pink_gt_2", gt_constraint_violation_msg_altered
                ),
            ],
            [
                migrations.AlterConstraint(
                    "Pony", "pink_gt_2", gt_constraint_violation_msg_altered
                )
            ],
        )
        other_constraint_violation_msg = models.CheckConstraint(
            condition=models.Q(weight__gt=3),
            name="pink_gt_3",
            violation_error_message="error",
        )
        self.assertDoesNotOptimize(
            [
                migrations.AlterConstraint(
                    "Pony", "pink_gt_2", gt_constraint_violation_msg_added
                ),
                migrations.AlterConstraint(
                    "Pony", "pink_gt_3", other_constraint_violation_msg
                ),
            ]
        )

    def test_alter_remove_constraint(self):
        self.assertOptimizesTo(
            [
                migrations.AlterConstraint(
                    "Pony",
                    "pink_gt_2",
                    models.CheckConstraint(
                        condition=models.Q(pink__gt=2), name="pink_gt_2"
                    ),
                ),
                migrations.RemoveConstraint("Pony", "pink_gt_2"),
            ],
            [migrations.RemoveConstraint("Pony", "pink_gt_2")],
        )

    def test_add_alter_constraint(self):
        constraint = models.CheckConstraint(
            condition=models.Q(pink__gt=2), name="pink_gt_2"
        )
        constraint_with_error = models.CheckConstraint(
            condition=models.Q(pink__gt=2),
            name="pink_gt_2",
            violation_error_message="error",
        )
        self.assertOptimizesTo(
            [
                migrations.AddConstraint("Pony", constraint),
                migrations.AlterConstraint("Pony", "pink_gt_2", constraint_with_error),
            ],
            [migrations.AddConstraint("Pony", constraint_with_error)],
        )

    def test_create_model_add_index(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [models.Index(fields=["age"], name="idx_pony_age")],
                    },
                ),
                migrations.AddIndex(
                    "Pony",
                    models.Index(fields=["weight"], name="idx_pony_weight"),
                ),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [
                            models.Index(fields=["age"], name="idx_pony_age"),
                            models.Index(fields=["weight"], name="idx_pony_weight"),
                        ],
                    },
                ),
            ],
        )

    def test_create_model_remove_index(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [
                            models.Index(fields=["age"], name="idx_pony_age"),
                            models.Index(fields=["weight"], name="idx_pony_weight"),
                        ],
                    },
                ),
                migrations.RemoveIndex("Pony", "idx_pony_age"),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [
                            models.Index(fields=["weight"], name="idx_pony_weight"),
                        ],
                    },
                ),
            ],
        )

    def test_create_model_rename_index_no_old_fields(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [models.Index(fields=["age"], name="idx_pony_age")],
                    },
                ),
                migrations.RenameIndex(
                    "Pony", new_name="idx_pony_age_new", old_name="idx_pony_age"
                ),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                        ("age", models.IntegerField()),
                    ],
                    options={
                        "indexes": [models.Index(fields=["age"], name="idx_pony_age")],
                    },
                ),
                migrations.RenameIndex(
                    "Pony", new_name="idx_pony_age_new", old_name="idx_pony_age"
                ),
            ],
        )

    def test_create_model_add_constraint(self):
        gt_constraint = models.CheckConstraint(
            condition=models.Q(weight__gt=0), name="pony_weight_gt_0"
        )
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                ),
                migrations.AddConstraint("Pony", gt_constraint),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                    options={"constraints": [gt_constraint]},
                ),
            ],
        )

    def test_create_model_alter_constraint(self):
        original_constraint = models.CheckConstraint(
            condition=models.Q(weight__gt=0), name="pony_weight_gt_0"
        )
        altered_constraint = models.CheckConstraint(
            condition=models.Q(weight__gt=0),
            name="pony_weight_gt_0",
            violation_error_message="incorrect weight",
        )
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                    options={
                        "constraints": [
                            original_constraint,
                            models.UniqueConstraint(
                                "weight", name="pony_weight_unique"
                            ),
                        ],
                    },
                ),
                migrations.AlterConstraint(
                    "Pony", "pony_weight_gt_0", altered_constraint
                ),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                    options={
                        "constraints": [
                            models.UniqueConstraint(
                                "weight",
                                name="pony_weight_unique",
                            ),
                            altered_constraint,
                        ]
                    },
                ),
            ],
        )

    def test_create_model_remove_constraint(self):
        self.assertOptimizesTo(
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                    options={
                        "constraints": [
                            models.CheckConstraint(
                                condition=models.Q(weight__gt=0),
                                name="pony_weight_gt_0",
                            ),
                            models.UniqueConstraint(
                                "weight", name="pony_weight_unique"
                            ),
                        ],
                    },
                ),
                migrations.RemoveConstraint("Pony", "pony_weight_gt_0"),
            ],
            [
                migrations.CreateModel(
                    name="Pony",
                    fields=[
                        ("weight", models.IntegerField()),
                    ],
                    options={
                        "constraints": [
                            models.UniqueConstraint(
                                "weight", name="pony_weight_unique"
                            ),
                        ]
                    },
                ),
            ],
        )
