# Copyright 2019, 2021-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 models."""

from datetime import timedelta
from pathlib import Path
from typing import ClassVar

import django.db.utils
from django.conf import settings
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils import timezone

from debusine.artifacts.local_artifact import WorkRequestDebugLogs
from debusine.artifacts.models import ArtifactCategory, EmptyArtifactData
from debusine.db.context import context
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    File,
    FileInArtifact,
    FileInStore,
    FileStore,
    FileUpload,
    NotificationChannel,
    Workspace,
    default_workspace,
)
from debusine.db.tests.utils import _calculate_hash_from_data
from debusine.server.file_backend.external import ExternalDebianSuiteFileBackend
from debusine.server.file_backend.local import LocalFileBackend
from debusine.server.file_backend.memory import MemoryFileBackend
from debusine.test.django import TestCase
from debusine.test.utils import data_generator


class FileManagerTests(TestCase):
    """Unit tests for the ``FileManager`` class."""

    def setUp(self) -> None:
        """Set up File to be used in the tests."""
        self.file_contents = b"test"
        self.file_hash = _calculate_hash_from_data(self.file_contents)

        self.file = self.create_file(self.file_contents)

    def test_get_or_create_not_created(self) -> None:
        """
        File.objects.get_or_create returns created=False.

        File.objects.get_or_create() tried to create a file with the same
        file_hash and size as one already existing.
        """
        fileobj, created = File.objects.get_or_create(
            hash_digest=self.file_hash, size=len(self.file_contents)
        )

        self.assertEqual(self.file, fileobj)
        self.assertFalse(created)

    def test_get_or_create_created(self) -> None:
        """
        File.objects.get_or_create returns created=True.

        File.objects.get_or_create() created a new File (file_hash and size
        did not exist).
        """
        file_contents = self.file_contents + b"-new-contents"
        hash_digest = _calculate_hash_from_data(file_contents)
        size = len(file_contents)

        fileobj, created = File.objects.get_or_create(
            hash_digest=hash_digest, size=size
        )

        self.assertIsInstance(fileobj, File)
        self.assertEqual(fileobj.hash_digest, hash_digest)
        self.assertEqual(fileobj.size, size)
        self.assertTrue(created)


class FileTests(TestCase):
    """Tests for the File class."""

    def setUp(self) -> None:
        """Set up File to be used in the tests."""
        self.file_contents = b"test"
        self.file_hash = _calculate_hash_from_data(self.file_contents)

        self.file = self.create_file(self.file_contents)

    def test_sha256(self) -> None:
        """File.sha256 have the expected hash."""
        self.assertEqual(self.file.sha256, self.file_hash)
        self.assertEqual(self.file.sha256.hex(), self.file_hash.hex())

    def test_hash_digest_getter(self) -> None:
        """File.hash_digest return the expected hash digest."""
        self.assertIsInstance(self.file.hash_digest, bytes)
        self.assertEqual(self.file.hash_digest, self.file_hash)
        self.assertEqual(self.file.hash_digest.hex(), self.file_hash.hex())

    def test_hash_digest_setter(self) -> None:
        """File.hash_digest setter sets the hash digest."""
        the_hash = _calculate_hash_from_data(b"some data")
        self.file.hash_digest = the_hash

        self.file.save()
        self.file.refresh_from_db()

        self.assertEqual(self.file.hash_digest, the_hash)

    def test_calculate_hash(self) -> None:
        """calculate_hash returns the expected hash digest."""
        file_contents = b"testing"
        local_file = self.create_temporary_file(contents=file_contents)
        expected_hash = _calculate_hash_from_data(file_contents)

        self.assertEqual(
            self.file.calculate_hash(
                local_file,
            ),
            expected_hash,
        )

    def test_str(self) -> None:
        """__str__() return sha256 and size."""
        self.assertEqual(
            str(self.file),
            f"id: {self.file.id} "
            f"sha256: {self.file_hash.hex()} "
            f"size: {len(self.file_contents)}",
        )

    def test_unique_constraint_hash_size(self) -> None:
        """File with same hash_digest and size cannot be created."""
        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_file_unique_sha256_size"
        ):
            File.objects.create(
                **{
                    File.current_hash_algorithm: self.file.hash_digest,
                    "size": self.file.size,
                }
            )

    def test_hash_can_be_duplicated(self) -> None:
        """File with the same hash_digest and different size can be created."""
        file_existing = File.objects.earliest("id")
        file_new, _ = File.objects.get_or_create(
            hash_digest=file_existing.hash_digest, size=file_existing.size + 10
        )
        self.assertIsNotNone(file_new.id)
        self.assertNotEqual(file_new.id, file_existing.id)

    def test_constraint_hash_digest_not_empty(self) -> None:
        """File.hash_digest cannot be empty."""
        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_file_sha256_not_empty"
        ):
            File.objects.get_or_create(hash_digest=b"", size=5)


