# 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 CollectionManagerInterface."""
from typing import Any
from unittest.mock import patch

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

from debusine.db.models import (
    Artifact,
    Collection,
    CollectionItem,
    User,
    default_workspace,
)
from debusine.server.collections import (
    CollectionManagerInterface,
    DebianSuiteManager,
    ItemAdditionError,
)
from debusine.test import TestHelpersMixin


class TestInstance(CollectionManagerInterface):
    """Instance used for testing."""

    COLLECTION_CATEGORY = "debusine:testing"

    def do_add_artifact(
        self,
        artifact: Artifact,
        *,
        user: User,
        variables: dict[str, Any] | None = None,
        name: str | None = None,
        replace: bool = False,
    ) -> CollectionItem:
        """Raise NotImplementedError."""
        raise NotImplementedError()

    def do_remove_artifact(
        self,
        artifact: Artifact,
        *,
        user: User | None = None,
    ):
        """Raise NotImplementedError."""
        raise NotImplementedError()

    def do_add_collection(
        self,
        collection: Collection,
        *,
        user: User,
        variables: dict[str, Any] | None = None,
        name: str | None = None,
    ) -> CollectionItem:
        """Raise NotImplementedError."""
        raise NotImplementedError

    def do_remove_collection(
        self,
        collection: Collection,
        *,
        user: User | None = None,
    ):
        """Raise NotImplementedError."""
        raise NotImplementedError


class CollectionManagerInterfaceTests(TestHelpersMixin, TestCase):
    """Tests for CollectionManagerInterface."""

    def setUp(self):
        """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="Test Collection",
            category="debusine:testing",
            workspace=self.workspace,
        )
        self.manager = TestInstance(self.collection)

    def test_add_artifact(self):
        """Test add_artifact() call do_add_artifact()."""
        category = "debusine:something"
        artifact, _ = self.create_artifact(category=category)

        self.manager.VALID_ARTIFACT_CATEGORIES = {category}

        with patch.object(self.manager, "do_add_artifact") as mocked:
            self.manager.add_artifact(artifact, user=self.user)

        mocked.assert_called_with(
            artifact, user=self.user, variables=None, name=None, replace=False
        )

    def test_add_artifact_invalid_category_raise_item_addition_error(self):
        """Test add_artifact() raise ItemAdditionError(): invalid category."""
        category = "invalid"
        artifact, _ = self.create_artifact(category=category)

        msg = (
            f'^Artifact category "{category}" '
            f'not supported by the collection$'
        )
        self.assertRaisesRegex(
            ItemAdditionError,
            msg,
            self.manager.add_artifact,
            artifact,
            user=self.user,
        )

    def test_add_artifact_any_category_valid(self):
        """If VALID_ARTIFACT_CATEGORIES is None, any category is valid."""
        category = "debusine:anything"
        artifact, _ = self.create_artifact(category=category)
        self.manager.VALID_ARTIFACT_CATEGORIES = None

        with patch.object(self.manager, "do_add_artifact") as mocked:
            self.manager.add_artifact(artifact, user=self.user)

        mocked.assert_called_with(
            artifact, user=self.user, variables=None, name=None, replace=False
        )

    def test_add_collection(self):
        """Test add_collection() call do_add_collection()."""
        category = "debusine:something"

        self.manager.VALID_COLLECTION_CATEGORIES = {category}

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

        with patch.object(self.manager, "do_add_collection") as mocked:
            self.manager.add_collection(collection, user=self.user)

        mocked.assert_called_with(
            collection, user=self.user, variables=None, name=None
        )

    def test_add_collection_invalid_category_raise_item_addition_error(self):
        """Test add_artifact() raise ItemAdditionError(): invalid category."""
        category = "invalid"
        collection = Collection.objects.create(
            name="Some name", category=category, workspace=self.workspace
        )

        msg = (
            f'^Collection category "{category}" '
            f'not supported by the collection$'
        )
        self.assertRaisesRegex(
            ItemAdditionError,
            msg,
            self.manager.add_collection,
            collection,
            user=self.user,
        )

    def test_add_collection_any_category_valid(self):
        """If VALID_COLLECTION_CATEGORIES is None, any category is valid."""
        category = "debusine:anything"
        collection = Collection.objects.create(
            name="Some name", category=category, workspace=self.workspace
        )
        self.manager.VALID_COLLECTION_CATEGORIES = None

        with patch.object(self.manager, "do_add_collection") as mocked:
            self.manager.add_collection(collection, user=self.user)

        mocked.assert_called_with(
            collection, user=self.user, variables=None, name=None
        )

    def test_remove_artifact(self):
        """Test remove_artifact() call do_remove_artifact()."""
        artifact, _ = self.create_artifact()

        with patch.object(self.manager, "do_remove_artifact") as mocked:
            self.manager.remove_artifact(artifact, user=self.user)

        mocked.assert_called_with(artifact, user=self.user)

    def test_remove_collection(self):
        """Test remove_collection() call do_remove_collection()."""
        collection = Collection.objects.create(
            name="Testing", category="Testing", workspace=self.workspace
        )

        with patch.object(self.manager, "do_remove_collection") as mocked:
            self.manager.remove_collection(collection, user=self.user)

        mocked.assert_called_with(collection, user=self.user)

    def test_lookup_unexpected_format_raise_lookup_error(self) -> None:
        """`lookup` raises `LookupError`: invalid format."""
        with self.assertRaisesRegex(
            LookupError, '^Unexpected lookup format: "nonsense"$'
        ):
            self.manager.lookup("nonsense")

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

    def test_get_manager_for(self):
        """Test getting specialized manager."""
        collection = Collection.objects.create(
            name="Testing", category="debian:suite", workspace=self.workspace
        )
        manager = CollectionManagerInterface.get_manager_for(collection)
        self.assertEqual(type(manager), DebianSuiteManager)
