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

"""Tests for the file backend interface."""

from django.test import TestCase

from debusine.db.models import File, FileInStore, FileStore
from debusine.server.file_backend.interface import FileBackendInterface
from debusine.server.file_backend.models import FileBackendConfiguration
from debusine.test import TestHelpersMixin
from debusine.utils import NotSupportedError


class FakeFileBackend(FileBackendInterface[FileBackendConfiguration]):
    """Fake file backend to test methods in FileBackendInterface."""

    def __init__(self):
        """Initialize object."""
        super().__init__()

        self.db_store = FileStore.objects.create(
            name="Fake", backend="Fake", configuration={}
        )
        self.remove_file_calls = []

    def add_file(self, local_path, fileobj: File | None = None) -> File:
        """Add file in the store."""
        fileobj = File.from_local_path(local_path)
        FileInStore.objects.get_or_create(
            store=self.db_store, file=fileobj, data={}
        )

        return fileobj

    def get_local_path(self, fileobj):  # noqa: U100
        """Return local path of fileobj."""
        return None

    def get_url(self, fileobj):  # noqa: U100
        """Return URL of fileobj."""
        return None

    def _remove_file(self, fileobj: File):
        # Ensure that the file is NOT in the DB before remove_file is called
        # The FileBackendInterface.remove_file should have removed it from the
        # DB before deleting the file from disk
        if FileInStore.objects.filter(
            store=self.db_store, file=fileobj
        ).exists():
            raise RuntimeError(
                "Attempted to remove file from disk but still is referenced "
                "by the FileInStore model"
            )  # pragma: no cover

        self.remove_file_calls.append(fileobj)


class FileBackendInterfaceTests(TestHelpersMixin, TestCase):
    """Tests for code of FileBackendInterface."""

    def setUp(self):
        """Initialize file backend to be tested."""
        self.file_backend = FakeFileBackend()

    def test_remove_file_not_in_file_backend(self):
        """
        remove_file() call _remove_file.

        Assert that when the file in disk is deleted from the backend
        the fileobj model is not deleted yet. Debusine can have
        orphaned files in disk (not referenced by the DB) but must not have
        files in DB that don't exist in disk.
        """
        fileobj = File.objects.create(hash_digest=b"test", size=15)

        self.file_backend.remove_file(fileobj)

        self.assertEqual(self.file_backend.remove_file_calls, [fileobj])

    def test_remove_file_in_file_backend(self):
        """remove_file() call _remove_file, delete file from FileInStore()."""
        fileobj = self.create_file_in_backend(self.file_backend)

        self.assertTrue(
            FileInStore.objects.filter(
                file=fileobj, store=self.file_backend.db_store
            ).exists()
        )

        self.file_backend.remove_file(fileobj)

        self.assertFalse(
            FileInStore.objects.filter(
                file=fileobj, store=self.file_backend.db_store
            ).exists()
        )

    def test_get_stream_non_local(self):
        """get_stream() without local URL: RuntimeError."""
        file = File()

        with self.assertRaisesRegex(
            NotSupportedError,
            "^This FileBackend doesn't support streaming$",
        ):
            self.file_backend.get_stream(file).__enter__()
