# (c) cavaliba.com - test_filestore_cleanup

import os
import shutil
import tempfile
import time
import uuid
from unittest.mock import patch

from django.test import TestCase, override_settings
from django.utils import timezone

from app_data.filestore import filestore_cleanup
from app_data.models import DataFile
from app_home.models import CavalibaLog


class TestFilestoreCleanup(TestCase):
    def setUp(self):
        self.rootdir = tempfile.mkdtemp(prefix="cavaliba_test_filestore_")
        self.override = override_settings(CAVALIBA_FILESTORE=self.rootdir)
        self.override.enable()

    def tearDown(self):
        self.override.disable()
        shutil.rmtree(self.rootdir, ignore_errors=True)

    def _write(self, name, age_hours=0):
        path = os.path.join(self.rootdir, name)
        with open(path, "w") as f:
            f.write("data")
        if age_hours:
            old = time.time() - age_hours * 3600
            os.utime(path, (old, old))
        return path

    def _make_datafile(self, fileid=None, filepath=None, not_after=None):
        return DataFile.objects.create(
            fileid=fileid or str(uuid.uuid4()),
            filename="f.txt",
            filepath=filepath or "",
            not_after=not_after,
        )

    def test_removes_expired_datafile_and_its_file(self):
        fileid = str(uuid.uuid4())
        path = self._write(fileid)
        df = self._make_datafile(
            fileid=fileid, filepath=path, not_after=timezone.now() - timezone.timedelta(days=1)
        )
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 1)
        self.assertEqual(orphaned, 0)
        self.assertFalse(DataFile.objects.filter(pk=df.pk).exists())
        self.assertFalse(os.path.exists(path))

    def test_keeps_non_expired_datafile(self):
        fileid = str(uuid.uuid4())
        path = self._write(fileid)
        df = self._make_datafile(
            fileid=fileid, filepath=path, not_after=timezone.now() + timezone.timedelta(days=1)
        )
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 0)
        self.assertTrue(DataFile.objects.filter(pk=df.pk).exists())
        self.assertTrue(os.path.exists(path))

    def test_keeps_datafile_without_not_after(self):
        fileid = str(uuid.uuid4())
        path = self._write(fileid)
        df = self._make_datafile(fileid=fileid, filepath=path, not_after=None)
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 0)
        self.assertTrue(DataFile.objects.filter(pk=df.pk).exists())
        self.assertTrue(os.path.exists(path))

    def test_removes_old_orphan_tmp_file(self):
        path = self._write("tmp-abc", age_hours=48)
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 0)
        self.assertEqual(orphaned, 1)
        self.assertFalse(os.path.exists(path))

    def test_keeps_recent_orphan_tmp_file(self):
        path = self._write("tmp-abc", age_hours=1)
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(orphaned, 0)
        self.assertTrue(os.path.exists(path))

    def test_keeps_file_backed_by_datafile_even_if_old(self):
        fileid = str(uuid.uuid4())
        path = self._write(fileid, age_hours=48)
        self._make_datafile(fileid=fileid, filepath=path)
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(orphaned, 0)
        self.assertTrue(os.path.exists(path))

    def test_missing_rootdir_returns_zero_orphaned(self):
        shutil.rmtree(self.rootdir, ignore_errors=True)
        cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 0)
        self.assertEqual(orphaned, 0)

    def test_tmp_orphan_removal_logged_as_info_not_error(self):
        self._write("tmp-abc", age_hours=48)
        filestore_cleanup(hours=24)
        self.assertFalse(
            CavalibaLog.objects.filter(view="filestore", action="orphan", level="ERROR").exists()
        )
        self.assertTrue(
            CavalibaLog.objects.filter(view="filestore", action="orphan", level="INFO").exists()
        )

    def test_non_tmp_orphan_removal_logged_as_error(self):
        self._write("unexpected-stray-file", age_hours=48)
        filestore_cleanup(hours=24)
        self.assertTrue(
            CavalibaLog.objects.filter(view="filestore", action="orphan", level="ERROR").exists()
        )

    def test_orphan_delete_failure_is_logged_not_silently_swallowed(self):
        path = self._write("tmp-abc", age_hours=48)
        with patch("app_data.filestore.os.remove", side_effect=OSError("permission denied")):
            cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(orphaned, 0)
        self.assertTrue(os.path.exists(path))
        self.assertTrue(
            CavalibaLog.objects.filter(
                view="filestore", action="cleanup", status="FAILED", level="ERROR"
            ).exists()
        )

    def test_expired_datafile_delete_failure_is_logged_not_silently_swallowed(self):
        fileid = str(uuid.uuid4())
        path = self._write(fileid)
        self._make_datafile(
            fileid=fileid, filepath=path, not_after=timezone.now() - timezone.timedelta(days=1)
        )
        with patch("app_data.filestore.os.remove", side_effect=OSError("permission denied")):
            cleaned, orphaned = filestore_cleanup(hours=24)
        self.assertEqual(cleaned, 1)
        self.assertTrue(os.path.exists(path))
        self.assertTrue(
            CavalibaLog.objects.filter(
                view="filestore", action="cleanup", status="FAILED", level="ERROR"
            ).exists()
        )
