from __future__ import annotations

import os
import unittest
from unittest import skipIf

import django
from django.conf import settings
from django.core.management import call_command

from django_migration_linter import MigrationLinter
from tests import fixtures


class BaseBackwardCompatibilityDetection:
    def setUp(self, *args, **kwargs):
        self.database = next(iter(self.databases))
        self.test_project_path = os.path.dirname(settings.BASE_DIR)
        call_command(
            "migrate",
            "app_unique_together",
            "0002",
            database=self.database,
        )
        self.addCleanup(
            call_command,
            "migrate",
            "app_unique_together",
            database=self.database,
        )
        return super().setUp(*args, **kwargs)

    def _test_linter_finds_errors(self, app=None, commit_id=None):
        linter = self._launch_linter(app, commit_id)
        self.assertTrue(linter.has_errors)
        self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0)
        self.assertGreater(linter.nb_total, 0)

    def _test_linter_finds_no_errors(self, app=None, commit_id=None):
        linter = self._launch_linter(app, commit_id)
        self.assertFalse(linter.has_errors)
        self.assertNotEqual(linter.nb_valid + linter.nb_erroneous, 0)
        self.assertGreater(linter.nb_total, 0)

    def _test_linter_linted_no_migration(self, app=None, commit_id=None):
        linter = self._launch_linter(app, commit_id)
        self.assertEqual(linter.nb_valid, 0)
        self.assertEqual(linter.nb_erroneous, 0)
        self.assertEqual(linter.nb_total, 0)

    def _launch_linter(self, app=None, commit_id=None):
        linter = self._get_linter()
        linter.lint_all_migrations(app_label=app, git_commit_id=commit_id)
        return linter

    def _get_linter(self):
        return MigrationLinter(
            self.test_project_path,
            database=self.database,
            no_cache=True,
        )

    def test_create_table_with_not_null_column(self):
        app = fixtures.CREATE_TABLE_WITH_NOT_NULL_COLUMN
        self._test_linter_finds_no_errors(app)

    def test_detect_adding_not_null_column(self):
        app = fixtures.ADD_NOT_NULL_COLUMN
        self._test_linter_finds_errors(app)

    def test_detect_make_column_not_null_with_django_default(self):
        app = fixtures.MAKE_NOT_NULL_WITH_DJANGO_DEFAULT
        self._test_linter_finds_errors(app)

    def test_detect_make_column_not_null_with_lib_default(self):
        app = fixtures.MAKE_NOT_NULL_WITH_LIB_DEFAULT
        self._test_linter_finds_no_errors(app)

    def test_detect_drop_column(self):
        app = fixtures.DROP_COLUMN
        self._test_linter_finds_errors(app)

    def test_detect_drop_table(self):
        app = fixtures.DROP_TABLE
        self._test_linter_finds_errors(app)

    def test_detect_rename_column(self):
        app = fixtures.RENAME_COLUMN
        self._test_linter_finds_errors(app)

    def test_detect_rename_table(self):
        app = fixtures.RENAME_TABLE
        self._test_linter_finds_errors(app)

    def test_ignore_migration(self):
        app = fixtures.IGNORE_MIGRATION
        self._test_linter_finds_no_errors(app)

    def test_accept_not_null_column_followed_by_adding_default(self):
        app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT
        self._test_linter_finds_no_errors(app)

    @skipIf(django.VERSION[0] < 5, "db_default was implemented in Django 5.0")
    def test_accept_not_null_column_followed_by_adding_db_default(self):
        app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT
        self._test_linter_finds_no_errors(app)

    def test_detect_alter_column(self):
        app = fixtures.ALTER_COLUMN
        self._test_linter_finds_no_errors(app)

    def test_drop_unique_together(self):
        app = fixtures.DROP_UNIQUE_TOGETHER
        self._test_linter_finds_errors(app)

    def test_accept_alter_column_drop_not_null(self):
        app = fixtures.ALTER_COLUMN_DROP_NOT_NULL
        self._test_linter_finds_no_errors(app)

    def test_accept_adding_manytomany_field(self):
        app = fixtures.ADD_MANYTOMANY_FIELD
        self._test_linter_finds_no_errors(app)

    def test_with_git_ref(self):
        self._test_linter_finds_errors(commit_id="v0.1.4")

    def test_failing_get_sql(self):
        call_command("migrate", "app_unique_together", database=self.database)

        linter = MigrationLinter(database=self.database)
        with self.assertRaises(ValueError):
            linter.get_sql("app_unique_together", "0003")

    def test_custom_named_app(self):
        # Folder name is not found.
        app = fixtures.CUSTOM_APP_NAME_DIRECTORY
        self._test_linter_linted_no_migration(app)

        # Django app name is found.
        app = fixtures.CUSTOM_APP_LABEL
        self._test_linter_finds_no_errors(app)

        # And with git ref:
        app = fixtures.CUSTOM_APP_NAME_DIRECTORY
        self._test_linter_linted_no_migration(app, commit_id="v0.1.4")

        app = fixtures.CUSTOM_APP_LABEL
        self._test_linter_finds_no_errors(app, commit_id="v0.1.4")


class SqliteBackwardCompatibilityDetectionTestCase(
    BaseBackwardCompatibilityDetection, unittest.TestCase
):
    databases = ["sqlite"]

    def test_accept_not_null_column_followed_by_adding_default(self):
        app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT
        self._test_linter_finds_errors(app)

    def test_detect_make_column_not_null_with_django_default(self):
        app = fixtures.MAKE_NOT_NULL_WITH_DJANGO_DEFAULT
        self._test_linter_finds_errors(app)

    @skipIf(django.VERSION[0] < 5, "db_default was implemented in Django 5.0")
    def test_accept_not_null_column_followed_by_adding_db_default(self):
        app = fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DB_DEFAULT
        self._test_linter_finds_errors(app)

    def test_detect_make_column_not_null_with_lib_default(self):
        # The 'django-add-default-value' doesn't handle sqlite correctly
        app = fixtures.MAKE_NOT_NULL_WITH_LIB_DEFAULT
        self._test_linter_finds_errors(app)


class MySqlBackwardCompatibilityDetectionTestCase(
    BaseBackwardCompatibilityDetection, unittest.TestCase
):
    databases = ["mysql"]


class PostgresqlBackwardCompatibilityDetectionTestCase(
    BaseBackwardCompatibilityDetection, unittest.TestCase
):
    databases = ["postgresql"]

    def test_detect_alter_column(self):
        app = fixtures.ALTER_COLUMN
        self._test_linter_finds_errors(app)

    def test_create_index_exclusive(self):
        linter = self._launch_linter(fixtures.CREATE_INDEX_EXCLUSIVE)
        self.assertFalse(linter.has_errors)
        self.assertTrue(linter.nb_warnings)
