"""Staff back office: uploading documents and managing owners."""
import shutil
import tempfile

from django.contrib.auth import authenticate, get_user_model
from django.core import mail
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.urls import reverse

from backoffice.naming import title_from_filename
from documents.models import Category, Collection, Document

User = get_user_model()
_TMP = tempfile.mkdtemp(prefix="hartling-bo-test-")

PDF = b"%PDF-1.4 minutes"


def pdf(name="agm-minutes.pdf"):
    return SimpleUploadedFile(name, PDF, content_type="application/pdf")


class TitleSuggestionTests(TestCase):
    def test_filenames_become_readable_titles(self):
        cases = {
            "Strata-Corp-25-2025-AGM-Minutes.pdf": "Strata Corp 25 2025 AGM Minutes",
            "financial_statements_2026_FINAL.pdf": "Financial Statements 2026",
            "2026-05-13 board budget.pdf": "Board Budget",
            "insurance certificate (1).pdf": "Insurance Certificate",
        }
        for filename, expected in cases.items():
            self.assertEqual(title_from_filename(filename), expected, filename)

    def test_unusable_filename_yields_nothing(self):
        self.assertEqual(title_from_filename("1234.pdf"), "")
        self.assertEqual(title_from_filename(""), "")


@override_settings(PRIVATE_MEDIA_ROOT=_TMP)
class UploadTests(TestCase):
    @classmethod
    def tearDownClass(cls):
        shutil.rmtree(_TMP, ignore_errors=True)
        super().tearDownClass()

    def setUp(self):
        self.staff = User.objects.create_user("staff", password="pw-staff-12345", is_staff=True)
        self.owner = User.objects.create_user("owner", password="pw-owner-12345")
        self.category = Category.objects.create(name="Minutes", slug="minutes")

    def test_owner_cannot_reach_the_uploader(self):
        self.client.force_login(self.owner)
        self.assertEqual(self.client.get(reverse("backoffice:upload"), secure=True).status_code, 302)

    def test_anonymous_cannot_reach_the_uploader(self):
        self.assertEqual(self.client.get(reverse("backoffice:upload"), secure=True).status_code, 302)

    def test_the_old_upload_url_now_opens_the_library_uploader(self):
        """The uploader moved onto the library page; the old URL still works."""
        self.client.force_login(self.staff)
        response = self.client.get(reverse("backoffice:upload"), secure=True)
        self.assertEqual(response.status_code, 302)
        self.assertIn("upload=1", response.url)

    def test_staff_get_the_uploader_on_the_library_page(self):
        self.client.force_login(self.staff)
        response = self.client.get(reverse("documents:library"), secure=True)
        self.assertContains(response, "Drop files here")
        self.assertContains(response, "upload-dialog")

    def test_the_library_offers_a_drop_area_not_a_button(self):
        """Staff should see they can drag files in without being told."""
        self.client.force_login(self.staff)
        body = self.client.get(reverse("documents:library"), secure=True).content.decode()
        self.assertIn("page-dropzone", body)
        self.assertIn("Add Documents", body)
        self.assertIn("Drop files here", body)

    def test_dropped_files_are_captured_synchronously(self):
        """Regression: dataTransfer is emptied once the drop handler returns, so
        reading it after a timeout handed the uploader nothing and the sheet
        opened empty."""
        self.client.force_login(self.staff)
        body = self.client.get(reverse("documents:library"), secure=True).content.decode()
        self.assertIn("const dropped = [...(e.dataTransfer?.files || [])]", body)
        self.assertNotIn("setTimeout(() => window.portalUploaderAdd", body)

    def test_owners_get_no_uploader_markup(self):
        self.client.force_login(self.owner)
        response = self.client.get(reverse("documents:library"), secure=True)
        self.assertNotContains(response, "upload-dialog")
        self.assertNotContains(response, "Drop files here")

    def test_suggest_proposes_titles(self):
        self.client.force_login(self.staff)
        response = self.client.post(
            reverse("backoffice:upload_suggest"),
            {"files": [pdf("2026-AGM-Minutes.pdf")]}, secure=True,
        )
        data = response.json()["files"][0]
        self.assertEqual(data["title"], "2026 AGM Minutes")
        self.assertTrue(data["ok"])

    def test_suggest_works_on_a_site_with_no_history(self):
        """A brand-new portal has no documents to learn from — it must still
        answer rather than fail."""
        Document.objects.all().delete()
        self.client.force_login(self.staff)
        response = self.client.post(
            reverse("backoffice:upload_suggest"),
            {"files": [pdf("anything.pdf")]}, secure=True,
        )
        self.assertEqual(response.status_code, 200)
        self.assertIn("group", response.json())

    def test_suggest_reads_the_date_out_of_the_filename(self):
        self.client.force_login(self.staff)
        response = self.client.post(reverse("backoffice:upload_suggest"), {
            "files": [pdf("2026-05-13 AGM Minutes.pdf"), pdf("Budget May 2024.pdf")],
        }, secure=True)
        files = response.json()["files"]
        self.assertEqual(files[0]["date"], "2026-05-13")
        self.assertEqual(files[1]["date"], "2024-05-01")

    def test_suggest_proposes_a_group_with_a_renameable_name(self):
        self.client.force_login(self.staff)
        response = self.client.post(reverse("backoffice:upload_suggest"), {
            "files": [pdf("2026-05-13 minutes.pdf"), pdf("2026-05-13 financials.pdf")],
        }, secure=True)
        group = response.json()["group"]
        self.assertIn("uploads", group["name"])
        self.assertEqual(group["date"], "2026-05-13")

    def test_distinctive_words_beat_a_big_category(self):
        """Regression: a large category used to win on generic vocabulary.

        "Strata" appears in every category on these sites, so it must carry
        almost no signal, while "minutes" — which belongs to one — must decide.
        Before this weighting, "Strata Plan 50 AGM 2026 Minutes" was filed under
        the biggest category (Financials) with full confidence.
        """
        minutes = self.category          # "Minutes", created in setUp
        financials = Category.objects.create(name="Financials", slug="financials")
        # Financials is deliberately much larger, and shares generic words.
        for n in range(14):
            Document.objects.create(
                title=f"Strata Plan Financial Statement {n}", category=financials,
                published=True, file=pdf(f"f{n}.pdf"))
        for n in range(3):
            Document.objects.create(
                title=f"Strata Plan AGM Minutes {n}", category=minutes,
                published=True, file=pdf(f"m{n}.pdf"))

        from backoffice.suggest import suggest_category
        chosen, confidence = suggest_category(["Strata Plan 2026 AGM Minutes.pdf"], "pdf")
        self.assertEqual(chosen, minutes)
        self.assertGreater(confidence, 0.4)

    def test_suggest_learns_the_category_from_existing_documents(self):
        insurance = Category.objects.create(name="Insurance", slug="insurance")
        for title in ("2025 Insurance Certificate", "Insurance Renewal Schedule",
                      "Reinsurance Liability Certificate"):
            Document.objects.create(title=title, category=insurance, published=True,
                                    file=pdf(title + ".pdf"))
        self.client.force_login(self.staff)
        response = self.client.post(reverse("backoffice:upload_suggest"), {
            "files": [pdf("2026 Insurance Certificate Renewal.pdf")],
        }, secure=True)
        self.assertEqual(response.json()["files"][0]["category"], insurance.pk)

    def test_upload_creates_documents(self):
        self.client.force_login(self.staff)
        response = self.client.post(reverse("backoffice:upload_commit"), {
            "files": [pdf("a.pdf"), pdf("b.pdf")],
            "titles": ["First paper", "Second paper"],
            "dates": ["2026-05-13", "2026-05-13"],
            "categories": [self.category.pk, self.category.pk],
            "targets": ["", ""],
            "group_name": "May batch", "group_date": "2026-05-13",
            "group_category": self.category.pk,
        }, secure=True)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.json()["created"]), 2)
        document = Document.objects.get(title="First paper")
        self.assertEqual(document.category, self.category)
        self.assertEqual(str(document.published_date), "2026-05-13")
        self.assertEqual(document.uploaded_by, self.staff)
        self.assertEqual(document.file.read(), PDF)

    def test_upload_can_create_a_collection_for_the_batch(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:upload_commit"), {
            "files": [pdf("a.pdf"), pdf("b.pdf")],
            "titles": ["Minutes", "Financials"],
            "targets": ["", ""],
            "group_name": "2026 AGM documents", "group_date": "2026-05-13",
        }, secure=True)
        collection = Collection.objects.get(title="2026 AGM documents")
        self.assertEqual(collection.documents.count(), 2)

    def test_a_file_can_be_routed_to_an_existing_group(self):
        existing = Collection.objects.create(title="2025 AGM documents", slug="agm-2025")
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:upload_commit"), {
            "files": [pdf("a.pdf"), pdf("b.pdf")],
            "titles": ["Goes to the old group", "Stays in the new one"],
            "targets": [str(existing.pk), ""],
            "group_name": "2026 AGM documents", "group_date": "2026-05-13",
        }, secure=True)
        self.assertEqual(existing.documents.count(), 1)
        self.assertEqual(existing.documents.first().title, "Goes to the old group")
        self.assertEqual(
            Collection.objects.get(title="2026 AGM documents").documents.count(), 1)

    def test_per_file_overrides_beat_the_group(self):
        other = Category.objects.create(name="Insurance", slug="insurance")
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:upload_commit"), {
            "files": [pdf("a.pdf"), pdf("b.pdf")],
            "titles": ["Follows the group", "Overridden"],
            "dates": ["", "2020-01-02"],
            "categories": ["", other.pk],
            "targets": ["", ""],
            "group_name": "Batch", "group_date": "2026-05-13",
            "group_category": self.category.pk,
        }, secure=True)
        followed = Document.objects.get(title="Follows the group")
        overridden = Document.objects.get(title="Overridden")
        self.assertEqual(followed.category, self.category)
        self.assertEqual(str(followed.published_date), "2026-05-13")
        self.assertEqual(overridden.category, other)
        self.assertEqual(str(overridden.published_date), "2020-01-02")

    def test_a_single_file_needs_no_group(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:upload_commit"), {
            "files": [pdf("solo.pdf")], "titles": ["On its own"], "targets": [""],
            "group_name": "Should not be created", "group_date": "2026-05-13",
        }, secure=True)
        self.assertIsNone(Document.objects.get(title="On its own").collection)
        self.assertFalse(Collection.objects.exists())

    def test_executable_uploads_are_refused(self):
        self.client.force_login(self.staff)
        response = self.client.post(reverse("backoffice:upload_commit"), {
            "files": [SimpleUploadedFile("evil.php", b"<?php ?>", content_type="application/x-php")],
            "titles": ["evil"],
        }, secure=True)
        self.assertEqual(response.json()["created"], [])
        self.assertEqual(len(response.json()["rejected"]), 1)
        self.assertFalse(Document.objects.exists())

    def test_html_and_svg_are_refused(self):
        self.client.force_login(self.staff)
        for name in ("x.html", "x.svg", "x.js"):
            response = self.client.post(reverse("backoffice:upload_commit"), {
                "files": [SimpleUploadedFile(name, b"<script>", content_type="text/html")],
                "titles": ["x"],
            }, secure=True)
            self.assertEqual(response.json()["created"], [], name)


