from __future__ import annotations

import os
import unittest

from django.conf import settings
from django.db import migrations

from django_migration_linter import MigrationLinter
from tests import fixtures


class DataMigrationDetectionTestCase(unittest.TestCase):
    def setUp(self, *args, **kwargs):
        self.test_project_path = os.path.dirname(settings.BASE_DIR)
        self.linter = MigrationLinter(
            self.test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
        )

    def test_reverse_data_migration(self):
        self.assertEqual(0, self.linter.nb_warnings)
        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0002_missing_reverse")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(1, self.linter.nb_warnings)
        self.assertFalse(self.linter.has_errors)

    def test_reverse_data_migration_ignore(self):
        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0003_incorrect_arguments")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(1, self.linter.nb_warnings)
        self.assertFalse(self.linter.has_errors)

    def test_exclude_warning_from_test(self):
        self.linter = MigrationLinter(
            self.test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
            exclude_migration_tests=("RUNPYTHON_REVERSIBLE",),
        )

        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0002_missing_reverse")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(0, self.linter.nb_warnings)
        self.assertEqual(1, self.linter.nb_valid)
        self.assertFalse(self.linter.has_errors)

    def test_all_warnings_as_errors(self):
        self.linter = MigrationLinter(
            self.test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
            all_warnings_as_errors=True,
        )

        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0003_incorrect_arguments")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(0, self.linter.nb_warnings)
        self.assertEqual(1, self.linter.nb_erroneous)
        self.assertTrue(self.linter.has_errors)

    def test_warnings_as_errors_tests_matches(self):
        self.linter = MigrationLinter(
            self.test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
            warnings_as_errors_tests=["RUNPYTHON_ARGS_NAMING_CONVENTION"],
        )

        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0003_incorrect_arguments")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(0, self.linter.nb_warnings)
        self.assertEqual(1, self.linter.nb_erroneous)
        self.assertTrue(self.linter.has_errors)

    def test_warnings_as_errors_tests_no_match(self):
        self.linter = MigrationLinter(
            self.test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
            warnings_as_errors_tests=[
                "RUNPYTHON_MODEL_IMPORT",
                "RUNPYTHON_MODEL_VARIABLE_NAME",
            ],
        )

        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0003_incorrect_arguments")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(1, self.linter.nb_warnings)
        self.assertEqual(0, self.linter.nb_erroneous)
        self.assertFalse(self.linter.has_errors)

    def test_partial_function(self):
        reverse_migration = self.linter.migration_loader.disk_migrations[
            ("app_data_migrations", "0004_partial_function")
        ]
        self.linter.lint_migration(reverse_migration)

        self.assertEqual(1, self.linter.nb_warnings)
        self.assertFalse(self.linter.has_errors)


class DataMigrationModelImportTestCase(unittest.TestCase):
    def test_missing_get_model_import(self):
        def incorrect_importing_model_forward(apps, schema_editor):
            from tests.test_project.app_data_migrations.models import MyModel

            MyModel.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(
            incorrect_importing_model_forward
        )
        self.assertEqual(1, len(issues))

    def test_correct_get_model_import(self):
        def correct_importing_model_forward(apps, schema_editor):
            MyModel = apps.get_model("app_data_migrations", "MyModel")
            MyVeryLongLongLongModel = apps.get_model(
                "app_data_migrations", "MyVeryLongLongLongModel"
            )
            MultiLineModel = apps.get_model(
                "app_data_migrations",
                "MultiLineModel",
            )

            MyModel.objects.filter(id=1).first()
            MyVeryLongLongLongModel.objects.filter(id=1).first()
            MultiLineModel.objects.all()

        issues = MigrationLinter.get_runpython_model_import_issues(
            correct_importing_model_forward
        )
        self.assertEqual(0, len(issues))

    def test_not_overlapping_model_name(self):
        """
        Correct for the import error, but should raise a warning.
        """

        def forward_method(apps, schema_editor):
            User = apps.get_model("auth", "CustomUserModel")

            User.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
        self.assertEqual(0, len(issues))

    def test_correct_one_param_get_model_import(self):
        def forward_method(apps, schema_editor):
            User = apps.get_model("auth.User")

            User.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
        self.assertEqual(0, len(issues))

    def test_not_overlapping_one_param(self):
        """
        Not an error, but should raise a warning.
        """

        def forward_method(apps, schema_editor):
            User = apps.get_model("auth.CustomUserModel")

            User.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
        self.assertEqual(0, len(issues))

    def test_m2m_through_orm_usage(self):
        def forward_method(apps, schema_editor):
            MyModel = apps.get_model("myapp", "MyModel")

            MyModel.many_to_many.through.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
        self.assertEqual(0, len(issues))

    def test_missing_m2m_through_orm(self):
        def forward_method(apps, schema_editor):
            from tests.test_project.app_data_migrations.models import MyModel

            MyModel.many_to_many.through.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_import_issues(forward_method)
        self.assertEqual(1, len(issues))


