# Copyright 2022 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Unit tests for the migrations."""

import hashlib
from typing import Any

from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import is_password_usable
from django.db import connection
from django.db.migrations.exceptions import IrreversibleError
from django.db.migrations.executor import MigrationExecutor
from django.db.migrations.state import StateApps
from django.test import TestCase

from debusine.db.models import (
    DEFAULT_WORKSPACE_NAME,
    FileStore,
    SYSTEM_USER_NAME,
    default_file_store,
    default_workspace,
)
from debusine.db.models.collections import _CollectionRetainsArtifacts
from debusine.tasks.models import TaskTypes
from debusine.test import TestHelpersMixin


def _migrate_check_constraints(action: str, *args: Any, **kwargs: Any) -> None:
    """
    Check constraints before and after (un)applying each migration.

    When applying migrations in tests, each migration only ends with a
    savepoint rather than a full commit, so deferred constraints aren't
    immediately checked.  As a result, we need to explicitly check
    constraints to avoid "cannot ALTER TABLE ... because it has pending
    trigger events" errors in some cases.
    """
    if action in {
        "apply_start",
        "apply_success",
        "unapply_start",
        "unapply_success",
    }:
        connection.check_constraints()


class MigrationTests(TestHelpersMixin, TestCase):
    """Test migrations."""

    @staticmethod
    def get_test_user_for_apps(apps: StateApps):
        """
        Return a test user.

        We may be testing a migration, so use the provided application state.
        """
        user_model = apps.get_model("db", "User")
        try:
            return user_model.objects.get(username="usertest")
        except user_model.DoesNotExist:
            return user_model.objects.create_user(
                username="usertest",
                password="userpassword",
                email="usertest@example.org",
            )

    def test_default_store_created(self):
        """Assert Default FileStore has been created."""
        default_file_store = FileStore.default()

        self.assertEqual(
            default_file_store.backend, FileStore.BackendChoices.LOCAL
        )

        self.assertEqual(default_file_store.configuration, {})

    def test_system_workspace_created(self):
        """Assert System Workspace has been created."""
        workspace = default_workspace()
        self.assertEqual(workspace.default_file_store, default_file_store())

    def test_artifact_underscore_names(self):
        """Artifacts have their data migrated to new names."""
        migrate_from = [("db", "0013_expiration_delay")]
        migrate_to = [("db", "0014_artifact_underscore_names")]

        # Several of the category/data combinations here wouldn't appear in
        # practice, but are here to ensure full coverage of the data
        # migration code.
        old_artifacts = [
            ("debusine:work-request-debug-logs", {}),
            ("debian:package-build-log", {"source": "hello"}),
            ("debian:upload", {"type": "dpkg"}),
            (
                "debian:upload",
                {"type": "dpkg", "changes-fields": {"Source": "hello"}},
            ),
            (
                "debian:upload",
                {"type": "dpkg", "changes-fields": {"Source": "coreutils"}},
            ),
            (
                "debian:source-package",
                {
                    "name": "hello",
                    "version": "1",
                    "type": "dpkg",
                    "dsc-fields": {"Source": "hello"},
                },
            ),
            ("debian:binary-package", {"srcpkg-name": "hello"}),
            (
                "debian:binary-package",
                {
                    "srcpkg-name": "hello",
                    "srcpkg-version": "1",
                    "deb-fields": {"Package": "hello"},
                    "deb-control-files": ["control"],
                },
            ),
            (
                "debian:binary-packages",
                {
                    "srcpkg-name": "hello",
                    "srcpkg-version": "1",
                    "version": "1",
                    "architecture": "amd64",
                    "packages": ["hello"],
                },
            ),
        ]
        new_artifacts = [
            ("debusine:work-request-debug-logs", {}),
            ("debian:package-build-log", {"source": "hello"}),
            ("debian:upload", {"type": "dpkg"}),
            (
                "debian:upload",
                {"type": "dpkg", "changes_fields": {"Source": "hello"}},
            ),
            (
                "debian:upload",
                {"type": "dpkg", "changes_fields": {"Source": "coreutils"}},
            ),
            (
                "debian:source-package",
                {
                    "name": "hello",
                    "version": "1",
                    "type": "dpkg",
                    "dsc_fields": {"Source": "hello"},
                },
            ),
            ("debian:binary-package", {"srcpkg_name": "hello"}),
            (
                "debian:binary-package",
                {
                    "srcpkg_name": "hello",
                    "srcpkg_version": "1",
                    "deb_fields": {"Package": "hello"},
                    "deb_control_files": ["control"],
                },
            ),
            (
                "debian:binary-packages",
                {
                    "srcpkg_name": "hello",
                    "srcpkg_version": "1",
                    "version": "1",
                    "architecture": "amd64",
                    "packages": ["hello"],
                },
            ),
        ]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        workspace = old_apps.get_model("db", "Workspace").objects.get(
            name=DEFAULT_WORKSPACE_NAME
        )
        for category, data in old_artifacts:
            old_apps.get_model("db", "Artifact").objects.create(
                category=category, workspace=workspace, data=data
            )

        executor.loader.build_graph()
        executor.migrate(migrate_to)
        new_apps = executor.loader.project_state(migrate_to).apps

        self.assertCountEqual(
            [
                (artifact.category, artifact.data)
                for artifact in new_apps.get_model(
                    "db", "Artifact"
                ).objects.all()
            ],
            new_artifacts,
        )

        executor.loader.build_graph()
        executor.migrate(migrate_from)

        self.assertCountEqual(
            [
                (artifact.category, artifact.data)
                for artifact in old_apps.get_model(
                    "db", "Artifact"
                ).objects.all()
            ],
            old_artifacts,
        )

    def assert_work_request_task_data_renamed(
        self,
        migrate_from: str,
        migrate_to: str,
        old_work_requests: list[tuple[str, dict[str, Any]]],
        new_work_requests: list[tuple[str, dict[str, Any]]],
    ) -> None:
        """Assert that migrations rename task data in work requests."""
        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate([("db", migrate_from)])
        old_apps = executor.loader.project_state([("db", migrate_from)]).apps
        workspace = old_apps.get_model("db", "Workspace").objects.get(
            name=DEFAULT_WORKSPACE_NAME
        )
        for task_name, task_data in old_work_requests:
            old_apps.get_model("db", "WorkRequest").objects.create(
                created_by_id=self.get_test_user_for_apps(old_apps).id,
                task_name=task_name,
                workspace=workspace,
                task_data=task_data,
            )

        executor.loader.build_graph()
        executor.migrate([("db", migrate_to)])
        new_apps = executor.loader.project_state([("db", migrate_to)]).apps

        self.assertCountEqual(
            [
                (request.task_name, request.task_data)
                for request in new_apps.get_model(
                    "db", "WorkRequest"
                ).objects.all()
            ],
            new_work_requests,
        )

        executor.loader.build_graph()
        executor.migrate([("db", migrate_from)])

        self.assertCountEqual(
            [
                (request.task_name, request.task_data)
                for request in old_apps.get_model(
                    "db", "WorkRequest"
                ).objects.all()
            ],
            old_work_requests,
        )

    def test_autopkgtest_work_request_architecture_rename(self):
        """Work requests for autopkgtest rename their architecture field."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0018_workrequest_worker_blank",
            migrate_to="0019_autopkgtest_task_data",
            old_work_requests=[
                ("foo", {"architecture": "amd64"}),
                ("autopkgtest", {"architecture": "amd64"}),
            ],
            new_work_requests=[
                ("foo", {"architecture": "amd64"}),
                ("autopkgtest", {"host_architecture": "amd64"}),
            ],
        )

    def test_hash_added(self):
        """Assert token hash has been added."""
        migrate_from = [
            (
                'db',
                '0022_collectionitem_db_collectionitem_unique_debian_environment',  # noqa: E501
            )
        ]
        migrate_to = [('db', '0025_token_hash_final')]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        Token = old_apps.get_model('db', 'Token')
        old_token = Token.objects.create(
            comment="Test token",
        )
        self.assertIsNotNone(old_token.key)
        with self.assertRaises(AttributeError):
            old_token.hash

        executor.loader.build_graph()
        executor.migrate(migrate_to)
        new_apps = executor.loader.project_state(migrate_to).apps

        def _generate_hash(secret: str) -> str:
            return hashlib.sha256(secret.encode()).hexdigest()

        Token = new_apps.get_model('db', 'Token')
        new_token = Token.objects.get(hash=_generate_hash(old_token.key))

        with self.assertRaises(AttributeError):
            new_token.key

        self.assertIsNotNone(new_token)
        self.assertEqual(new_token.hash, _generate_hash(old_token.key))

    def test_hash_migration_irreversible(self):
        """Assert token hash migration is irreversible if there are tokens."""
        migrate_from = [('db', '0025_token_hash_final')]
        migrate_to = [('db', '0023_token_hash_initial')]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        Token = old_apps.get_model('db', 'Token')
        Token.objects.create(
            comment="Test token",
        )

        with self.assertRaises(IrreversibleError):
            executor.loader.build_graph()
            executor.migrate(migrate_to)

    def test_autopkgtest_work_request_environment_rename(self):
        """Work requests for autopkgtest rename their environment field."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0034_workrequest_pydantic",
            migrate_to="0035_autopkgtest_extra_environment",
            old_work_requests=[
                ("foo", {"environment": {"FOO": "foo", "BAR": "bar"}}),
                ("autopkgtest", {"environment": {"FOO": "foo", "BAR": "bar"}}),
            ],
            new_work_requests=[
                ("foo", {"environment": {"FOO": "foo", "BAR": "bar"}}),
                (
                    "autopkgtest",
                    {"extra_environment": {"FOO": "foo", "BAR": "bar"}},
                ),
            ],
        )

    def test_fix_task_type(self):
        """Work requests have their task_type fixed."""
        migrate_from = [("db", "0035_autopkgtest_extra_environment")]
        migrate_to = [("db", "0036_workrequest_fix_task_type")]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        workspace = old_apps.get_model("db", "Workspace").objects.get(
            name=DEFAULT_WORKSPACE_NAME
        )
        for task_type in ("worker", "server", "internal", "workflow"):
            old_apps.get_model("db", "WorkRequest").objects.create(
                workspace=workspace,
                created_by_id=self.get_test_user_for_apps(old_apps).id,
                task_type=task_type,
                task_name="noop",
            )

        executor.loader.build_graph()
        executor.migrate(migrate_to)
        new_apps = executor.loader.project_state(migrate_to).apps

        self.assertEqual(
            [
                wr.task_type
                for wr in new_apps.get_model(
                    "db", "WorkRequest"
                ).objects.order_by("id")
            ],
            [
                TaskTypes.WORKER,
                TaskTypes.SERVER,
                TaskTypes.INTERNAL,
                TaskTypes.WORKFLOW,
            ],
        )

        executor.loader.build_graph()
        executor.migrate(migrate_from)

        self.assertEqual(
            [
                wr.task_type
                for wr in new_apps.get_model(
                    "db", "WorkRequest"
                ).objects.order_by("id")
            ],
            ["worker", "server", "internal", "workflow"],
        )

    def test_environment_id_rename(self):
        """Work requests rename their environment_id field."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0038_alter_workrequest_parent",
            migrate_to="0039_rename_environment_id",
            old_work_requests=[
                ("autopkgtest", {"environment_id": 1}),
                ("lintian", {"environment_id": 1}),
                ("piuparts", {"environment_id": 1}),
                ("sbuild", {"backend": "schroot", "distribution": "bookworm"}),
                ("sbuild", {"backend": "unshare", "environment_id": 1}),
            ],
            new_work_requests=[
                ("autopkgtest", {"environment": 1}),
                ("lintian", {"environment": 1}),
                ("piuparts", {"environment": 1}),
                ("sbuild", {"backend": "schroot", "distribution": "bookworm"}),
                ("sbuild", {"backend": "unshare", "environment": 1}),
            ],
        )

    def test_blhc_artifact_id_rename(self):
        """Work requests for blhc rename their artifact_id field."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0039_rename_environment_id",
            migrate_to="0040_blhc_lookup",
            old_work_requests=[
                ("foo", {"input": {"artifact_id": 1}}),
                ("blhc", {"input": {}}),
                ("blhc", {"output": {"source_analysis": False}}),
                ("blhc", {"input": {"artifact_id": 1}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"artifact_id": 1}}),
                ("blhc", {"input": {}}),
                ("blhc", {"output": {"source_analysis": False}}),
                ("blhc", {"input": {"artifact": 1}}),
            ],
        )

    def test_piuparts_binary_artifacts_ids_rename(self):
        """Work requests for piuparts rename binary_artifacts_ids."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0040_blhc_lookup",
            migrate_to="0041_piuparts_lookup",
            old_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("piuparts", {"input": {}}),
                ("piuparts", {"backend": "incus-lxc"}),
                ("piuparts", {"input": {"binary_artifacts_ids": [1, 2]}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("piuparts", {"input": {}}),
                ("piuparts", {"backend": "incus-lxc"}),
                ("piuparts", {"input": {"binary_artifacts": [1, 2]}}),
            ],
        )

    def test_piuparts_base_tgz_id_rename(self):
        """Work requests for piuparts rename base_tgz_id."""
        self.assert_work_request_task_data_renamed(
            migrate_from="0040_blhc_lookup",
            migrate_to="0041_piuparts_lookup",
            old_work_requests=[
                ("foo", {"base_tgz_id": 1}),
                ("piuparts", {"backend": "incus-lxc"}),
                ("piuparts", {"base_tgz_id": 1}),
            ],
            new_work_requests=[
                ("foo", {"base_tgz_id": 1}),
                ("piuparts", {"backend": "incus-lxc"}),
                ("piuparts", {"base_tgz": 1}),
            ],
        )

    def test_create_system_user(self):
        """A system user is created."""
        migrate_from = [("db", "0042_user_is_system")]
        migrate_to = [("db", "0043_create_system_user")]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        self.assertFalse(
            old_apps.get_model("db", "User")
            .objects.filter(username=SYSTEM_USER_NAME)
            .exists()
        )

        executor.loader.build_graph()
        executor.migrate(migrate_to)
        new_apps = executor.loader.project_state(migrate_to).apps
        system_user = new_apps.get_model("db", "User").objects.get(
            username=SYSTEM_USER_NAME
        )
        self.assertFalse(is_password_usable(system_user.password))
        self.assertEqual(system_user.first_name, "Debusine")
        self.assertEqual(system_user.last_name, "System")
        self.assertTrue(system_user.is_system)

        executor.loader.build_graph()
        executor.migrate(migrate_from)
        self.assertFalse(
            old_apps.get_model("db", "User")
            .objects.filter(username=SYSTEM_USER_NAME)
            .exists()
        )

    def test_autopkgtest_source_artifact_id_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0044_collection_add_retains_artifacts",
            migrate_to="0045_autopkgtest_lookup",
            old_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"source_artifact_id": 1}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"source_artifact": 1}}),
            ],
        )

    def test_autopkgtest_binary_artifacts_ids_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0044_collection_add_retains_artifacts",
            migrate_to="0045_autopkgtest_lookup",
            old_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"binary_artifacts_ids": [1, 2]}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"binary_artifacts": [1, 2]}}),
            ],
        )

    def test_autopkgtest_context_artifacts_ids_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0044_collection_add_retains_artifacts",
            migrate_to="0045_autopkgtest_lookup",
            old_work_requests=[
                ("foo", {"input": {"context_artifacts_ids": [1, 2]}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"context_artifacts_ids": [1, 2]}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"context_artifacts_ids": [1, 2]}}),
                ("autopkgtest", {"input": {}}),
                ("autopkgtest", {"host_architecture": "amd64"}),
                ("autopkgtest", {"input": {"context_artifacts": [1, 2]}}),
            ],
        )

    def test_lintian_source_artifact_id_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0045_autopkgtest_lookup",
            migrate_to="0046_lintian_lookup",
            old_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("lintian", {"input": {}}),
                ("lintian", {"backend": "incus-lxc"}),
                ("lintian", {"input": {"source_artifact_id": 1}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("lintian", {"input": {}}),
                ("lintian", {"backend": "incus-lxc"}),
                ("lintian", {"input": {"source_artifact": 1}}),
            ],
        )

    def test_lintian_binary_artifacts_ids_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0045_autopkgtest_lookup",
            migrate_to="0046_lintian_lookup",
            old_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("lintian", {"input": {}}),
                ("lintian", {"backend": "incus-lxc"}),
                ("lintian", {"input": {"binary_artifacts_ids": [1, 2]}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"binary_artifacts_ids": [1, 2]}}),
                ("lintian", {"input": {}}),
                ("lintian", {"backend": "incus-lxc"}),
                ("lintian", {"input": {"binary_artifacts": [1, 2]}}),
            ],
        )

    def test_sbuild_source_artifact_id_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0046_lintian_lookup",
            migrate_to="0047_sbuild_lookup",
            old_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("sbuild", {"input": {}}),
                ("sbuild", {"host_architecture": "amd64"}),
                ("sbuild", {"input": {"source_artifact_id": 1}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"source_artifact_id": 1}}),
                ("sbuild", {"input": {}}),
                ("sbuild", {"host_architecture": "amd64"}),
                ("sbuild", {"input": {"source_artifact": 1}}),
            ],
        )

    def test_sbuild_extra_binary_artifacts_ids_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0046_lintian_lookup",
            migrate_to="0047_sbuild_lookup",
            old_work_requests=[
                ("foo", {"input": {"extra_binary_artifact_ids": [1, 2]}}),
                ("sbuild", {"input": {}}),
                ("sbuild", {"host_architecture": "amd64"}),
                ("sbuild", {"input": {"extra_binary_artifact_ids": [1, 2]}}),
            ],
            new_work_requests=[
                ("foo", {"input": {"extra_binary_artifact_ids": [1, 2]}}),
                ("sbuild", {"input": {}}),
                ("sbuild", {"host_architecture": "amd64"}),
                ("sbuild", {"input": {"extra_binary_artifacts": [1, 2]}}),
            ],
        )

    def test_updatesuitelintiancollection_base_collection_id_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0048_alter_collection_retains_artifacts",
            migrate_to="0049_update_suite_lintian_collection_lookup",
            old_work_requests=[
                ("foo", {"base_collection_id": 1}),
                ("updatesuitelintiancollection", {"force": True}),
                ("updatesuitelintiancollection", {"base_collection_id": 1}),
            ],
            new_work_requests=[
                ("foo", {"base_collection_id": 1}),
                ("updatesuitelintiancollection", {"force": True}),
                ("updatesuitelintiancollection", {"base_collection": 1}),
            ],
        )

    def test_updatesuitelintiancollection_derived_collection_id_rename(self):
        self.assert_work_request_task_data_renamed(
            migrate_from="0048_alter_collection_retains_artifacts",
            migrate_to="0049_update_suite_lintian_collection_lookup",
            old_work_requests=[
                ("foo", {"derived_collection_id": 1}),
                ("updatesuitelintiancollection", {"force": True}),
                ("updatesuitelintiancollection", {"derived_collection_id": 1}),
            ],
            new_work_requests=[
                ("foo", {"derived_collection_id": 1}),
                ("updatesuitelintiancollection", {"force": True}),
                ("updatesuitelintiancollection", {"derived_collection": 1}),
            ],
        )

    def test_collection_retains_artifacts_workflow(self):
        """The Collection.{retains_artifacts,workflow} migration works."""
        migrate_from = [("db", "0049_update_suite_lintian_collection_lookup")]
        migrate_to = [("db", "0052_collection_workflow_final")]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps
        Collection = old_apps.get_model("db", "Collection")
        workspace = old_apps.get_model("db", "Workspace").objects.get(
            name=DEFAULT_WORKSPACE_NAME
        )
        Collection.objects.create(
            name="test",
            category="debian:environments",
            workspace=workspace,
            retains_artifacts=True,
        )
        workflow = old_apps.get_model("db", "WorkRequest").objects.create(
            workspace=workspace,
            created_by_id=self.get_test_user_for_apps(old_apps).id,
            task_type=TaskTypes.WORKFLOW,
            task_name="noop",
        )
        Collection.objects.create(
            name=f"workflow-{workflow.id}",
            category="debusine:workflow-internal",
            workspace=workspace,
            retains_artifacts=False,
        )
        Collection.objects.create(
            name="test",
            category="debusine:workflow-internal",
            workspace=workspace,
            retains_artifacts=False,
        )
        Collection.objects.create(
            name="test",
            category="debusine:test",
            workspace=workspace,
            retains_artifacts=False,
        )

        executor.loader.build_graph()
        executor.migrate(migrate_to)
        new_apps = executor.loader.project_state(migrate_to).apps

        self.assertEqual(
            [
                (collection.retains_artifacts, collection.workflow)
                for collection in new_apps.get_model(
                    "db", "Collection"
                ).objects.order_by("id")
            ],
            [
                (_CollectionRetainsArtifacts.ALWAYS, None),
                (
                    _CollectionRetainsArtifacts.WORKFLOW,
                    new_apps.get_model("db", "WorkRequest").objects.get(
                        id=workflow.id
                    ),
                ),
                (_CollectionRetainsArtifacts.NEVER, None),
                (_CollectionRetainsArtifacts.NEVER, None),
            ],
        )

        executor.loader.build_graph()
        executor.migrate(migrate_from)
        new_apps = executor.loader.project_state(migrate_to).apps

        self.assertEqual(
            [
                collection.retains_artifacts
                for collection in old_apps.get_model(
                    "db", "Collection"
                ).objects.order_by("id")
            ],
            [True, False, False, False],
        )