class FileStoreTests(TestCase):
    """Tests for the FileStore class."""

    def setUp(self) -> None:
        """Set up FileStore to be used in the tests."""
        self.backend = FileStore.BackendChoices.LOCAL
        self.file_store_name = "nas-01"

        self.file_store = FileStore.objects.create(
            name=self.file_store_name,
            backend=self.backend,
        )

    def test_default_values(self) -> None:
        """Test default values."""
        file_store = FileStore.objects.create(
            name="nas-02", backend=FileStore.BackendChoices.LOCAL
        )
        self.assertEqual(file_store.configuration, {})
        file_store.clean_fields()
        file_store.save()

    def test_backend_choice(self) -> None:
        """Assert FileStore.BackendChoices is correctly accessed."""
        self.assertEqual(self.file_store.backend, self.backend)

    def test_configuration_validation_error(self) -> None:
        """`ValidationError` is raised if configuration is invalid."""
        file_store = FileStore.objects.create(
            name="test",
            backend=FileStore.BackendChoices.LOCAL,
            configuration=[],
        )
        with self.assertRaises(ValidationError) as raised:
            file_store.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"configuration": ["configuration must be a dictionary"]},
        )

        file_store.configuration = {"nonexistent": ""}
        with self.assertRaises(ValidationError) as raised:
            file_store.full_clean()
        messages = raised.exception.message_dict
        self.assertCountEqual(messages.keys(), ["configuration"])
        self.assertRegex(
            messages["configuration"][0],
            r"(?s)invalid file store configuration:.*"
            r"extra fields not permitted",
        )

    def test_files_through(self) -> None:
        """FileStore.files() return the expected file."""
        # Create a new file
        file = self.create_file()

        # Add the file in the store
        FileInStore.objects.create(file=file, store=self.file_store, data={})

        # Assert that self.file_store through model return the expected file
        self.assertEqual(self.file_store.files.first(), file)

    def test_get_backend_object(self) -> None:
        """get_backend_object instantiates the right backend."""
        fs_local = FileStore(
            name="local",
            backend=FileStore.BackendChoices.LOCAL,
            configuration={"base_directory": "/tmp"},
        )
        fs_memory = FileStore(
            name="memory",
            backend=FileStore.BackendChoices.MEMORY,
            configuration={"name": "test"},
        )
        fs_external = FileStore(
            name="external",
            backend=FileStore.BackendChoices.EXTERNAL_DEBIAN_SUITE,
            configuration={
                "archive_root_url": "https://deb.debian.org/debian",
                "suite": "bookworm",
                "components": ["main"],
            },
        )

        self.assertIsInstance(fs_local.get_backend_object(), LocalFileBackend)
        self.assertIsInstance(fs_memory.get_backend_object(), MemoryFileBackend)
        self.assertIsInstance(
            fs_external.get_backend_object(), ExternalDebianSuiteFileBackend
        )

    def test_default(self) -> None:
        """default() returns FileStore with name=="Default"."""
        self.assertEqual(FileStore.default().name, "Default")

    def test_str(self) -> None:
        """__str__() return the correct information."""
        self.assertEqual(
            self.file_store.__str__(),
            f"Id: {self.file_store.id} "
            f"Name: {self.file_store.name} "
            f"Backend: {self.file_store.backend}",
        )


