# (c) cavaliba.com - tests / data / exporter abort - task, view, and file cleanup

import os
import shutil
import tempfile
import uuid
from unittest import mock

from django.test import TestCase, override_settings
from django.urls import reverse

import app_home.cache as cache
from app_data.exporter import ExportSpec
from app_data.models import DataFile, DataTask
from app_data.task_manager import create_datatask
from app_data.tasks import task_export
from tests import helper

AAA = {"username": "unittest", "perms": ["p_data_admin"]}


@override_settings(CAVALIBA_AUTH_MODE="unittest")
class ViewExporterAbortTest(TestCase):
    def _isolate_filestore(self):
        """Point the filestore at a throwaway directory for this test.

        An async export writes a real blob, and the test DB is dropped at the end
        of the run while the blob is not - so without this every run would leave
        orphans behind in the shared CAVALIBA_FILESTORE.
        """

        rootdir = tempfile.mkdtemp(prefix="cavaliba_test_filestore_")
        override = override_settings(CAVALIBA_FILESTORE=rootdir)
        override.enable()
        self.addCleanup(override.disable)
        self.addCleanup(shutil.rmtree, rootdir, ignore_errors=True)
        return rootdir

    def setUp(self):
        self._isolate_filestore()
        cache.clear()
        helper.add_admin_user(login="unittest")
        helper.add_schema(
            classname="vabort",
            field_definition={
                "mystring": {"dataformat": "string", "displayname": "MyString"},
            },
        )
        for i in range(1, 6):
            helper.add_instance(
                classname="vabort", keyname=f"vabort-{i:02d}", fields={"mystring": f"row {i}"}
            )

        self.spec = ExportSpec(schema="vabort", format="csv")
        self.params = {"spec": self.spec.to_dict(), "aaa": AAA}
        self.task = create_datatask(
            name="Export csv - vabort", params=self.params, owner_type="user", owner_id="unittest"
        )

    def _abort_url(self, handle=None):
        return reverse("app_data:data_exporter_abort", args=[handle or self.task.handle])

    # ---------------------------------------------------------------
    # the view
    # ---------------------------------------------------------------

    def test_abort_marks_the_task_aborted(self):
        response = self.client.post(self._abort_url())

        self.assertEqual(response.status_code, 200)
        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "ABORTED")

    def test_abort_rerenders_the_progress_partial(self):
        response = self.client.post(self._abort_url())

        self.assertContains(response, "cv-export-progress")
        self.assertNotContains(response, 'hx-trigger="every 2s"')

    def test_abort_rejects_get(self):
        response = self.client.get(self._abort_url())

        self.assertEqual(response.status_code, 405)

    def test_abort_unknown_handle_is_404(self):
        response = self.client.post(self._abort_url(handle=uuid.uuid4()))

        self.assertEqual(response.status_code, 404)

    def test_admin_can_abort_another_users_task(self):
        # the setUp user is an admin, so p_task_all widens the ownership check
        self.task.owner_id = "someoneelse"
        self.task.save(update_fields=["owner_id"])

        response = self.client.post(self._abort_url())

        self.assertEqual(response.status_code, 200)
        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "ABORTED")

    # ---------------------------------------------------------------
    # the worker
    # ---------------------------------------------------------------

    def test_worker_skips_an_already_aborted_task(self):
        DataTask.objects.filter(handle=self.task.handle).update(state="ABORTED")

        task_export(str(self.task.handle), self.params)

        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "ABORTED")
        self.assertIsNone(self.task.attachment)
        self.assertEqual(DataFile.objects.count(), 0)

    def test_abort_mid_run_discards_the_partial_file(self):
        with (
            mock.patch("app_data.exporter.BATCH_SIZE", 1),
            mock.patch("app_data.tasks.is_aborted", return_value=True),
        ):
            task_export(str(self.task.handle), self.params)

        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "ABORTED")
        self.assertIsNone(self.task.attachment)
        self.assertTrue(self.task.output["aborted"])
        # neither the row nor the blob survives: a truncated export must not be
        # downloadable, and must not be left as a non-tmp- filestore orphan
        self.assertEqual(DataFile.objects.count(), 0)

    def test_abort_mid_run_leaves_no_blob_on_disk(self):
        from django.conf import settings

        before = set(os.listdir(str(settings.CAVALIBA_FILESTORE)))

        with (
            mock.patch("app_data.exporter.BATCH_SIZE", 1),
            mock.patch("app_data.tasks.is_aborted", return_value=True),
        ):
            task_export(str(self.task.handle), self.params)

        after = set(os.listdir(str(settings.CAVALIBA_FILESTORE)))
        self.assertEqual(before, after)

    def test_failure_discards_the_file_and_marks_failed(self):
        with mock.patch("app_data.tasks.export_write", side_effect=RuntimeError("disk on fire")):
            task_export(str(self.task.handle), self.params)

        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "FAILED")
        self.assertIn("disk on fire", self.task.output["error"])
        self.assertIsNone(self.task.attachment)
        self.assertEqual(DataFile.objects.count(), 0)

    def test_clean_run_keeps_the_file(self):
        task_export(str(self.task.handle), self.params)

        self.task.refresh_from_db()
        self.assertEqual(self.task.state, "DONE")
        self.assertEqual(DataFile.objects.count(), 1)
        datafile = DataFile.objects.first()
        self.assertEqual(self.task.attachment["fileid"], datafile.fileid)
        self.assertTrue(os.path.isfile(datafile.filepath))
