# Copyright 2024 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 collection models."""
from datetime import timedelta

from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.utils import IntegrityError
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.db.models import (
    Artifact,
    Collection,
    CollectionItem,
)
from debusine.server.collections import DebianSuiteManager
from debusine.test import TestHelpersMixin


class CollectionTests(TestHelpersMixin, TestCase):
    """Tests for Collection class."""

    def setUp(self):
        """Set up tests."""
        self.user = get_user_model().objects.create_user(
            username="John", email="john@example.org"
        )
        self.workspace = self.create_workspace(name="System")

    def test_str(self):
        """Stringification should return Id, Name and Category."""
        name = "Environments"
        category = CollectionCategory.ENVIRONMENTS
        collection = Collection.objects.create(
            name=name, category=category, workspace=self.workspace
        )
        self.assertEqual(
            str(collection),
            f"Id: {collection.id} Name: {name} Category: {category}",
        )

    def test_get_absolute_url(self):
        """Test get_absolute_url results."""
        name = "Environments"
        category = CollectionCategory.ENVIRONMENTS
        collection = Collection.objects.create(
            name=name, category=category, workspace=self.workspace
        )

        self.assertEqual(
            collection.get_absolute_url(),
            reverse(
                "workspaces:collections:detail",
                kwargs={
                    "wname": self.workspace.name,
                    "ccat": category,
                    "cname": name,
                },
            ),
        )

    def test_constraint_name_category_workspace(self):
        """Raise integrity error: name, category, workspace must be unique."""
        name = "Environments"
        category = CollectionCategory.ENVIRONMENTS

        Collection.objects.create(
            name=name, category=category, workspace=self.workspace
        )

        with transaction.atomic(), self.assertRaisesRegex(
            IntegrityError, "db_collection_unique_name_category_workspace"
        ):
            Collection.objects.create(
                name=name, category=category, workspace=self.workspace
            )

        other_workspace = self.create_workspace(name="other")
        Collection.objects.create(
            name=name, category=category, workspace=other_workspace
        )

    def test_name_not_empty(self):
        """Cannot create Collection with an empty name."""
        with self.assertRaisesRegex(
            IntegrityError, "db_collection_name_not_empty"
        ):
            Collection.objects.create(
                name="",
                category=CollectionCategory.ENVIRONMENTS,
                workspace=self.workspace,
            )

    def test_category_not_empty(self):
        """Cannot create Collection with an empty category."""
        with self.assertRaisesRegex(
            IntegrityError, "db_collection_category_not_empty"
        ):
            Collection.objects.create(
                name="name", category="", workspace=self.workspace
            )

    def test_retains_artifacts(self):
        """By default, a new Collection stops its artifacts expiring."""
        collection = Collection.objects.create(
            name="Environments",
            category=CollectionCategory.ENVIRONMENTS,
            workspace=self.workspace,
        )
        self.assertEqual(
            collection.retains_artifacts, Collection.RetainsArtifacts.ALWAYS
        )

    def test_manager(self):
        """Test getting specialized manager."""
        collection = Collection.objects.create(
            name="Testing",
            category=CollectionCategory.SUITE,
            workspace=self.workspace,
        )
        self.assertEqual(type(collection.manager), DebianSuiteManager)

    def test_child_items_artifacts_collections(self):
        """Child items returns expected CollectionItems, artifacts, etc."""
        collection_parent_1 = Collection.objects.create(
            name="collection-1",
            category="collection-1",
            workspace=self.workspace,
        )
        collection_parent_2 = Collection.objects.create(
            name="collection-2",
            category="collection-2",
            workspace=self.workspace,
        )

        collection = Collection.objects.create(
            name="collection-3",
            category="collection-3",
            workspace=self.workspace,
        )
        collection_item_collection_1 = CollectionItem.objects.create(
            name="test",
            category="test",
            parent_collection=collection_parent_1,
            child_type=CollectionItem.Types.COLLECTION,
            collection=collection,
            created_by_user=self.user,
        )

        artifact, _ = self.create_artifact()
        artifact_item_collection_1 = CollectionItem.objects.create(
            name="test-2",
            category="test-2",
            parent_collection=collection_parent_1,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact,
            created_by_user=self.user,
        )

        # Used to see that is not returned when querying
        # collection_parent_1.child_items.all()
        CollectionItem.objects.create(
            name="test",
            category="test",
            parent_collection=collection_parent_2,
            child_type=CollectionItem.Types.COLLECTION,
            collection=Collection.objects.create(
                name="collection-4",
                category="collection-3",
                workspace=self.workspace,
            ),
            created_by_user=self.user,
        )

        # Collection.child_items returns all CollectionItems
        self.assertQuerysetEqual(
            collection_parent_1.child_items.all(),
            [collection_item_collection_1, artifact_item_collection_1],
            ordered=False,
        )

        # Collection.child_artifacts returns only the artifacts
        self.assertQuerysetEqual(
            collection_parent_1.child_artifacts.all(), [artifact]
        )

        # Collection.child_collections returns only the collections
        self.assertQuerysetEqual(
            collection_parent_1.child_collections.all(), [collection]
        )

        # Given the artifact: there is a reverse relationship
        # with the collections that belongs to
        self.assertQuerysetEqual(
            artifact.parent_collections.all(),
            [collection_parent_1],
        )

        # Given a collection: there is a reverse relationship
        # with the collections that belongs to
        self.assertQuerysetEqual(
            collection.parent_collections.all(),
            [collection_parent_1],
        )

        # Given a artifact: there is a reverse relationship
        # to the CollectionItems that belongs to
        self.assertQuerysetEqual(
            artifact.collection_items.all(),
            [artifact_item_collection_1],
        )

        # Given a collection: there is a reverse relationship
        # to the CollectionItems that belongs to
        self.assertQuerysetEqual(
            collection.collection_items.all(),
            [collection_item_collection_1],
        )