class FileInStoreTests(TestCase):
    """Tests for the FileInStore class."""

    def setUp(self) -> None:
        """Set up FileInStore to be used in the tests."""
        self.file_store_name = "nas-01"
        self.file_store = FileStore.objects.create(
            name="nas-01",
            backend=FileStore.BackendChoices.LOCAL,
            configuration={},
        )

        file_contents = b"test"

        self.file = self.create_file(file_contents)
        self.file_in_store = FileInStore(
            store=self.file_store,
            file=self.file,
        )

    def test_default_values(self) -> None:
        """Test default values."""
        # Delete all FileInStore to create a new one, in this test,
        # reusing self.file_store and self.file
        FileInStore.objects.all().delete()
        file_in_store = FileInStore(store=self.file_store, file=self.file)

        self.assertEqual(file_in_store.data, {})
        file_in_store.clean_fields()
        file_in_store.save()

    def test_str(self) -> None:
        """__str__() return the correct string."""
        self.assertEqual(
            self.file_in_store.__str__(),
            f"Id: {self.file_in_store.id} "
            f"Store: {self.file_in_store.store.name} "
            f"File: {self.file_in_store.file.hash_digest.hex()}",
        )


class ArtifactManagerTests(TestCase):
    """Tests for the ArtifactManager class."""

    workspace: ClassVar[Workspace]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        with context.disable_permission_checks():
            cls.workspace = cls.playground.create_workspace(name="test")

    def test_create_from_local_artifact(self) -> None:
        """create_from_local_artifact() creates a matching Artifact."""
        temp_dir = self.create_temporary_directory()
        paths = [temp_dir / "1.log", temp_dir / "2.log"]
        paths[0].write_text("1\n")
        paths[1].write_text("2\n")
        local_artifact = WorkRequestDebugLogs.create(files=paths)
        work_request = self.playground.create_work_request()

        artifact = Artifact.objects.create_from_local_artifact(
            local_artifact, self.workspace, created_by_work_request=work_request
        )

        self.assertEqual(artifact.category, local_artifact.category)
        self.assertEqual(artifact.workspace, self.workspace)
        self.assertEqual(artifact.data, local_artifact.data)
        self.assertEqual(artifact.created_by_work_request, work_request)
        self.assertEqual(artifact.fileinartifact_set.count(), 2)
        file_store = self.workspace.default_file_store.get_backend_object()
        for file_in_artifact, path in zip(
            artifact.fileinartifact_set.order_by("id"), paths
        ):
            self.assertEqual(file_in_artifact.path, path.name)
            with file_store.get_stream(file_in_artifact.file) as file:
                self.assertEqual(file.read(), path.read_bytes())

    def test_not_expired_return_not_expired_artifacts(self) -> None:
        """not_expired() return only the not expired artifacts."""
        artifact_1 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_1.created_at = timezone.now() - timedelta(days=2)
        artifact_1.save()
        artifact_2 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_3 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(0)
        )

        self.assertQuerySetEqual(
            Artifact.objects.not_expired(timezone.now()),
            {artifact_2, artifact_3},
            ordered=False,
        )

    def test_expired_return_expired_artifacts(self) -> None:
        """expired() return only the expired artifacts."""
        artifact_1 = Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        artifact_1.created_at = timezone.now() - timedelta(days=2)
        artifact_1.save()

        Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(days=1)
        )
        Artifact.objects.create(
            workspace=self.workspace, expiration_delay=timedelta(0)
        )

        self.assertQuerySetEqual(
            Artifact.objects.expired(timezone.now()), {artifact_1}
        )


