# 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 DebianSuiteSigningKeysManager."""

import base64
from datetime import datetime
from typing import Any

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone

from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    KeyPurpose,
)
from debusine.db.models import (
    Artifact,
    Collection,
    CollectionItem,
    default_workspace,
)
from debusine.server.collections import (
    DebianSuiteSigningKeysManager,
    ItemAdditionError,
    ItemRemovalError,
)
from debusine.test import TestHelpersMixin


class DebianSuiteSigningKeysManagerTests(TestHelpersMixin, TestCase):
    """Tests for DebianSuiteSigningKeysManager."""

    def setUp(self) -> None:
        """Set up tests."""
        self.user = get_user_model().objects.create_user(
            username="John", email="john@example.org"
        )

        self.workspace = default_workspace()

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

        self.manager = DebianSuiteSigningKeysManager(collection=self.collection)

    def create_signing_key(
        self, purpose: KeyPurpose, fingerprint: str
    ) -> Artifact:
        """Create a minimal `debusine:signing-key` artifact."""
        artifact, _ = self.create_artifact(
            category=ArtifactCategory.SIGNING_KEY,
            data={
                "purpose": purpose,
                "fingerprint": fingerprint,
                "public_key": base64.b64encode(b"a public key").decode(),
            },
        )
        return artifact

    def test_init_wrong_collection_category_raise_value_error(self) -> None:
        """Init raises `ValueError`: wrong collection category."""
        category = "debian:something-else"
        collection = Collection.objects.create(
            name="Name is not used",
            category=category,
            workspace=self.workspace,
        )

        msg = (
            f'^DebianSuiteSigningKeysManager cannot manage "{category}" '
            f'category$'
        )

        with self.assertRaisesRegex(ValueError, msg):
            DebianSuiteSigningKeysManager(collection)

    def test_do_add_artifact(self) -> None:
        """`do_add_artifact` adds the artifact."""
        signing_key_artifact = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )

        collection_item = self.manager.add_artifact(
            signing_key_artifact, user=self.user
        )

        collection_item.refresh_from_db()
        self.assertEqual(collection_item.name, "uefi")
        self.assertEqual(collection_item.data, {"purpose": "uefi"})

    def test_do_add_artifact_with_source_package_name(self) -> None:
        """`do_add_artifact` adds an artifact with a source package name."""
        signing_key_artifact = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )

        collection_item = self.manager.add_artifact(
            signing_key_artifact,
            user=self.user,
            variables={"source_package_name": "grub2"},
        )

        collection_item.refresh_from_db()
        self.assertEqual(collection_item.name, "uefi_grub2")
        self.assertEqual(
            collection_item.data,
            {"purpose": "uefi", "source_package_name": "grub2"},
        )

    def test_do_add_artifact_raise_item_addition_error(self) -> None:
        """`do_add_artifact` raises error: duplicated CollectionItem data."""
        signing_key_artifact_1 = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )
        signing_key_artifact_2 = self.create_signing_key(
            KeyPurpose.UEFI, "1" * 64
        )

        self.manager.add_artifact(signing_key_artifact_1, user=self.user)

        with self.assertRaisesRegex(
            ItemAdditionError, "db_collectionitem_unique_active_name"
        ):
            self.manager.add_artifact(signing_key_artifact_2, user=self.user)

    def test_do_add_artifact_replace(self):
        """`do_add_artifact` can replace an existing artifact."""
        signing_key_artifact_1 = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )
        collection_item_1 = self.manager.add_artifact(
            signing_key_artifact_1, user=self.user
        )
        signing_key_artifact_2 = self.create_signing_key(
            KeyPurpose.UEFI, "1" * 64
        )

        collection_item_2 = self.manager.add_artifact(
            signing_key_artifact_2, user=self.user, replace=True
        )

        collection_item_1.refresh_from_db()
        self.assertEqual(collection_item_1.artifact, signing_key_artifact_1)
        self.assertEqual(collection_item_1.removed_by_user, self.user)
        self.assertIsNotNone(collection_item_1.removed_at)
        self.assertEqual(collection_item_2.name, "uefi")
        self.assertEqual(collection_item_2.artifact, signing_key_artifact_2)
        self.assertIsNone(collection_item_2.removed_at)

    def test_do_add_artifact_replace_nonexistent(self):
        """Replacing a nonexistent artifact is allowed."""
        signing_key_artifact = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )

        collection_item = self.manager.add_artifact(
            signing_key_artifact, user=self.user, replace=True
        )

        self.assertEqual(collection_item.name, "uefi")
        self.assertEqual(collection_item.artifact, signing_key_artifact)

    def test_do_remove_artifact(self) -> None:
        """`do_remove_artifact` removes the artifact."""
        signing_key_artifact = self.create_signing_key(
            KeyPurpose.UEFI, "0" * 64
        )
        collection_item = self.manager.add_artifact(
            signing_key_artifact, user=self.user
        )

        # Test removing the artifact from the collection
        self.manager.remove_artifact(signing_key_artifact, user=self.user)

        collection_item.refresh_from_db()

        # The artifact is not removed yet (retention period applies)
        self.assertEqual(collection_item.artifact, signing_key_artifact)

        self.assertEqual(collection_item.removed_by_user, self.user)
        self.assertIsInstance(collection_item.removed_at, datetime)

    def test_do_add_collection_raise_item_addition_error(self) -> None:
        """
        `do_add_collection` raises `ItemAdditionError`.

        No Collections can be added to the debian:suite-signing-keys
        collection.
        """
        msg = (
            f'^Cannot add collections into '
            f'"{self.manager.COLLECTION_CATEGORY}"$'
        )
        collection = Collection.objects.create(
            name="Some-collection",
            category="Some category",
            workspace=self.workspace,
        )

        with self.assertRaisesRegex(ItemAdditionError, msg):
            self.manager.do_add_collection(collection, user=self.user)

    def test_do_remove_collection_raise_item_removal_error(self) -> None:
        """
        `do_remove_collection` raises `ItemRemovalError`.

        No Collections can be removed from the debian:suite-signing-keys
        collection.
        """
        msg = (
            f'^Cannot remove collections from '
            f'"{self.manager.COLLECTION_CATEGORY}"$'
        )
        collection = Collection.objects.create(
            name="Some-collection",
            category="Some category",
            workspace=self.workspace,
        )

        with self.assertRaisesRegex(ItemRemovalError, msg):
            self.manager.do_remove_collection(collection, user=self.user)

    def test_lookup_unexpected_format_raise_lookup_error(self) -> None:
        """`lookup` raises `LookupError`: invalid format."""
        msg = '^Unexpected lookup format: "foo:bar"$'

        with self.assertRaisesRegex(LookupError, msg):
            self.manager.lookup("foo:bar")

    def test_lookup_return_none(self) -> None:
        """`lookup` returns None if there are no matches."""
        self.assertIsNone(self.manager.lookup("key:uefi"))

    def test_lookup_return_matching_collection_item(self) -> None:
        """`lookup` returns a matching collection item."""
        items: list[CollectionItem] = []
        for purpose, fingerprint, source_package_name in (
            (KeyPurpose.UEFI, "0" * 64, None),
            (KeyPurpose.UEFI, "1" * 64, "grub2"),
            (KeyPurpose.UEFI, "2" * 64, "linux"),
        ):
            signing_key_artifact = self.create_signing_key(purpose, fingerprint)
            variables: dict[str, Any] | None = None
            if source_package_name is not None:
                variables = {"source_package_name": source_package_name}
            items.append(
                self.manager.add_artifact(
                    signing_key_artifact, user=self.user, variables=variables
                )
            )

        signing_key_artifact = self.create_signing_key(
            KeyPurpose.UEFI, "3" * 64
        )
        items.append(
            self.manager.add_artifact(
                signing_key_artifact,
                user=self.user,
                variables={"source_package_name": "systemd"},
            )
        )
        items[-1].removed_at = timezone.now()
        items[-1].save()

        # CollectionItem of type BARE should not exist in this collection
        # (the manager does not allow adding it).  Add one to ensure that it
        # is filtered out.
        CollectionItem.objects.create(
            child_type=CollectionItem.Types.BARE,
            created_by_user=self.user,
            parent_collection=self.collection,
            category=ArtifactCategory.SIGNING_KEY,
            name="something",
            data={"purpose": "uefi"},
        )

        self.assertEqual(self.manager.lookup("key:uefi"), items[0])
        self.assertEqual(self.manager.lookup("key:uefi_grub2"), items[1])
        self.assertEqual(self.manager.lookup("key:uefi_linux"), items[2])

        # Fall back to an item with no source package name constraints.
        self.assertEqual(self.manager.lookup("key:uefi_base-files"), items[0])

        # items[3] would match, but has been removed.  Fall back to an item
        # with no source package name constraints.
        self.assertEqual(self.manager.lookup("key:uefi_systemd"), items[0])

        self.assertIsNone(self.manager.lookup("key:kmod"))

        self.assertEqual(self.manager.lookup("name:uefi"), items[0])
        self.assertEqual(self.manager.lookup("name:uefi_grub2"), items[1])
        self.assertEqual(self.manager.lookup("name:uefi_linux"), items[2])
        self.assertIsNone(self.manager.lookup("name:uefi_base-files"))
        self.assertIsNone(self.manager.lookup("name:uefi_systemd"))
        self.assertIsNone(self.manager.lookup("name:kmod"))