class CollectionItemManagerTests(TestHelpersMixin, TestCase):
    """Tests for CollectionItemManager class."""

    def setUp(self):
        """Create objects for the tests."""
        self.workspace = self.create_workspace(name="System")
        self.collection = Collection.objects.create(
            name="Name",
            category=CollectionCategory.ENVIRONMENTS,
            workspace=self.workspace,
        )
        self.user = get_user_model().objects.create_user(
            username="John", email="john@example.org"
        )

    def test_create_from_artifact(self):
        """Verify create_from_artifact method."""
        data = {"a": "b"}
        artifact, _ = self.create_artifact(data=data)

        name = "some-name"
        collection_item = CollectionItem.objects.create_from_artifact(
            artifact,
            parent_collection=self.collection,
            name=name,
            data=data,
            created_by_user=self.user,
        )

        self.assertEqual(collection_item.parent_collection, self.collection)
        self.assertEqual(collection_item.name, name)
        self.assertEqual(collection_item.artifact, artifact)
        self.assertEqual(
            collection_item.child_type, CollectionItem.Types.ARTIFACT
        )
        self.assertEqual(collection_item.category, artifact.category)
        self.assertEqual(collection_item.data, data)
        self.assertEqual(collection_item.created_by_user, self.user)

    def test_create_from_collection(self):
        """Verify create_from_collection method."""
        category = "some-category"
        name = "collection-name"
        data = {"a": "b"}

        collection = Collection.objects.create(
            name="collection", category=category, workspace=self.workspace
        )

        collection_item = CollectionItem.objects.create_from_collection(
            collection,
            parent_collection=self.collection,
            name=name,
            data=data,
            created_by_user=self.user,
        )

        self.assertEqual(collection_item.parent_collection, self.collection)
        self.assertEqual(collection_item.name, name)
        self.assertEqual(collection_item.collection, collection)
        self.assertEqual(
            collection_item.child_type, CollectionItem.Types.COLLECTION
        )
        self.assertEqual(collection_item.category, category)
        self.assertEqual(collection_item.data, data)
        self.assertEqual(collection_item.created_by_user, self.user)

    def test_drop_full_history(self):
        """Verify drop_full_history method."""
        data = {"a": "b"}
        artifact, _ = self.create_artifact(data=data)

        self.collection.full_history_retention_period = timedelta(days=2)
        self.collection.save()

        collection_item_removed = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=3),
        )

        collection_item_young = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=1),
        )

        collection_item_keep = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
        )

        CollectionItem.objects.drop_full_history(timezone.now())

        collection_item_removed.refresh_from_db()
        self.assertIsNone(collection_item_removed.artifact)

        collection_item_young.refresh_from_db()
        self.assertIsNotNone(collection_item_young.artifact)

        collection_item_keep.refresh_from_db()
        self.assertIsNotNone(collection_item_keep.artifact)

    def test_drop_full_history_no_retention_period(self):
        """Verify drop_full_history method without retention_period."""
        data = {"a": "b"}
        artifact, _ = self.create_artifact(data=data)

        collection_item_removed = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=3),
        )

        collection_item_young = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=1),
        )

        collection_item_keep = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
        )

        CollectionItem.objects.drop_full_history(timezone.now())

        collection_item_removed.refresh_from_db()
        self.assertIsNotNone(collection_item_removed.artifact)

        collection_item_young.refresh_from_db()
        self.assertIsNotNone(collection_item_young.artifact)

        collection_item_keep.refresh_from_db()
        self.assertIsNotNone(collection_item_keep.artifact)

    def test_drop_metadata(self):
        """Verify drop_metadata method."""
        data = {"a": "b"}
        artifact, _ = self.create_artifact(data=data)

        self.collection.full_history_retention_period = timedelta(days=1)
        self.collection.metadata_only_retention_period = timedelta(days=1)
        self.collection.save()

        collection_item_removed = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=3),
        )

        collection_item_young = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=1),
        )

        collection_item_keep = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
        )

        CollectionItem.objects.drop_metadata(timezone.now())

        with self.assertRaises(CollectionItem.DoesNotExist):
            CollectionItem.objects.get(id=collection_item_removed.id)

        collection_item_young.refresh_from_db()
        self.assertIsNotNone(collection_item_young)

        collection_item_keep.refresh_from_db()
        self.assertIsNotNone(collection_item_keep)

    def test_drop_metadata_no_retention_period(self):
        """Verify drop_metadata method."""
        data = {"a": "b"}
        artifact, _ = self.create_artifact(data=data)

        collection_item_removed = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=3),
        )

        collection_item_young = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
            removed_at=timezone.now() - timedelta(days=1),
        )

        collection_item_keep = CollectionItem.objects.create(
            parent_collection=self.collection,
            name="some-name",
            artifact=artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            category=artifact.category,
            data=data,
            created_by_user=self.user,
        )

        CollectionItem.objects.drop_metadata(timezone.now())

        collection_item_removed.refresh_from_db()
        self.assertIsNotNone(collection_item_removed)

        collection_item_young.refresh_from_db()
        self.assertIsNotNone(collection_item_young)

        collection_item_keep.refresh_from_db()
        self.assertIsNotNone(collection_item_keep)