class ArtifactTests(TestCase):
    """Tests for the Artifact class."""

    def test_default_values_of_fields(self) -> None:
        """Define the fields of the models and test their default values."""
        artifact = Artifact()

        self.assertEqual(artifact.category, "")

        artifact.category = "sample-type"

        self.assertEqual(artifact.data, {})

        with context.disable_permission_checks():
            artifact.workspace = self.playground.create_workspace(name="test")

        artifact.clean_fields()

        date_time_before_save = timezone.now()

        artifact.save()

        # date_time_before_save <= artifact.created_at <= timezone.now()
        self.assertLessEqual(date_time_before_save, artifact.created_at)
        self.assertLessEqual(artifact.created_at, timezone.now())

        # By default, the expiration_delay is None
        self.assertIsNone(artifact.expiration_delay)

        # By default, the created_by_work_request is None
        self.assertIsNone(artifact.created_by_work_request)

        # By default, the created_by is None
        self.assertIsNone(artifact.created_by)

    def test_expired(self) -> None:
        """Test Artifact.expired() method."""
        artifact = Artifact()

        artifact.created_at = timezone.now() - timedelta(days=2)
        artifact.expiration_delay = timedelta(days=1)
        self.assertTrue(artifact.expired(timezone.now()))

        artifact.expiration_delay = timedelta(0)
        self.assertFalse(artifact.expired(timezone.now()))

        artifact.created_at = timezone.now()
        artifact.expiration_delay = timedelta(days=1)
        self.assertFalse(artifact.expired(timezone.now()))

    def test_default_expiration(self) -> None:
        """Test workspace's default_expiration_delay."""
        default_ws = default_workspace()
        self.assertEqual(default_ws.default_expiration_delay, timedelta(0))
        artifact = Artifact.objects.create(workspace=default_ws)
        self.assertIsNone(artifact.expiration_delay)

        with context.disable_permission_checks():
            expiring_ws = self.playground.create_workspace(name="test")
        expiring_ws.default_expiration_delay = timedelta(days=7)
        test_creation_time = timezone.now()
        artifact = Artifact.objects.create(workspace=expiring_ws)
        delta = expiring_ws.default_expiration_delay
        assert artifact.expire_at is not None
        self.assertEqual(
            (artifact.expire_at - test_creation_time).days, delta.days
        )

    def test_str(self) -> None:
        """Test for Artifact.__str__."""
        artifact = Artifact.objects.create(workspace=default_workspace())
        self.assertEqual(
            artifact.__str__(),
            f"Id: {artifact.id} "
            f"Category: {artifact.category} "
            f"Workspace: {artifact.workspace.id}",
        )

    def test_get_absolute_url(self) -> None:
        """Test for Artifact.get_absolute_url."""
        artifact = Artifact.objects.create(workspace=default_workspace())
        self.assertEqual(
            artifact.get_absolute_url(),
            reverse("artifacts:detail", kwargs={"artifact_id": artifact.pk}),
        )

    def test_data_validation_error(self) -> None:
        """Assert that ValidationError is raised if data is not valid."""
        default_ws = default_workspace()

        artifact = Artifact.objects.create(
            category=ArtifactCategory.WORK_REQUEST_DEBUG_LOGS,
            workspace=default_ws,
            data=[],
        )
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"data": ["data must be a dictionary"]},
        )

        artifact.category = "missing:does-not-exist"
        artifact.data = {}
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        self.assertEqual(
            raised.exception.message_dict,
            {"category": ["missing:does-not-exist: invalid artifact category"]},
        )

        artifact.category = ArtifactCategory.PACKAGE_BUILD_LOG
        with self.assertRaises(ValidationError) as raised:
            artifact.full_clean()
        messages = raised.exception.message_dict
        self.assertCountEqual(messages.keys(), ["data"])
        self.assertRegex(
            messages["data"][0],
            r"invalid artifact data:"
            r" 3 validation errors for DebianPackageBuildLog\n",
        )

    def test_get_label_from_data(self) -> None:
        """Test getting artifact label from its data."""
        artifact = Artifact.objects.create(
            category=ArtifactCategory.SOURCE_PACKAGE,
            workspace=self.playground.get_default_workspace(),
            data={
                "name": "test",
                "version": "1.0-1",
                "type": "dpkg",
                "dsc_fields": {},
            },
        )
        self.assertEqual(artifact.get_label(), "test_1.0-1")

    def test_get_label_from_category(self) -> None:
        """Test defaults for artifact label."""
        artifact = Artifact.objects.create(
            category=ArtifactCategory.WORK_REQUEST_DEBUG_LOGS,
            workspace=self.playground.get_default_workspace(),
            data={},
        )
        self.assertEqual(
            artifact.get_label(), "debusine:work-request-debug-logs"
        )

    def test_get_label_from_data_reuse_model(self) -> None:
        """Test getting artifact label reusing the model instance."""
        artifact = Artifact.objects.create(
            category=ArtifactCategory.SOURCE_PACKAGE,
            workspace=self.playground.get_default_workspace(),
            data={
                "name": "test",
                "version": "1.0-1",
                "type": "dpkg",
                "dsc_fields": {},
            },
        )
        self.assertEqual(
            artifact.get_label(EmptyArtifactData()), "debian:source-package"
        )


