# 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 django.utils import timezone

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
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, WorkerType


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(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 = [
            (ArtifactCategory.WORK_REQUEST_DEBUG_LOGS, {}),
            (ArtifactCategory.PACKAGE_BUILD_LOG, {"source": "hello"}),
            (ArtifactCategory.UPLOAD, {"type": "dpkg"}),
            (
                ArtifactCategory.UPLOAD,
                {"type": "dpkg", "changes-fields": {"Source": "hello"}},
            ),
            (
                ArtifactCategory.UPLOAD,
                {"type": "dpkg", "changes-fields": {"Source": "coreutils"}},
            ),
            (
                ArtifactCategory.SOURCE_PACKAGE,
                {
                    "name": "hello",
                    "version": "1",
                    "type": "dpkg",
                    "dsc-fields": {"Source": "hello"},
                },
            ),
            (ArtifactCategory.BINARY_PACKAGE, {"srcpkg-name": "hello"}),
            (
                ArtifactCategory.BINARY_PACKAGE,
                {
                    "srcpkg-name": "hello",
                    "srcpkg-version": "1",
                    "deb-fields": {"Package": "hello"},
                    "deb-control-files": ["control"],
                },
            ),
            (
                ArtifactCategory.BINARY_PACKAGES,
                {
                    "srcpkg-name": "hello",
                    "srcpkg-version": "1",
                    "version": "1",
                    "architecture": "amd64",
                    "packages": ["hello"],
                },
            ),
        ]
        new_artifacts = [
            (ArtifactCategory.WORK_REQUEST_DEBUG_LOGS, {}),
            (ArtifactCategory.PACKAGE_BUILD_LOG, {"source": "hello"}),
            (ArtifactCategory.UPLOAD, {"type": "dpkg"}),
            (
                ArtifactCategory.UPLOAD,
                {"type": "dpkg", "changes_fields": {"Source": "hello"}},
            ),
            (
                ArtifactCategory.UPLOAD,
                {"type": "dpkg", "changes_fields": {"Source": "coreutils"}},
            ),
            (
                ArtifactCategory.SOURCE_PACKAGE,
                {
                    "name": "hello",
                    "version": "1",
                    "type": "dpkg",
                    "dsc_fields": {"Source": "hello"},
                },
            ),
            (ArtifactCategory.BINARY_PACKAGE, {"srcpkg_name": "hello"}),
            (
                ArtifactCategory.BINARY_PACKAGE,
                {
                    "srcpkg_name": "hello",
                    "srcpkg_version": "1",
                    "deb_fields": {"Package": "hello"},
                    "deb_control_files": ["control"],
                },
            ),
            (
                ArtifactCategory.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=CollectionCategory.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=CollectionCategory.WORKFLOW_INTERNAL,
            workspace=workspace,
            retains_artifacts=False,
        )
        Collection.objects.create(
            name="test",
            category=CollectionCategory.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],
        )

    def test_internalnoop_rename(self):
        """`internalnoop` work requests are renamed to `servernoop`."""
        migrate_from = [("db", "0061_workrequest_supersedes")]
        migrate_to = [("db", "0062_rename_internalnoop")]
        old_work_requests = [
            (TaskTypes.WORKER, "internalnoop", {"result": True}),
            (TaskTypes.SERVER, "noop", {"result": True}),
            (TaskTypes.SERVER, "internalnoop", {"result": True}),
            (TaskTypes.SERVER, "internalnoop", {"result": False}),
        ]
        new_work_requests = [
            (TaskTypes.WORKER, "internalnoop", {"result": True}),
            (TaskTypes.SERVER, "noop", {"result": True}),
            (TaskTypes.SERVER, "servernoop", {"result": True}),
            (TaskTypes.SERVER, "servernoop", {"result": False}),
        ]

        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, task_name, task_data in old_work_requests:
            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=task_name,
                task_data=task_data,
            )

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

        self.assertCountEqual(
            [
                (request.task_type, 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(migrate_from)

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

    def test_worker_worker_type(self):
        """The Worker.internal to Worker.worker_type migration works."""
        migrate_from = [("db", "0062_rename_internalnoop")]
        migrate_to = [("db", "0065_worker_worker_type_final")]

        executor = MigrationExecutor(
            connection, progress_callback=_migrate_check_constraints
        )
        executor.migrate(migrate_from)
        old_apps = executor.loader.project_state(migrate_from).apps

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

        Worker = old_apps.get_model("db", "Worker")
        Token = old_apps.get_model("db", "Token")
        external_workers = [
            Worker.objects.create(
                name=f"worker-{i}",
                token=Token.objects.create(hash=_generate_hash(str(i))),
                registered_at=timezone.now(),
            )
            for i in range(2)
        ]
        celery_worker = Worker.objects.create(
            name="celery", internal=True, registered_at=timezone.now()
        )

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

        self.assertEqual(
            [
                worker.worker_type
                for worker in new_apps.get_model(
                    "db", "Worker"
                ).objects.order_by("id")
            ],
            [WorkerType.EXTERNAL, WorkerType.EXTERNAL, WorkerType.CELERY],
        )

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

        self.assertEqual(
            [
                worker.internal
                for worker in old_apps.get_model(
                    "db", "Worker"
                ).objects.order_by("id")
            ],
            [False, False, True],
        )