class CollectionItemTests(TestHelpersMixin, TestCase):
    """Tests for CollectionItem class."""

    def setUp(self):
        """Create objects for the tests."""
        self.artifact, _ = self.create_artifact()

        self.workspace = self.create_workspace(name="System")
        self.collection = Collection.objects.create(
            name="Name",
            category=CollectionCategory.ENVIRONMENTS,
            workspace=self.workspace,
        )
        self.user = get_user_model().objects.create_user(
            username="John", email="john@example.org"
        )

    def test_artifact_collection_item_same_name(self):
        """Two CollectionItem: same name, category different child_type."""
        name = "name:duplicated"
        category = "category:duplicated"

        CollectionItem.objects.create(
            name=name,
            category=category,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=self.artifact,
            created_by_user=self.user,
        )
        with self.assertRaisesRegex(
            IntegrityError, "db_collectionitem_unique_active_name"
        ):
            CollectionItem.objects.create(
                name=name,
                category=category,
                parent_collection=self.collection,
                child_type=CollectionItem.Types.COLLECTION,
                collection=Collection.objects.create(
                    name="Test", category="test", workspace=self.workspace
                ),
                created_by_user=self.user,
            )

    def test_collection_and_parent_collection_not_the_same(self):
        """Cannot create CollectionItem with collection == parent_collection."""
        with self.assertRaisesRegex(
            IntegrityError, "db_collectionitem_distinct_parent_collection"
        ):
            CollectionItem.objects.create(
                name="Test",
                category="Category",
                parent_collection=self.collection,
                child_type=CollectionItem.Types.COLLECTION,
                collection=self.collection,
                created_by_user=self.user,
            )

    def test_only_one_active_item_in_collection(self):
        """Cannot create duplicated CollectionItem (not removed)."""
        name = "Name of the item"
        category = ArtifactCategory.SOURCE_PACKAGE

        CollectionItem.objects.create(
            name=name,
            category=category,
            artifact=self.artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        with self.assertRaisesRegex(
            IntegrityError,
            "db_collectionitem_unique_active_name",
        ):
            CollectionItem.objects.create(
                name=name,
                category=category,
                artifact=self.artifact,
                child_type=CollectionItem.Types.ARTIFACT,
                parent_collection=self.collection,
                created_by_user=self.user,
            )

    def test_second_active_item_added_first_one_removed(self):
        """Can create "duplicated" CollectionItem if first one is removed."""
        name = "Name of the item"
        category = ArtifactCategory.SOURCE_PACKAGE

        CollectionItem.objects.create(
            name=name,
            category=category,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
            removed_at=timezone.now(),
        )

        # Collection item can be added (no exception raised) because
        # the first one is removed
        CollectionItem.objects.create(
            name=name,
            category=category,
            artifact=self.artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

    def test_artifact_related_name(self):
        """Test artifact model "related_name" to CollectionItem."""
        artifact_in_collection_1 = CollectionItem.objects.create(
            name="name-1",
            category="category",
            artifact=self.artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        artifact_in_collection_2 = CollectionItem.objects.create(
            name="name-2",
            category="category",
            artifact=self.artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        self.assertQuerysetEqual(
            self.artifact.collection_items.all(),
            {artifact_in_collection_1, artifact_in_collection_2},
            ordered=False,
        )

    def test_artifact_constraint_are_valid_or_raise_integrity_error(self):
        """Verify constraints specific to an ARTIFACT type CollectionItem."""
        # CollectionItem of type ARTIFACT with an artifact
        CollectionItem.objects.create(
            name="Name",
            category="Category",
            artifact=self.artifact,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            created_by_user=self.user,
        )

        # CollectionItem of type ARTIFACT without an artifact
        # (the artifact was removed)
        CollectionItem.objects.create(
            name="Name-2",
            category="Category-2",
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            created_by_user=self.user,
            removed_at=timezone.now(),
        )

        collection = Collection.objects.create(
            name="Collection-2", category="Category-2", workspace=self.workspace
        )

        # CollectionItem of type ARTIFACT with a collection
        with self.assertRaisesRegex(
            IntegrityError, "db_collectionitem_childtype_removedat_consistent"
        ):
            CollectionItem.objects.create(
                name="Name-3",
                category="Category-3",
                parent_collection=self.collection,
                child_type=CollectionItem.Types.ARTIFACT,
                collection=collection,
                created_by_user=self.user,
            )

    def test_collection_constraint_are_valid_or_raise_integrity_error(self):
        """Verify constraints specific to a COLLECTION type CollectionItem."""
        CollectionItem.objects.create(
            name="Name",
            category="Category",
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=self.artifact,
            removed_at=timezone.now(),
            created_by_user=self.user,
        )

        collection = Collection.objects.create(
            name="Collection-2", category="Category-2", workspace=self.workspace
        )

        CollectionItem.objects.create(
            name="Name-2",
            category="Category-2",
            parent_collection=self.collection,
            child_type=CollectionItem.Types.COLLECTION,
            collection=collection,
            removed_at=timezone.now(),
            created_by_user=self.user,
        )

    def test_collection_type_item_constraints(self):
        """Verify constraints specific to a Collection type CollectionItem."""
        collection = Collection.objects.create(
            name="Collection-2", category="Category-2", workspace=self.workspace
        )

        # CollectionItem of type COLLECTION with a collection
        CollectionItem.objects.create(
            name="Name",
            category="Category",
            collection=collection,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.COLLECTION,
            created_by_user=self.user,
        )

        # CollectionItem of type COLLECTION without a collection (collection
        # was removed)
        CollectionItem.objects.create(
            name="Name-2",
            category="Category-2",
            parent_collection=self.collection,
            child_type=CollectionItem.Types.COLLECTION,
            created_by_user=self.user,
            removed_at=timezone.now(),
        )

        # CollectionItem of type COLLECTION with an artifact
        with self.assertRaisesRegex(
            IntegrityError, "db_collectionitem_childtype_removedat_consistent"
        ):
            CollectionItem.objects.create(
                name="Name-3",
                category="Category-3",
                parent_collection=self.collection,
                child_type=CollectionItem.Types.COLLECTION,
                artifact=self.artifact,
                created_by_user=self.user,
            )

    def test_bare_collection_item_constraints(self):
        """Verify constraints specific to a Bare type CollectionItem."""
        CollectionItem.objects.create(
            name="Name",
            category="Bare",
            parent_collection=self.collection,
            child_type=CollectionItem.Types.BARE,
            created_by_user=self.user,
        )

        with self.assertRaisesRegex(
            IntegrityError, "db_collectionitem_childtype_removedat_consistent"
        ):
            CollectionItem.objects.create(
                name="Name",
                category="Category",
                artifact=self.artifact,
                parent_collection=self.collection,
                child_type=CollectionItem.Types.BARE,
                created_by_user=self.user,
            )

    def test_debian_environments_no_more_than_one_codename_architecture(self):
        """Cannot create more than one duplicated active debian:environments."""
        category = CollectionCategory.ENVIRONMENTS
        data = {"codename": "bookworm", "architecture": "amd64"}

        artifact_1, _ = self.create_artifact(category=category)
        CollectionItem.objects.create(
            name="bookworm-amd64",
            category=category,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact_1,
            created_by_user=self.user,
            data=data,
            removed_at=timezone.now(),
        )

        # Can be created because the previous one is removed
        artifact_2, _ = self.create_artifact(category=category)
        CollectionItem.objects.create(
            name="bookworm-amd64-2",
            category=category,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact_2,
            created_by_user=self.user,
            data=data,
        )

        # Can be created because it's different variant
        artifact_3, _ = self.create_artifact(category=category)
        CollectionItem.objects.create(
            name="bookworm-amd64-3",
            category=category,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact_3,
            created_by_user=self.user,
            data={**data, "variant": "buildd"},
        )

        # Can be created because it's different backend
        artifact_4, _ = self.create_artifact(category=category)
        CollectionItem.objects.create(
            name="bookworm-amd64-4",
            category=category,
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact_4,
            created_by_user=self.user,
            data={**data, "backend": "unshare"},
        )

        # Can be created: duplicated but in a different collection
        artifact_5, _ = self.create_artifact(category=category)
        CollectionItem.objects.create(
            name="bookworm-amd64-5",
            category=category,
            parent_collection=Collection.objects.create(
                name="Testing",
                category=CollectionCategory.ENVIRONMENTS,
                workspace=self.workspace,
            ),
            child_type=CollectionItem.Types.ARTIFACT,
            artifact=artifact_5,
            created_by_user=self.user,
            data={**data, "variant": "buildd"},
        )

        # Cannot be created because already one active artifact
        # with the same data in the same collection (from artifact_3).
        artifact_6, _ = self.create_artifact(category=category)
        msg = "db_collectionitem_unique_debian_environment"
        with self.assertRaisesRegex(IntegrityError, msg):
            CollectionItem.objects.create(
                name="bookworm-amd64-6",
                category=category,
                parent_collection=self.collection,
                child_type=CollectionItem.Types.ARTIFACT,
                artifact=artifact_6,
                created_by_user=self.user,
                data={**data, "variant": "buildd"},
            )

    def test_str_collection_item_artifact(self):
        """Stringification is correct for CollectionItem for ARTIFACT."""
        name = "Name of the item"
        category = ArtifactCategory.SOURCE_PACKAGE
        artifact_item = CollectionItem.objects.create(
            name=name,
            category=category,
            artifact=self.artifact,
            child_type=CollectionItem.Types.ARTIFACT,
            parent_collection=self.collection,
            created_by_user=self.user,
        )
        self.assertEqual(
            str(artifact_item),
            f"Id: {artifact_item.id} Name: {name} "
            f"Parent collection id: {self.collection.id} "
            f"Child type: {artifact_item.child_type} "
            f"Artifact id: {self.artifact.id}",
        )

    def test_str_collection_item_collection(self):
        """Stringification contains collection id.."""
        collection = Collection.objects.create(
            name="Collection-2", category="Category-2", workspace=self.workspace
        )

        collection_item = CollectionItem.objects.create(
            name="Name of the item",
            category=ArtifactCategory.SOURCE_PACKAGE,
            collection=collection,
            child_type=CollectionItem.Types.COLLECTION,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        self.assertIn(f"Collection id: {collection.id}", str(collection_item))

    def test_str_collection_item_bare(self):
        """Stringification does not contain collection/artifact id."""
        collection_item = CollectionItem.objects.create(
            name="Name of the item",
            category=ArtifactCategory.SOURCE_PACKAGE,
            child_type=CollectionItem.Types.BARE,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        self.assertNotIn("Collection id:", str(collection_item))
        self.assertNotIn("Artifact id:", str(collection_item))

    def test_get_absolute_url(self):
        """Test get_absolute_url results."""
        item = CollectionItem.objects.create(
            name="Name of the item",
            category=ArtifactCategory.SOURCE_PACKAGE,
            child_type=CollectionItem.Types.BARE,
            parent_collection=self.collection,
            created_by_user=self.user,
        )

        self.assertEqual(
            item.get_absolute_url(),
            reverse(
                "workspaces:collections:item_detail",
                kwargs={
                    "wname": self.workspace.name,
                    "ccat": self.collection.category,
                    "cname": self.collection.name,
                    "iid": item.pk,
                    "iname": item.name,
                },
            ),
        )

    def test_expand_variables(self):
        """Variables are correctly expanded, with error handling."""
        variables = {
            "$package": "deb_fields.Package",
            "$version": "deb_fields.Version",
            "constant": "value",
        }
        artifact = Artifact()

        artifact.data = {
            "deb_fields": {
                "Package": "hello",
                "Version": "2.10-3",
                "Architecture": "any",
            }
        }
        self.assertEqual(
            CollectionItem.expand_variables(variables, artifact),
            {"package": "hello", "version": "2.10-3", "constant": "value"},
        )

        variables = {
            "$package": "deb_fields.*",
            "$version": "deb_fields.Version",
        }
        with self.assertRaisesRegex(ValueError, "Too many values expanding"):
            CollectionItem.expand_variables(variables, artifact)

        variables = {
            "$package": "]",
        }
        with self.assertRaisesRegex(ValueError, "Parse error"):
            CollectionItem.expand_variables(variables, artifact)

        variables = {"$package": "deb_fields.Package", "package": "constant"}
        with self.assertRaisesRegex(
            ValueError, r"Cannot set both '\$package' and 'package' variables"
        ):
            CollectionItem.expand_variables(variables, artifact)

        variables = {"$version": "deb_fields.Version"}
        artifact.data = {}
        with self.assertRaises(KeyError):
            CollectionItem.expand_variables(variables, artifact)

    def test_expand_name(self):
        """Item name is correctly expanded."""
        item_template = "{package}_{version}"
        expanded_variables = {"package": "hello", "version": "2.10-3"}

        self.assertEqual(
            CollectionItem.expand_name(item_template, expanded_variables),
            "hello_2.10-3",
        )