class FileInArtifactTests(TestCase):
    """Tests for the FileInArtifact class."""

    def create_file_in_artifact(
        self,
        fileobj: File | None = None,
        artifact: Artifact | None = None,
        path: str | None = None,
    ) -> FileInArtifact:
        """Return FileInArtifact to be used in tests."""
        if fileobj is None:
            fileobj = self.create_file()

        if artifact is None:
            artifact, _ = self.create_artifact()

        if path is None:
            path = "/usr/bin/test"

        return FileInArtifact.objects.create(
            artifact=artifact, file=fileobj, path=path
        )

    def test_artifact_path_unique_constraint(self) -> None:
        """Test two FileInArtifact cannot have the same artifact and path."""
        artifact, _ = self.create_artifact()

        file1 = self.create_file(b"contents1")
        file2 = self.create_file(b"contents2")

        self.create_file_in_artifact(file1, artifact, "/usr/bin/test")

        with self.assertRaisesRegex(
            django.db.utils.IntegrityError,
            "db_fileinartifact_unique_artifact_path",
        ):
            self.create_file_in_artifact(file2, artifact, "/usr/bin/test")

    def test_str(self) -> None:
        """Test FileInArtifact.__str__."""
        file_in_artifact = self.create_file_in_artifact()

        self.assertEqual(
            file_in_artifact.__str__(),
            f"Id: {file_in_artifact.id} "
            f"Artifact: {file_in_artifact.artifact.id} "
            f"Path: {file_in_artifact.path} "
            f"File: {file_in_artifact.file.id}",
        )


class FileUploadTests(TestCase):
    """Tests for FileUpload class."""

    def setUp(self) -> None:
        """Set up basic objects for the tests."""
        self.file_upload = self.create_file_upload()
        self.artifact = self.file_upload.file_in_artifact.artifact
        self.workspace = self.artifact.workspace
        self.file_path = self.file_upload.file_in_artifact.path
        self.file_size = self.file_upload.file_in_artifact.file.size

    def test_current_size_raise_no_file_in_artifact(self) -> None:
        """Test current_size() raise ValueError (FileInArtifact not found)."""
        wrong_path = "no-exist"
        with self.assertRaisesRegex(
            ValueError,
            f'^No FileInArtifact for Artifact {self.artifact.id} '
            f'and path "{wrong_path}"$',
        ):
            FileUpload.current_size(self.artifact, wrong_path)

    def test_current_size_raise_no_fileupload_for_file_in_artifact(
        self,
    ) -> None:
        """Test current_size() raise ValueError (FileUpload not found)."""
        artifact = Artifact.objects.create(
            category="test", workspace=self.workspace
        )
        file_in_artifact = FileInArtifact.objects.create(
            artifact=artifact, path="something", file=self.create_file()
        )

        with self.assertRaisesRegex(
            ValueError,
            f"^No FileUpload for FileInArtifact {file_in_artifact.id}$",
        ):
            FileUpload.current_size(artifact, "something")

    def test_current_size_return_last_position_received(self) -> None:
        """Test current_size() method return size of the file."""
        write_to_position = 30
        self.file_upload.absolute_file_path().write_bytes(
            next(data_generator(write_to_position))
        )
        self.assertEqual(
            FileUpload.current_size(
                artifact=self.artifact, path_in_artifact=self.file_path
            ),
            write_to_position,
        )

    def test_no_two_fileobj_to_same_path(self) -> None:
        """Test cannot create two FileUpload with same path."""
        data = next(data_generator(self.file_size))
        file = self.create_file(data)
        new_file_in_artifact = FileInArtifact.objects.create(
            artifact=self.artifact, path="README-2", file=file
        )

        with self.assertRaisesRegex(
            django.db.utils.IntegrityError, "db_fileupload_path_key"
        ):
            FileUpload.objects.create(
                file_in_artifact=new_file_in_artifact,
                path=self.file_upload.path,
            )

    def test_delete(self) -> None:
        """Test FileUpload delete() try to unlink file."""
        file_path = self.file_upload.absolute_file_path()
        Path(file_path).write_bytes(next(data_generator(self.file_size)))

        self.assertTrue(file_path.exists())

        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            self.file_upload.delete()

        self.assertFalse(file_path.exists())

        self.assertEqual(len(callbacks), 1)

    def test_delete_file_does_not_exist(self) -> None:
        """Test FileUpload delete() try to unlink file but did not exist."""
        file_path = self.file_upload.absolute_file_path()

        self.assertFalse(Path(file_path).exists())

        with self.captureOnCommitCallbacks(execute=True) as callbacks:
            self.file_upload.delete()

        self.assertEqual(len(callbacks), 1)

    def test_absolute_file_path(self) -> None:
        """Test absolute_file_path() is in DEBUSINE_UPLOAD_DIRECTORY."""
        self.assertEqual(
            self.file_upload.absolute_file_path(),
            Path(settings.DEBUSINE_UPLOAD_DIRECTORY) / self.file_upload.path,
        )

    def test_str(self) -> None:
        """Test __str__."""
        self.assertEqual(self.file_upload.__str__(), f"{self.file_upload.id}")


