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

"""Tests for MemoryFileBackend."""

from unittest import mock

from django.test import TestCase

from debusine.db.models import File, FileInStore, FileStore
from debusine.server.file_backend.memory import MemoryFileBackend
from debusine.test import TestHelpersMixin


class MemoryFileBackendTests(TestHelpersMixin, TestCase):
    """Tests for MemoryFileBackend."""

    playground_needed = False

    def setUp(self) -> None:
        """Initialize file backend to be tested."""
        super().setUp()
        self.store = FileStore.objects.create(
            name="memory",
            backend=FileStore.BackendChoices.MEMORY,
            configuration={"name": "test_memory"},
        )
        self.backend = MemoryFileBackend(self.store)

    def tearDown(self) -> None:
        """Remove test memory storage."""
        MemoryFileBackend.storages.pop("test_memory", None)
        super().tearDown()

    def test_get_local_path(self) -> None:
        """MemoryFileBackend.get_local_path returns None."""
        temp_file_path = self.create_temporary_file()
        file = self.backend.add_file(temp_file_path)
        local_path = self.backend.get_local_path(
            file
        )  # type: ignore[func-returns-value]
        self.assertIsNone(local_path)

    def test_get_url(self) -> None:
        """MemoryFileBackend.get_url return None."""
        temp_file_path = self.create_temporary_file()
        file = self.backend.add_file(temp_file_path)
        url = self.backend.get_url(file)  # type: ignore[func-returns-value]
        self.assertIsNone(url)

    def test_add_file_from_local_path_reuse_fileobj(self) -> None:
        """MemoryFileBackend.add_file reuse the fileobj."""
        temp_file_path = self.create_temporary_file(contents=b"This is a test")

        fileobj = File.from_local_path(temp_file_path)

        fileobj_before_id = fileobj.id

        added_fileobj = self.backend.add_file(temp_file_path, fileobj=fileobj)

        self.assertEqual(added_fileobj.id, fileobj_before_id)
        self.assertTrue(
            FileInStore.objects.filter(
                store=self.backend.db_store, file=added_fileobj
            ).exists()
        )

    def test_add_file_from_local_path_fileobj_size_do_not_match(self) -> None:
        """
        MemoryFileBackend.add_file with fileobj not reused: size mismatch.

        Exception is raised, file is not added in the backend.
        """
        original_contents = b"This is a test"
        new_contents = b"Different content length"

        temp_file_path = self.create_temporary_file(contents=original_contents)
        fileobj = File.from_local_path(temp_file_path)

        self.assertEqual(FileInStore.objects.count(), 0)

        with open(temp_file_path, "wb") as f:
            f.write(new_contents)

        expected_message = (
            "^add_file file size mismatch. Path: "
            f"{temp_file_path} Size in disk: {len(new_contents)} "
            f"fileobj.size: {len(original_contents)}$"
        )

        with self.assertRaisesRegex(ValueError, expected_message):
            self.backend.add_file(temp_file_path, fileobj=fileobj)

        # No file was added in the File table
        self.assertEqual(File.objects.count(), 1)

        # No file was added in the FileInStore (wouldn't be possible
        # since no file was added anyway)
        self.assertEqual(FileInStore.objects.count(), 0)

    def test_add_file_for_an_added_file(self) -> None:
        """add_file add a file that is already in the backend."""
        hexsum = (
            "c7be1ed902fb8dd4d48997c6452f5d7e509fbcdbe2808b16bcf4edce4c07d14e"
        )
        original_contents = b"This is a test"
        file_path = self.create_temporary_file(contents=original_contents)
        with mock.patch(
            "debusine.server.file_backend.memory.MemoryFileBackend._store_file"
        ) as store_file:
            self.backend.add_file(file_path)
        store_file.assert_called_with(hexsum, original_contents)

        self.backend._store_file(hexsum, original_contents)
        with mock.patch(
            "debusine.server.file_backend.memory.MemoryFileBackend._store_file"
        ) as store_file:
            self.backend.add_file(file_path)
        store_file.assert_not_called()

    def test_add_file_contents_mismatch(self) -> None:
        """File exists in the backend with same hash but different contents."""
        original_contents = b"This is a test"
        new_contents = b"Hash collision"  # same length as original_contents
        temp_file_path = self.create_temporary_file(contents=original_contents)
        fileobj = File.from_local_path(temp_file_path)
        self.backend.add_file(temp_file_path)

        self.assertEqual(File.objects.count(), 1)
        self.assertEqual(FileInStore.objects.count(), 1)

        with open(temp_file_path, "wb") as f:
            f.write(new_contents)

        expected_message = (
            f"add_file contents mismatch when trying to add "
            f"{fileobj.hash_digest.hex()}"
        )

        with self.assertRaisesRegex(ValueError, expected_message):
            self.backend.add_file(temp_file_path, fileobj=fileobj)

        self.assertEqual(File.objects.count(), 1)
        self.assertEqual(FileInStore.objects.count(), 1)

    def test_get_stream(self) -> None:
        """MemoryFileBackend.get_stream return a stream with file contents."""
        contents = b"this is a test"
        temp_file_path = self.create_temporary_file(contents=contents)
        file = self.backend.add_file(temp_file_path)

        with self.backend.get_stream(file) as f:
            self.assertEqual(f.read(), contents)

    def test_remove_file(self) -> None:
        """MemoryFileBackend._remove_file delete the file from the backend."""
        temp_file_path = self.create_temporary_file()
        file = self.backend.add_file(temp_file_path)
        with self.backend.get_stream(file) as fd:
            fd.read()

        self.backend._remove_file(file)

        with self.assertRaises(KeyError):
            with self.backend.get_stream(file) as fd:
                pass  # pragma: no cover

    def test_remove_file_twice(self) -> None:
        """
        MemoryFileBackend.remove_file try to delete a file that did not exist.

        This could happen if remove_file() was called concurrently.
        """
        temp_file_path = self.create_temporary_file()
        file = self.backend.add_file(temp_file_path)
        self.backend._remove_file(file)
        self.backend._remove_file(file)
