"""Board management, and the sanitiser that guards the rich-text bio."""
import shutil
import tempfile

from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from django.urls import reverse

from backoffice.sanitise import clean_html
from documents.models import BoardMember

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


class SanitiserTests(TestCase):
    """Bios are written as HTML and rendered as HTML, so the input is filtered.

    The threat is not a careless staff member — it is a stolen staff account
    storing script that then runs in every owner's browser.
    """

    def test_simple_formatting_survives(self):
        html = "<p>A <strong>chartered</strong> accountant, <em>retired</em>.</p>"
        self.assertEqual(clean_html(html), html)

    def test_lists_survive(self):
        html = "<ul><li>One</li><li>Two</li></ul>"
        self.assertEqual(clean_html(html), html)

    def test_scripts_are_removed(self):
        cleaned = clean_html("<p>Hi</p><script>alert('x')</script>")
        self.assertNotIn("<script", cleaned)
        self.assertNotIn("alert", cleaned.replace("alert", "") or cleaned)

    def test_event_handlers_are_stripped(self):
        cleaned = clean_html('<p onclick="steal()">Text</p>')
        self.assertNotIn("onclick", cleaned)
        self.assertIn("Text", cleaned)

    def test_javascript_links_are_refused(self):
        cleaned = clean_html('<a href="javascript:alert(1)">click</a>')
        self.assertNotIn("javascript", cleaned)
        self.assertIn("click", cleaned)

    def test_ordinary_links_survive(self):
        cleaned = clean_html('<a href="https://example.com">site</a>')
        self.assertIn('href="https://example.com"', cleaned)

    def test_images_and_iframes_are_dropped(self):
        cleaned = clean_html('<img src="x" onerror="go()"><iframe src="y"></iframe>hello')
        self.assertNotIn("<img", cleaned)
        self.assertNotIn("<iframe", cleaned)
        self.assertIn("hello", cleaned)

    def test_plain_text_is_escaped(self):
        self.assertEqual(clean_html("5 < 6 & 7 > 2"), "5 &lt; 6 &amp; 7 &gt; 2")

    def test_empty_input(self):
        self.assertEqual(clean_html(""), "")
        self.assertEqual(clean_html(None), "")