class ArtifactRelationTests(FileInArtifactTests, TestCase):
    """Implement ArtifactRelation tests."""

    def setUp(self) -> None:
        """Initialize test object."""
        self.artifact_1, _ = self.create_artifact()
        self.artifact_2 = Artifact.objects.create(
            workspace=self.artifact_1.workspace,
            category=self.artifact_1.category,
        )

    def test_type_not_valid_raise_validation_error(self) -> None:
        """Assert that ValidationError is raised if type is not valid."""
        artifact_relation = ArtifactRelation.objects.create(
            artifact=self.artifact_1,
            target=self.artifact_2,
            type="wrong-type",
        )
        with self.assertRaises(ValidationError):
            artifact_relation.full_clean()

    def test_str(self) -> None:
        """Assert __str__ return expected value."""
        artifact_relation = ArtifactRelation.objects.create(
            artifact=self.artifact_1,
            target=self.artifact_2,
            type=ArtifactRelation.Relations.RELATES_TO,
        )
        self.assertEqual(
            str(artifact_relation),
            f"{self.artifact_1.id} {ArtifactRelation.Relations.RELATES_TO} "
            f"{self.artifact_2.id}",
        )


class NotificationChannelTests(TestCase):
    """Tests for NotificationChannel class."""

    def setUp(self) -> None:
        """Set up test."""
        self.email_data = {
            "from": "sender@debusine.example.org",
            "to": ["recipient@example.com"],
        }

    def test_create_email(self) -> None:
        """Create email NotificationChannel."""
        name = "deblts-email"
        method = NotificationChannel.Methods.EMAIL

        notification_channel = NotificationChannel.objects.create(
            name=name, method=method, data=self.email_data
        )

        notification_channel.refresh_from_db()

        self.assertEqual(notification_channel.name, name)
        self.assertEqual(notification_channel.method, method)
        self.assertEqual(notification_channel.data, self.email_data)

    def test_create_email_invalid_data(self) -> None:
        """Create email NotificationChannel: invalid data, failure."""
        with self.assertRaises(ValidationError):
            NotificationChannel.objects.create(
                name="deblts-email",
                method=NotificationChannel.Methods.EMAIL,
                data={"something": "something"},
            )

    def test_name_unique(self) -> None:
        """Field name is unique."""
        name = "deblts-email"
        NotificationChannel.objects.create(
            name=name,
            method=NotificationChannel.Methods.EMAIL,
            data=self.email_data,
        )

        with self.assertRaises(ValidationError):
            NotificationChannel.objects.create(
                name=name,
                method=NotificationChannel.Methods.EMAIL,
                data=self.email_data,
            )

    def test_str(self) -> None:
        """Assert __str__ return the expected string."""
        name = "deblts-email"
        notification_channel = NotificationChannel.objects.create(
            name=name,
            method=NotificationChannel.Methods.EMAIL,
            data=self.email_data,
        )

        self.assertEqual(str(notification_channel), name)