class DataMigrationModelVariableNamingTestCase(unittest.TestCase):
    def test_same_variable_name(self):
        def forward_op(apps, schema_editor):
            MyModel = apps.get_model("app", "MyModel")

            MyModel.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(0, len(issues))

    def test_same_variable_name_multiline(self):
        def forward_op(apps, schema_editor):
            MyModelVeryLongLongLongLongLong = apps.get_model(
                "app", "MyModelVeryLongLongLongLongLong"
            )

            MyModelVeryLongLongLongLongLong.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(0, len(issues))

    def test_same_variable_name_multiline2(self):
        def forward_op(apps, schema_editor):
            MyModelVeryLongLongLongLongLong = apps.get_model(
                "app_name_longlonglonglongapp",
                "MyModelVeryLongLongLongLongLong",
            )

            MyModelVeryLongLongLongLongLong.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(0, len(issues))

    def test_different_variable_name(self):
        def forward_op(apps, schema_editor):
            some_model = apps.get_model("app", "MyModel")

            some_model.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(1, len(issues))

    def test_diff_variable_name_multiline(self):
        def forward_op(apps, schema_editor):
            MyModelVeryLongLongLongLongLongNot = apps.get_model(
                "app", "MyModelVeryLongLongLongLongLong"
            )

            MyModelVeryLongLongLongLongLongNot.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(1, len(issues))

    def test_diff_variable_name_multiline2(self):
        def forward_op(apps, schema_editor):
            MyModelVeryLongLongLongLongLongNot = apps.get_model(
                "app_name_longlonglonglongapp",
                "MyModelVeryLongLongLongLongLong",
            )

            MyModelVeryLongLongLongLongLongNot.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(1, len(issues))

    def test_same_variable_name_one_param(self):
        def forward_op(apps, schema_editor):
            MyModel = apps.get_model("app.MyModel")

            MyModel.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(0, len(issues))

    def test_different_variable_name_one_param(self):
        def forward_op(apps, schema_editor):
            mymodel = apps.get_model("app.MyModel")

            mymodel.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(1, len(issues))

    def test_correct_variable_name_one_param_multiline(self):
        def forward_op(apps, schema_editor):
            AVeryLongModelName = apps.get_model(
                "quite_long_app_name.AVeryLongModelName"
            )

            AVeryLongModelName.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(0, len(issues))

    def test_different_variable_name_one_param_multiline(self):
        def forward_op(apps, schema_editor):
            m = apps.get_model(
                "quite_long_app_name_name_name.AVeryLongModelNameNameName"
            )

            m.objects.filter(id=1).first()

        issues = MigrationLinter.get_runpython_model_variable_naming_issues(forward_op)
        self.assertEqual(1, len(issues))


class RunSQLMigrationTestCase(unittest.TestCase):
    def setUp(self):
        test_project_path = os.path.dirname(settings.BASE_DIR)
        self.linter = MigrationLinter(
            test_project_path,
            include_apps=fixtures.DATA_MIGRATIONS,
        )

    def test_missing_reserve_migration(self):
        runsql = migrations.RunSQL("sql;")

        error, ignored, warning = self.linter.lint_runsql(runsql)
        self.assertEqual("RUNSQL_REVERSIBLE", warning[0].code)

    def test_sql_linting_error(self):
        runsql = migrations.RunSQL("ALTER TABLE t DROP COLUMN t;")

        error, ignored, warning = self.linter.lint_runsql(runsql)
        self.assertEqual("DROP_COLUMN", error[0].code)

    def test_sql_linting_error_array(self):
        runsql = migrations.RunSQL(
            ["ALTER TABLE t DROP COLUMN c;", "ALTER TABLE t RENAME COLUMN c;"]
        )

        error, ignored, warning = self.linter.lint_runsql(runsql)
        self.assertEqual("DROP_COLUMN", error[0].code)
        self.assertEqual("RENAME_COLUMN", error[1].code)

    def test_sql_linting_error_args(self):
        runsql = migrations.RunSQL([("ALTER TABLE %s DROP COLUMN %s;", ("t", "c"))])

        error, ignored, warning = self.linter.lint_runsql(runsql)
        self.assertEqual("DROP_COLUMN", error[0].code)