@override_settings(PRIVATE_MEDIA_ROOT=_TMP)
class BoardManagementTests(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.chair = BoardMember.objects.create(name="A Chair", title="Chair", order=0)
        self.member = BoardMember.objects.create(name="B Member", title="Director", order=1)

    # ---- access ----
    def test_owners_cannot_add_a_member(self):
        self.client.force_login(self.owner)
        response = self.client.post(reverse("backoffice:board_add"), {"name": "Nope"}, secure=True)
        self.assertEqual(response.status_code, 302)
        self.assertFalse(BoardMember.objects.filter(name="Nope").exists())

    def test_owners_cannot_remove_a_member(self):
        self.client.force_login(self.owner)
        self.client.post(reverse("backoffice:board_remove", args=[self.chair.pk]), secure=True)
        self.chair.refresh_from_db()
        self.assertIsNone(self.chair.removed_at)

    # ---- add / edit ----
    def test_add_a_member(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_add"), {
            "name": "C Newcomer", "title": "Treasurer",
            "contact_email": "c@example.com",
            "bio": "<p>Joined <strong>2026</strong>.</p>",
        }, secure=True)
        added = BoardMember.objects.get(name="C Newcomer")
        self.assertEqual(added.title, "Treasurer")
        self.assertIn("<strong>2026</strong>", added.bio)
        self.assertTrue(added.active)
        self.assertEqual(added.order, 2)     # appended after the existing two

    def test_a_bio_is_sanitised_on_the_way_in(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_add"), {
            "name": "D Risky",
            "bio": '<p>Fine</p><script>alert(1)</script><a href="javascript:x()">bad</a>',
        }, secure=True)
        stored = BoardMember.objects.get(name="D Risky").bio
        self.assertIn("Fine", stored)
        self.assertNotIn("<script", stored)
        self.assertNotIn("javascript", stored)

    def test_edit_an_existing_member(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_save", args=[self.chair.pk]), {
            "name": "A Chair", "title": "Board Chairman", "bio": "<p>Updated.</p>",
        }, secure=True)
        self.chair.refresh_from_db()
        self.assertEqual(self.chair.title, "Board Chairman")
        self.assertEqual(self.chair.bio, "<p>Updated.</p>")

    def test_a_member_needs_a_name(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_add"), {"name": "  "}, secure=True)
        self.assertEqual(BoardMember.objects.count(), 2)

    # ---- remove / restore ----
    def test_removing_keeps_the_record(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_remove", args=[self.member.pk]), secure=True)
        self.member.refresh_from_db()
        self.assertIsNotNone(self.member.removed_at)
        self.assertFalse(self.member.active)
        # Still in the database — removal is a mistake people make.
        self.assertTrue(BoardMember.objects.filter(pk=self.member.pk).exists())

    def test_a_removed_member_is_off_the_public_board(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_remove", args=[self.member.pk]), secure=True)

        # A separate client, so the staff member's "removed" flash message does
        # not follow the owner into their own session.
        from django.test import Client
        owner_client = Client()
        owner_client.force_login(self.owner)
        body = owner_client.get(reverse("documents:board"), secure=True).content.decode()
        grid = body.split('class="board-grid"')[-1].split("</ul>")[0]
        self.assertNotIn("B Member", grid)
        self.assertIn("A Chair", grid)

    def test_restoring_puts_them_back(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_remove", args=[self.member.pk]), secure=True)
        self.client.post(reverse("backoffice:board_restore", args=[self.member.pk]), secure=True)
        self.member.refresh_from_db()
        self.assertIsNone(self.member.removed_at)
        self.assertTrue(self.member.active)

    def test_staff_see_the_removed_list_and_owners_do_not(self):
        self.client.force_login(self.staff)
        self.client.post(reverse("backoffice:board_remove", args=[self.member.pk]), secure=True)
        staff_view = self.client.get(reverse("documents:board"), secure=True)
        self.assertContains(staff_view, "Removed members")
        self.client.force_login(self.owner)
        owner_view = self.client.get(reverse("documents:board"), secure=True)
        self.assertNotContains(owner_view, "Removed members")

    # ---- reorder ----
    def test_reorder(self):
        self.client.force_login(self.staff)
        import json
        response = self.client.post(
            reverse("backoffice:board_reorder"),
            data=json.dumps({"order": [self.member.pk, self.chair.pk]}),
            content_type="application/json", secure=True,
        )
        self.assertEqual(response.status_code, 200)
        self.chair.refresh_from_db(); self.member.refresh_from_db()
        self.assertEqual(self.member.order, 0)
        self.assertEqual(self.chair.order, 1)
        self.assertEqual(
            [m.name for m in BoardMember.objects.filter(removed_at=None)],
            ["B Member", "A Chair"],
        )

    def test_owners_cannot_reorder(self):
        self.client.force_login(self.owner)
        import json
        response = self.client.post(
            reverse("backoffice:board_reorder"),
            data=json.dumps({"order": [self.member.pk, self.chair.pk]}),
            content_type="application/json", secure=True,
        )
        self.assertEqual(response.status_code, 302)

    # ---- page ----
    def test_staff_see_management_controls_and_owners_do_not(self):
        self.client.force_login(self.staff)
        body = self.client.get(reverse("documents:board"), secure=True).content.decode()
        self.assertIn("board-add", body)
        self.assertIn("Reorder", body)
        self.assertIn("edit-member", body)

        self.client.force_login(self.owner)
        body = self.client.get(reverse("documents:board"), secure=True).content.decode()
        self.assertNotIn("board-add", body)
        self.assertNotIn("edit-member", body)