class PeopleTests(TestCase):
    def setUp(self):
        self.staff = User.objects.create_user(
            "staff", email="staff@example.com", password="pw-staff-12345", is_staff=True
        )
        self.owner = User.objects.create_user(
            "owner", email="owner@example.com", password="pw-owner-12345"
        )
        self.client.force_login(self.staff)

    def test_owner_cannot_reach_people(self):
        self.client.force_login(self.owner)
        self.assertEqual(self.client.get(reverse("backoffice:people"), secure=True).status_code, 302)

    def test_list_and_search(self):
        response = self.client.get(reverse("backoffice:people"), {"q": "owner"}, secure=True)
        self.assertEqual([u.username for u in response.context["people"]], ["owner"])

    def test_add_owner_sends_a_set_up_link(self):
        self.client.post(reverse("backoffice:person_add"), {
            "email": "new@example.com", "first_name": "New", "method": "email",
        }, secure=True)
        user = User.objects.get(email="new@example.com")
        self.assertEqual(user.username, "new")
        self.assertEqual(len(mail.outbox), 1)
        self.assertIn("set", mail.outbox[0].body.lower())
        # The account holds a random password nobody was told — it exists only
        # so Django will send the reset mail, which it skips for unusable ones.
        for guess in ("", "password", "new@example.com", "new"):
            self.assertFalse(user.check_password(guess))

    def test_add_owner_with_a_generated_password(self):
        response = self.client.post(reverse("backoffice:person_add"), {
            "username": "handover", "email": "h@example.com", "method": "password",
        }, secure=True, follow=True)
        password = response.context["new_password"]
        self.assertTrue(password)
        self.assertTrue(User.objects.get(username="handover").check_password(password))
        # Shown once: a second view must not repeat it.
        again = self.client.get(reverse("backoffice:people"), secure=True)
        self.assertIsNone(again.context["new_password"])

    def test_duplicate_account_is_refused(self):
        self.client.post(reverse("backoffice:person_add"),
                         {"username": "owner", "email": "x@example.com"}, secure=True)
        self.assertEqual(User.objects.filter(username="owner").count(), 1)

    def test_generate_new_password_for_existing_user(self):
        response = self.client.post(
            reverse("backoffice:person_new_password", args=[self.owner.pk]), secure=True, follow=True
        )
        password = response.context["new_password"]
        self.owner.refresh_from_db()
        self.assertTrue(self.owner.check_password(password))

    def test_send_reset_link(self):
        self.client.post(reverse("backoffice:person_reset_link", args=[self.owner.pk]), secure=True)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].to, ["owner@example.com"])

    def test_deactivate_and_reactivate(self):
        self.client.post(reverse("backoffice:person_toggle_active", args=[self.owner.pk]), secure=True)
        self.owner.refresh_from_db()
        self.assertFalse(self.owner.is_active)
        self.assertIsNone(authenticate(username="owner", password="pw-owner-12345"))

    def test_cannot_deactivate_or_delete_yourself(self):
        self.client.post(reverse("backoffice:person_toggle_active", args=[self.staff.pk]), secure=True)
        self.staff.refresh_from_db()
        self.assertTrue(self.staff.is_active)
        self.client.post(reverse("backoffice:person_delete", args=[self.staff.pk]), secure=True)
        self.assertTrue(User.objects.filter(pk=self.staff.pk).exists())

    def test_delete_owner(self):
        self.client.post(reverse("backoffice:person_delete", args=[self.owner.pk]), secure=True)
        self.assertFalse(User.objects.filter(username="owner").exists())

    def test_cannot_delete_the_only_administrator(self):
        admin = User.objects.create_superuser("root", "root@example.com", "pw-root-123456")
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:person_delete", args=[admin.pk]), secure=True)
        self.assertTrue(User.objects.filter(pk=admin.pk).exists())

    def test_actions_require_post(self):
        for name in ("person_new_password", "person_delete", "person_toggle_active"):
            response = self.client.get(reverse(f"backoffice:{name}", args=[self.owner.pk]), secure=True)
            self.assertEqual(response.status_code, 405, name)
