"""
Tests for the aa_customizer app.

Coverage:
  - CustomBranding model field round-trips (custom CSS/HTML fields)
  - help_text safety — no raw HTML angle-bracket tags that would break the
    Django admin page when rendered via |safe
  - Context processor injects AA_CUSTOMIZER and SITE_NAME
  - Template logic correctly injects custom CSS/HTML into rendered output
  - Template logic suppresses output when fields are blank
  - SPA mode: model fields, template wrapping, CSS loading, split-class suppression
"""

from django.db import models as django_models
from django.template import Context, Template
from django.test import RequestFactory, TestCase

from .models import CustomBranding


# ---------------------------------------------------------------------------
# Model round-trip tests
# ---------------------------------------------------------------------------


class CustomCSSFieldsTest(TestCase):
    """CustomBranding fields that hold raw CSS/HTML store and retrieve correctly."""

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    def test_custom_css_round_trip(self):
        css = "body { background: #1a1a2e; } .navbar { color: red; }"
        self.branding.custom_css = css
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().custom_css, css)

    def test_custom_css_url_round_trip(self):
        url = "https://cdn.example.com/dark-theme.css"
        self.branding.custom_css_url = url
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().custom_css_url, url)

    def test_head_extra_html_round_trip(self):
        html = '<meta name="description" content="Test site"><script src="analytics.js" defer></script>'
        self.branding.head_extra_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().head_extra_html, html)

    def test_login_page_css_url_round_trip(self):
        url = "https://cdn.example.com/login-override.css"
        self.branding.login_page_css_url = url
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().login_page_css_url, url)

    def test_login_page_css_round_trip(self):
        css = ".card-login { background: rgba(0,0,0,0.9); border-radius: 12px; }"
        self.branding.login_page_css = css
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().login_page_css, css)

    def test_login_page_head_html_round_trip(self):
        html = '<link rel="preconnect" href="https://fonts.googleapis.com">'
        self.branding.login_page_head_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().login_page_head_html, html)

    def test_login_page_body_html_round_trip(self):
        html = '<div id="particle-canvas"></div><script src="particles.min.js"></script>'
        self.branding.login_page_body_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().login_page_body_html, html)

    def test_dashboard_css_url_round_trip(self):
        url = "https://cdn.example.com/dashboard-override.css"
        self.branding.dashboard_css_url = url
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().dashboard_css_url, url)

    def test_dashboard_css_round_trip(self):
        css = ".dashboard-widget { background: #0d0d1a; }"
        self.branding.dashboard_css = css
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().dashboard_css, css)

    def test_dashboard_head_html_round_trip(self):
        html = '<link rel="preconnect" href="https://fonts.googleapis.com">'
        self.branding.dashboard_head_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().dashboard_head_html, html)

    def test_dashboard_body_html_round_trip(self):
        html = '<div id="dash-overlay"></div><script src="widgets.min.js"></script>'
        self.branding.dashboard_body_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().dashboard_body_html, html)

    def test_superuser_dashboard_css_url_round_trip(self):
        url = "https://cdn.example.com/admin-panel.css"
        self.branding.superuser_dashboard_css_url = url
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().superuser_dashboard_css_url, url)

    def test_superuser_dashboard_css_round_trip(self):
        css = ".admin-widget { border: 2px solid red; }"
        self.branding.superuser_dashboard_css = css
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().superuser_dashboard_css, css)

    def test_superuser_dashboard_head_html_round_trip(self):
        html = '<script src="https://cdn.example.com/chart.js" defer></script>'
        self.branding.superuser_dashboard_head_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().superuser_dashboard_head_html, html)

    def test_superuser_dashboard_body_html_round_trip(self):
        html = '<div id="admin-overlay"></div>'
        self.branding.superuser_dashboard_body_html = html
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().superuser_dashboard_body_html, html)

    def test_all_custom_code_fields_blank_by_default(self):
        for field_name in (
            "custom_css",
            "custom_css_url",
            "head_extra_html",
            "login_page_css_url",
            "login_page_css",
            "login_page_head_html",
            "login_page_body_html",
            "dashboard_css_url",
            "dashboard_css",
            "dashboard_head_html",
            "dashboard_body_html",
            "superuser_dashboard_css_url",
            "superuser_dashboard_css",
            "superuser_dashboard_head_html",
            "superuser_dashboard_body_html",
        ):
            with self.subTest(field=field_name):
                self.assertEqual(getattr(self.branding, field_name), "")


# ---------------------------------------------------------------------------
# help_text safety — guards against admin page parse-break regression
# ---------------------------------------------------------------------------


class HelpTextSafetyTest(TestCase):
    """
    Verify that help_text strings for CSS/HTML fields use HTML entities, not
    raw angle-bracket tags.

    Background: Django admin renders help_text via ``{{ field.field.help_text|safe }}``,
    bypassing all escaping.  A literal ``<style>`` in help_text causes the browser
    to enter CSS-ingestion mode, consuming every subsequent fieldset as CSS text
    — making them invisible in the DOM entirely.

    Fixed in v1.1.16 (commit 7170c15): all raw tags replaced with HTML entities.
    This test prevents regressions.
    """

    FIELDS_TO_CHECK = [
        "custom_css",
        "custom_css_url",
        "head_extra_html",
        "login_page_head_html",
        "login_page_body_html",
        "dashboard_head_html",
        "dashboard_body_html",
        "superuser_dashboard_head_html",
        "superuser_dashboard_body_html",
    ]

    RAW_TAGS_FORBIDDEN = ["<style>", "<head>", "</head>", "</body>"]

    def test_help_text_contains_no_raw_html_tags(self):
        fields_by_name = {
            f.name: f
            for f in CustomBranding._meta.get_fields()
            if isinstance(f, django_models.Field)
        }
        for field_name in self.FIELDS_TO_CHECK:
            field = fields_by_name[field_name]
            help_text = str(field.help_text)
            for raw_tag in self.RAW_TAGS_FORBIDDEN:
                with self.subTest(field=field_name, tag=raw_tag):
                    self.assertNotIn(
                        raw_tag,
                        help_text,
                        f"{field_name}.help_text contains unescaped '{raw_tag}' — "
                        "use HTML entities (&lt;…&gt;) instead to avoid breaking the admin page.",
                    )


# ---------------------------------------------------------------------------
# Context processor
# ---------------------------------------------------------------------------


class ContextProcessorTest(TestCase):
    def test_injects_branding_instance(self):
        from .context_processors import aa_customizer

        ctx = aa_customizer(RequestFactory().get("/"))
        self.assertIn("AA_CUSTOMIZER", ctx)
        self.assertIsInstance(ctx["AA_CUSTOMIZER"], CustomBranding)

    def test_uses_custom_site_name_when_set(self):
        from .context_processors import aa_customizer

        branding = CustomBranding.get_solo()
        branding.site_name = "R3V-W Auth"
        branding.save()
        ctx = aa_customizer(RequestFactory().get("/"))
        self.assertEqual(ctx["SITE_NAME"], "R3V-W Auth")

    def test_falls_back_to_string_site_name_when_blank(self):
        from .context_processors import aa_customizer

        ctx = aa_customizer(RequestFactory().get("/"))
        self.assertIn("SITE_NAME", ctx)
        self.assertIsInstance(ctx["SITE_NAME"], str)


# ---------------------------------------------------------------------------
# Template rendering — mirrors the actual template logic in base-bs5.html and
# public/base.html so we can verify injection without a full AA stack.
# ---------------------------------------------------------------------------


def _render(template_str, context_data):
    return Template(template_str).render(Context(context_data))


class TemplateInjectionTest(TestCase):
    """
    Inline template tests that mirror the exact ``{% if %}``/``|safe`` patterns
    used in the real templates.  Confirms each field ends up in the rendered
    output at the right position.
    """

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    # ── global custom CSS ────────────────────────────────────────────────────

    def test_custom_css_renders_as_inline_style_block(self):
        self.branding.custom_css = "body { background: #0d0d1a; }"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.custom_css %}"
            "<style>{{ AA_CUSTOMIZER.custom_css|safe }}</style>"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn("<style>body { background: #0d0d1a; }</style>", out)

    def test_custom_css_url_renders_as_link_tag(self):
        self.branding.custom_css_url = "https://cdn.example.com/dark.css"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.custom_css_url %}"
            '<link rel="stylesheet" href="{{ AA_CUSTOMIZER.custom_css_url }}">'
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('href="https://cdn.example.com/dark.css"', out)

    def test_head_extra_html_renders_verbatim(self):
        self.branding.head_extra_html = '<meta name="theme-color" content="#0d0d1a">'
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.head_extra_html %}"
            "{{ AA_CUSTOMIZER.head_extra_html|safe }}"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('<meta name="theme-color" content="#0d0d1a">', out)

    # ── login-page-only fields ───────────────────────────────────────────────

    def test_login_page_css_url_renders_as_link_tag(self):
        self.branding.login_page_css_url = "https://cdn.example.com/login.css"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.login_page_css_url %}"
            '<link rel="stylesheet" href="{{ AA_CUSTOMIZER.login_page_css_url }}">'
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('href="https://cdn.example.com/login.css"', out)

    def test_login_page_css_renders_as_inline_style_block(self):
        self.branding.login_page_css = ".card-login { background: rgba(0,0,0,0.9); }"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.login_page_css %}"
            "<style>{{ AA_CUSTOMIZER.login_page_css|safe }}</style>"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn(
            "<style>.card-login { background: rgba(0,0,0,0.9); }</style>", out
        )

    def test_login_page_head_html_renders_verbatim(self):
        self.branding.login_page_head_html = (
            '<link rel="preconnect" href="https://fonts.googleapis.com">'
        )
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.login_page_head_html %}"
            "{{ AA_CUSTOMIZER.login_page_head_html|safe }}"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn(
            '<link rel="preconnect" href="https://fonts.googleapis.com">', out
        )

    def test_login_page_body_html_injected_before_body_close(self):
        self.branding.login_page_body_html = '<div id="particles"></div>'
        self.branding.save()
        out = _render(
            "<body>"
            "{% if AA_CUSTOMIZER.login_page_body_html %}"
            "{{ AA_CUSTOMIZER.login_page_body_html|safe }}"
            "{% endif %}"
            "</body>",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('<div id="particles"></div></body>', out)

    # ── blank fields produce no output ──────────────────────────────────────

    def test_blank_fields_produce_no_output(self):
        templates = {
            "custom_css": (
                "{% if AA_CUSTOMIZER.custom_css %}"
                "<style>{{ AA_CUSTOMIZER.custom_css|safe }}</style>"
                "{% endif %}"
            ),
            "custom_css_url": (
                "{% if AA_CUSTOMIZER.custom_css_url %}"
                '<link rel="stylesheet" href="{{ AA_CUSTOMIZER.custom_css_url }}">'
                "{% endif %}"
            ),
            "head_extra_html": (
                "{% if AA_CUSTOMIZER.head_extra_html %}"
                "{{ AA_CUSTOMIZER.head_extra_html|safe }}"
                "{% endif %}"
            ),
            "login_page_css": (
                "{% if AA_CUSTOMIZER.login_page_css %}"
                "<style>{{ AA_CUSTOMIZER.login_page_css|safe }}</style>"
                "{% endif %}"
            ),
            "login_page_head_html": (
                "{% if AA_CUSTOMIZER.login_page_head_html %}"
                "{{ AA_CUSTOMIZER.login_page_head_html|safe }}"
                "{% endif %}"
            ),
            "login_page_body_html": (
                "{% if AA_CUSTOMIZER.login_page_body_html %}"
                "{{ AA_CUSTOMIZER.login_page_body_html|safe }}"
                "{% endif %}"
            ),
        }
        for field_name, tpl_str in templates.items():
            with self.subTest(field=field_name):
                out = _render(tpl_str, {"AA_CUSTOMIZER": self.branding})
                self.assertEqual(
                    out.strip(),
                    "",
                    f"Expected no output for blank {field_name}, got: {out!r}",
                )


# ---------------------------------------------------------------------------
# Template rendering — dashboard-specific fields
# ---------------------------------------------------------------------------


class DashboardTemplateInjectionTest(TestCase):
    """
    Tests for main dashboard CSS/HTML injection fields, mirroring the
    ``{% block extra_css %}`` and ``{% block extra_javascript %}`` patterns
    in ``authentication/dashboard.html``.
    """

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    def test_dashboard_css_url_renders_as_link_tag(self):
        self.branding.dashboard_css_url = "https://cdn.example.com/dash.css"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.dashboard_css_url %}"
            '<link rel="stylesheet" href="{{ AA_CUSTOMIZER.dashboard_css_url }}">'
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('href="https://cdn.example.com/dash.css"', out)

    def test_dashboard_css_renders_as_inline_style_block(self):
        self.branding.dashboard_css = ".dashboard-widget { background: #0d0d1a; }"
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.dashboard_css %}"
            "<style>{{ AA_CUSTOMIZER.dashboard_css|safe }}</style>"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn(
            "<style>.dashboard-widget { background: #0d0d1a; }</style>", out
        )

    def test_dashboard_head_html_renders_verbatim(self):
        self.branding.dashboard_head_html = (
            '<link rel="preconnect" href="https://fonts.googleapis.com">'
        )
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.dashboard_head_html %}"
            "{{ AA_CUSTOMIZER.dashboard_head_html|safe }}"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn(
            '<link rel="preconnect" href="https://fonts.googleapis.com">', out
        )

    def test_dashboard_body_html_renders_verbatim(self):
        self.branding.dashboard_body_html = '<div id="dash-overlay"></div>'
        self.branding.save()
        out = _render(
            "{% if AA_CUSTOMIZER.dashboard_body_html %}"
            "{{ AA_CUSTOMIZER.dashboard_body_html|safe }}"
            "{% endif %}",
            {"AA_CUSTOMIZER": self.branding},
        )
        self.assertIn('<div id="dash-overlay"></div>', out)

    def test_blank_dashboard_fields_produce_no_output(self):
        templates = {
            "dashboard_css_url": (
                "{% if AA_CUSTOMIZER.dashboard_css_url %}"
                '<link rel="stylesheet" href="{{ AA_CUSTOMIZER.dashboard_css_url }}">'
                "{% endif %}"
            ),
            "dashboard_css": (
                "{% if AA_CUSTOMIZER.dashboard_css %}"
                "<style>{{ AA_CUSTOMIZER.dashboard_css|safe }}</style>"
                "{% endif %}"
            ),
            "dashboard_head_html": (
                "{% if AA_CUSTOMIZER.dashboard_head_html %}"
                "{{ AA_CUSTOMIZER.dashboard_head_html|safe }}"
                "{% endif %}"
            ),
            "dashboard_body_html": (
                "{% if AA_CUSTOMIZER.dashboard_body_html %}"
                "{{ AA_CUSTOMIZER.dashboard_body_html|safe }}"
                "{% endif %}"
            ),
        }
        for field_name, tpl_str in templates.items():
            with self.subTest(field=field_name):
                out = _render(tpl_str, {"AA_CUSTOMIZER": self.branding})
                self.assertEqual(
                    out.strip(),
                    "",
                    f"Expected no output for blank {field_name}, got: {out!r}",
                )


# ---------------------------------------------------------------------------
# Template rendering — superuser dashboard fields (via superuser_branding tag)
# ---------------------------------------------------------------------------


class SuperuserDashboardTemplateInjectionTest(TestCase):
    """
    Tests for the superuser admin status widget CSS/HTML injection fields.
    These mirror the patterns in ``allianceauth/admin-status/overview.html``
    which uses the ``{% superuser_branding as aac_sb %}`` simple_tag because
    the inclusion tag that renders overview.html does not pass request context.
    """

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    def _render_with_tag(self, snippet):
        """Render snippet with crum simulating a logged-in superuser."""
        from unittest.mock import MagicMock, patch

        mock_user = MagicMock(is_superuser=True, is_staff=True)
        with patch(
            "aa_customizer.templatetags.aa_customizer_tags.get_current_user",
            return_value=mock_user,
        ):
            return _render(
                "{% load aa_customizer_tags %}"
                "{% superuser_branding as aac_sb %}"
                + snippet,
                {},
            )

    def test_superuser_dashboard_css_url_renders_as_link_tag(self):
        self.branding.superuser_dashboard_css_url = "https://cdn.example.com/admin.css"
        self.branding.save()
        out = self._render_with_tag(
            "{% if aac_sb.superuser_dashboard_css_url %}"
            '<link rel="stylesheet" href="{{ aac_sb.superuser_dashboard_css_url }}">'
            "{% endif %}"
        )
        self.assertIn('href="https://cdn.example.com/admin.css"', out)

    def test_superuser_dashboard_css_renders_as_inline_style_block(self):
        self.branding.superuser_dashboard_css = ".admin-widget { border: 2px solid red; }"
        self.branding.save()
        out = self._render_with_tag(
            "{% if aac_sb.superuser_dashboard_css %}"
            "<style>{{ aac_sb.superuser_dashboard_css|safe }}</style>"
            "{% endif %}"
        )
        self.assertIn(
            "<style>.admin-widget { border: 2px solid red; }</style>", out
        )

    def test_superuser_dashboard_head_html_renders_verbatim(self):
        self.branding.superuser_dashboard_head_html = (
            '<script src="https://cdn.example.com/chart.js" defer></script>'
        )
        self.branding.save()
        out = self._render_with_tag(
            "{% if aac_sb.superuser_dashboard_head_html %}"
            "{{ aac_sb.superuser_dashboard_head_html|safe }}"
            "{% endif %}"
        )
        self.assertIn(
            '<script src="https://cdn.example.com/chart.js" defer></script>', out
        )

    def test_superuser_dashboard_body_html_renders_verbatim(self):
        self.branding.superuser_dashboard_body_html = '<div id="admin-overlay"></div>'
        self.branding.save()
        out = self._render_with_tag(
            "{% if aac_sb.superuser_dashboard_body_html %}"
            "{{ aac_sb.superuser_dashboard_body_html|safe }}"
            "{% endif %}"
        )
        self.assertIn('<div id="admin-overlay"></div>', out)

    def test_superuser_code_suppressed_for_non_superusers(self):
        """
        When crum reports a non-superuser, superuser_branding() returns None
        so every ``{% if aac_sb.xxx %}`` check is falsy and nothing renders.
        """
        from unittest.mock import MagicMock, patch

        self.branding.superuser_dashboard_css = ".secret { color: red; }"
        self.branding.superuser_dashboard_head_html = "<script>alert(1)</script>"
        self.branding.save()

        non_superuser = MagicMock(is_superuser=False, is_staff=False)
        with patch(
            "aa_customizer.templatetags.aa_customizer_tags.get_current_user",
            return_value=non_superuser,
        ):
            output = _render(
                "{% load aa_customizer_tags %}"
                "{% superuser_branding as aac_sb %}"
                "{% if aac_sb.superuser_dashboard_css %}"
                "<style>{{ aac_sb.superuser_dashboard_css|safe }}</style>"
                "{% endif %}"
                "{% if aac_sb.superuser_dashboard_head_html %}"
                "{{ aac_sb.superuser_dashboard_head_html|safe }}"
                "{% endif %}",
                {},
            )

        self.assertEqual(output, "")
        self.assertNotIn(".secret", output)
        self.assertNotIn("alert(1)", output)

    def test_blank_superuser_dashboard_fields_produce_no_output(self):
        templates = {
            "superuser_dashboard_css_url": (
                "{% if aac_sb.superuser_dashboard_css_url %}"
                '<link rel="stylesheet" href="{{ aac_sb.superuser_dashboard_css_url }}">'
                "{% endif %}"
            ),
            "superuser_dashboard_css": (
                "{% if aac_sb.superuser_dashboard_css %}"
                "<style>{{ aac_sb.superuser_dashboard_css|safe }}</style>"
                "{% endif %}"
            ),
            "superuser_dashboard_head_html": (
                "{% if aac_sb.superuser_dashboard_head_html %}"
                "{{ aac_sb.superuser_dashboard_head_html|safe }}"
                "{% endif %}"
            ),
            "superuser_dashboard_body_html": (
                "{% if aac_sb.superuser_dashboard_body_html %}"
                "{{ aac_sb.superuser_dashboard_body_html|safe }}"
                "{% endif %}"
            ),
        }
        for field_name, tpl_str in templates.items():
            with self.subTest(field=field_name):
                out = self._render_with_tag(tpl_str)
                self.assertEqual(
                    out.strip(),
                    "",
                    f"Expected no output for blank {field_name}, got: {out!r}",
                )


# ---------------------------------------------------------------------------
# Trusted-user admin restriction (AA_CUSTOMIZER_TRUSTED_USER_IDS)
# ---------------------------------------------------------------------------


class TrustedAdminUserTests(TestCase):
    """
    Tests for the AA_CUSTOMIZER_TRUSTED_USER_IDS admin access restriction.

    When the setting is absent or empty, any superuser passes.
    When the setting is a non-empty list, only users whose pk appears in that
    list are allowed — regardless of superuser flag.
    """

    def _make_request(self, pk, is_superuser=True):
        request = RequestFactory().get("/")
        from unittest.mock import MagicMock
        request.user = MagicMock(pk=pk, is_superuser=is_superuser, is_staff=is_superuser)
        return request

    def test_no_setting_allows_superuser(self):
        """Without the setting, any superuser is trusted."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[]):
            self.assertTrue(_is_trusted_admin(self._make_request(pk=42, is_superuser=True)))

    def test_no_setting_blocks_non_superuser(self):
        """Without the setting, non-superusers are blocked."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[]):
            self.assertFalse(_is_trusted_admin(self._make_request(pk=42, is_superuser=False)))

    def test_trusted_list_allows_listed_pk(self):
        """A user whose PK appears in the trusted list is allowed."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[1, 7]):
            self.assertTrue(_is_trusted_admin(self._make_request(pk=7)))

    def test_trusted_list_blocks_unlisted_superuser(self):
        """A superuser whose PK is NOT in the trusted list is blocked."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[1, 7]):
            self.assertFalse(_is_trusted_admin(self._make_request(pk=99, is_superuser=True)))

    def test_trusted_list_allows_by_pk_regardless_of_superuser_flag(self):
        """_is_trusted_admin gates on PK membership alone when the list is set."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[5]):
            # pk=5 is in list; is_superuser flag is irrelevant when list is active
            self.assertTrue(_is_trusted_admin(self._make_request(pk=5, is_superuser=False)))

    def test_trusted_list_with_single_entry(self):
        """Edge case: a single-element list still enforces the restriction correctly."""
        from .permissions import _is_trusted_admin
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[3]):
            self.assertTrue(_is_trusted_admin(self._make_request(pk=3)))
            self.assertFalse(_is_trusted_admin(self._make_request(pk=4)))


# ---------------------------------------------------------------------------
# Admin integration tests — real ModelAdmin instances + real User objects
# ---------------------------------------------------------------------------


class CustomBrandingAdminPermissionTests(TestCase):
    """
    End-to-end permission tests for CustomBrandingAdmin.

    These use real Django User objects and real admin class instances so that
    any bypass introduced by the MRO (SingletonModelAdmin → ModelAdmin) is
    caught, rather than only testing the helper function in isolation.
    """

    def setUp(self):
        from django.contrib.admin import site as admin_site
        from aa_customizer.admin import CustomBrandingAdmin
        from aa_customizer.models import CustomBranding

        self.admin_instance = CustomBrandingAdmin(CustomBranding, admin_site)

        # Trusted superuser (pk will be determined after creation)
        from django.contrib.auth import get_user_model
        User = get_user_model()
        self.trusted = User.objects.create_superuser(
            username='trusted', password='x', email='t@example.com'
        )
        self.untrusted = User.objects.create_superuser(
            username='untrusted', password='x', email='u@example.com'
        )

    def _req(self, user):
        req = RequestFactory().get('/')
        req.user = user
        return req

    # -- with AA_CUSTOMIZER_TRUSTED_USER_IDS set --

    def test_trusted_user_has_view_permission(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(
                self.admin_instance.has_view_permission(self._req(self.trusted))
            )

    def test_trusted_user_has_change_permission(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(
                self.admin_instance.has_change_permission(self._req(self.trusted))
            )

    def test_untrusted_superuser_denied_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(
                self.admin_instance.has_view_permission(self._req(self.untrusted))
            )

    def test_untrusted_superuser_denied_change(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(
                self.admin_instance.has_change_permission(self._req(self.untrusted))
            )

    def test_untrusted_superuser_model_perms_all_false(self):
        """get_model_perms returns all False → model hidden from admin index."""
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            perms = self.admin_instance.get_model_perms(self._req(self.untrusted))
            self.assertFalse(any(perms.values()), perms)

    def test_trusted_user_model_perms_has_true(self):
        """get_model_perms has at least view/change True for a trusted user."""
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            perms = self.admin_instance.get_model_perms(self._req(self.trusted))
            self.assertTrue(perms.get('view') or perms.get('change'), perms)

    # -- without AA_CUSTOMIZER_TRUSTED_USER_IDS (fallback mode) --

    def test_fallback_any_superuser_can_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[]):
            self.assertTrue(
                self.admin_instance.has_view_permission(self._req(self.untrusted))
            )

    def test_fallback_non_superuser_blocked(self):
        from django.contrib.auth import get_user_model
        regular = get_user_model().objects.create_user(
            username='regular', password='x', email='r@example.com'
        )
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[]):
            self.assertFalse(
                self.admin_instance.has_view_permission(self._req(regular))
            )


class CustomBrandingAdminHTTPTests(TestCase):
    """
    HTTP-level admin tests using Django's test client.

    These hit the actual admin views to confirm that Django's permission
    stack (admin_view wrapper → changeform_view → has_view_or_change_permission)
    honours our overrides end-to-end.
    """

    def setUp(self):
        from django.contrib.auth import get_user_model
        User = get_user_model()
        self.trusted = User.objects.create_superuser(
            username='trusted_http', password='pass', email='th@example.com'
        )
        self.untrusted = User.objects.create_superuser(
            username='untrusted_http', password='pass', email='uh@example.com'
        )
        # Ensure the singleton exists
        from aa_customizer.models import CustomBranding
        CustomBranding.get_solo()

    def _admin_url(self):
        from django.contrib.admin import site as admin_site
        from aa_customizer.models import CustomBranding
        opts = CustomBranding._meta
        # With SOLO_ADMIN_SKIP_OBJECT_LIST_PAGE=True (default), the root url
        # maps straight to change_view; test the change URL explicitly.
        from django.urls import reverse
        return reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name),
                       args=(1,))

    def test_trusted_user_can_access_change_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.client.force_login(self.trusted)
            resp = self.client.get(self._admin_url())
            # 200 = show form; 302 = redirect after save (also fine)
            self.assertIn(resp.status_code, (200, 302))

    def test_untrusted_superuser_gets_403_on_change_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.client.force_login(self.untrusted)
            resp = self.client.get(self._admin_url())
            self.assertEqual(resp.status_code, 403)

    def test_untrusted_superuser_not_in_admin_app_list(self):
        """The model must not appear in the admin index for an untrusted user."""
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.client.force_login(self.untrusted)
            resp = self.client.get('/admin/')
            # App list JSON (used by the sidebar) should not mention custombranding
            content = resp.content.decode()
            self.assertNotIn('custombranding', content.lower())


class AACMediaImageAdminPermissionTests(TestCase):
    """
    Verify that AACMediaImageAdmin is also restricted by _is_trusted_admin,
    not just CustomBrandingAdmin.
    """

    def setUp(self):
        from django.contrib.admin import site as admin_site
        from aa_customizer.admin import AACMediaImageAdmin
        from aa_customizer.models import AACMediaImage
        self.admin_instance = AACMediaImageAdmin(AACMediaImage, admin_site)

        from django.contrib.auth import get_user_model
        User = get_user_model()
        self.trusted = User.objects.create_superuser(
            username='media_trusted', password='x', email='mt@example.com'
        )
        self.untrusted = User.objects.create_superuser(
            username='media_untrusted', password='x', email='mu@example.com'
        )

    def _req(self, user):
        req = RequestFactory().get('/')
        req.user = user
        return req

    def test_trusted_can_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(self.admin_instance.has_view_permission(self._req(self.trusted)))

    def test_trusted_can_add(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(self.admin_instance.has_add_permission(self._req(self.trusted)))

    def test_trusted_can_change(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(self.admin_instance.has_change_permission(self._req(self.trusted)))

    def test_trusted_can_delete(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertTrue(self.admin_instance.has_delete_permission(self._req(self.trusted)))

    def test_untrusted_superuser_denied_view(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(self.admin_instance.has_view_permission(self._req(self.untrusted)))

    def test_untrusted_superuser_denied_add(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(self.admin_instance.has_add_permission(self._req(self.untrusted)))

    def test_untrusted_superuser_denied_change(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(self.admin_instance.has_change_permission(self._req(self.untrusted)))

    def test_untrusted_superuser_denied_delete(self):
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.assertFalse(self.admin_instance.has_delete_permission(self._req(self.untrusted)))

    def test_untrusted_not_in_admin_index(self):
        """Media Library must not appear in the admin index for an untrusted user."""
        with self.settings(AA_CUSTOMIZER_TRUSTED_USER_IDS=[self.trusted.pk]):
            self.client.force_login(self.untrusted)
            resp = self.client.get('/admin/')
            self.assertNotIn('aacmediaimage', resp.content.decode().lower())


# ---------------------------------------------------------------------------
# SPA mode — model fields and template rendering
# ---------------------------------------------------------------------------


class SPAModeModelTest(TestCase):
    """Tests for SPA mode model fields."""

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    def test_spa_disabled_by_default(self):
        self.assertFalse(self.branding.login_spa_enabled)

    def test_spa_nav_brand_blank_by_default(self):
        self.assertEqual(self.branding.login_spa_nav_brand, "")

    def test_spa_enabled_round_trip(self):
        self.branding.login_spa_enabled = True
        self.branding.save()
        self.assertTrue(CustomBranding.get_solo().login_spa_enabled)

    def test_spa_disabled_round_trip(self):
        self.branding.login_spa_enabled = True
        self.branding.save()
        self.branding.login_spa_enabled = False
        self.branding.save()
        self.assertFalse(CustomBranding.get_solo().login_spa_enabled)

    def test_spa_nav_brand_round_trip(self):
        self.branding.login_spa_nav_brand = "REV3NANTS WRATH"
        self.branding.save()
        self.assertEqual(CustomBranding.get_solo().login_spa_nav_brand, "REV3NANTS WRATH")

    def test_spa_nav_brand_max_length(self):
        field = CustomBranding._meta.get_field("login_spa_nav_brand")
        self.assertEqual(field.max_length, 120)


class SPATemplateRenderingTest(TestCase):
    """
    Tests for SPA mode template rendering, mirroring the logic in
    ``public/base.html``.
    """

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    # Inline template that mirrors the real base.html SPA block
    SPA_BODY_TPL = (
        "{% if AA_CUSTOMIZER.login_spa_enabled %}"
        '<div id="aac-spa-body-src" style="display:none">'
        "{{ AA_CUSTOMIZER.login_page_body_html|safe }}</div>"
        "{% else %}"
        "{% if AA_CUSTOMIZER.login_page_body_html %}"
        "{{ AA_CUSTOMIZER.login_page_body_html|safe }}"
        "{% endif %}"
        "{% endif %}"
    )

    SPA_CSS_TPL = (
        "{% load static %}"
        "{% if AA_CUSTOMIZER.login_spa_enabled %}"
        '<link rel="stylesheet" href="{% static \'aa_customizer/css/login-spa.css\' %}">'
        "{% endif %}"
    )

    def test_spa_enabled_wraps_body_html_in_src_div(self):
        """SPA on: body HTML is wrapped in #aac-spa-body-src."""
        self.branding.login_spa_enabled = True
        self.branding.login_page_body_html = '<div id="aac-spa-content">…</div>'
        self.branding.save()
        out = _render(self.SPA_BODY_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertIn('id="aac-spa-body-src"', out)
        self.assertIn('<div id="aac-spa-content">…</div>', out)

    def test_spa_disabled_body_html_injected_directly(self):
        """SPA off: body HTML is NOT wrapped in #aac-spa-body-src."""
        self.branding.login_spa_enabled = False
        self.branding.login_page_body_html = '<div id="custom-markup">test</div>'
        self.branding.save()
        out = _render(self.SPA_BODY_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertNotIn('id="aac-spa-body-src"', out)
        self.assertIn('<div id="custom-markup">test</div>', out)

    def test_spa_disabled_body_html_blank_produces_no_output(self):
        """SPA off, blank body HTML: nothing rendered."""
        self.branding.login_spa_enabled = False
        self.branding.login_page_body_html = ""
        self.branding.save()
        out = _render(self.SPA_BODY_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertEqual(out.strip(), "")

    def test_spa_enabled_empty_body_html_still_renders_src_div(self):
        """SPA on even with blank body HTML: wrapper div is present (JS will just skip)."""
        self.branding.login_spa_enabled = True
        self.branding.login_page_body_html = ""
        self.branding.save()
        out = _render(self.SPA_BODY_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertIn('id="aac-spa-body-src"', out)

    def test_spa_enabled_loads_spa_css(self):
        """SPA on: login-spa.css link tag is rendered."""
        self.branding.login_spa_enabled = True
        self.branding.save()
        out = _render(self.SPA_CSS_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertIn("login-spa.css", out)

    def test_spa_disabled_does_not_load_spa_css(self):
        """SPA off: login-spa.css is NOT rendered."""
        self.branding.login_spa_enabled = False
        self.branding.save()
        out = _render(self.SPA_CSS_TPL, {"AA_CUSTOMIZER": self.branding})
        self.assertNotIn("login-spa.css", out)


class SPASplitClassSuppressionTest(TestCase):
    """
    Verify that split layout classes are suppressed (v1.2.4 fix) when SPA is on.

    Mirrors the conditional in ``public/middle_box.html``:
        class="aac-login-root{% if not login_spa_enabled %}{% if split %} aac-split{% endif %}…{% endif %}"
    """

    TPL = (
        "{% if AA_CUSTOMIZER.login_spa_enabled %}"
        '<div class="aac-login-root">'
        "{% else %}"
        '<div class="aac-login-root'
        '{% if AA_CUSTOMIZER.login_layout == "split" or AA_CUSTOMIZER.login_layout == "split-right" %}'
        " aac-split"
        "{% endif %}"
        '{% if AA_CUSTOMIZER.login_layout == "split-right" %}'
        " aac-split-right"
        "{% endif %}"
        '">'
        "{% endif %}"
    )

    def setUp(self):
        self.branding = CustomBranding.get_solo()

    def _out(self):
        return _render(self.TPL, {"AA_CUSTOMIZER": self.branding})

    def test_card_layout_no_split_classes(self):
        self.branding.login_spa_enabled = False
        self.branding.login_layout = "card"
        self.branding.save()
        out = self._out()
        self.assertIn('class="aac-login-root"', out)
        self.assertNotIn("aac-split", out)

    def test_split_layout_adds_aac_split_class(self):
        self.branding.login_spa_enabled = False
        self.branding.login_layout = "split"
        self.branding.save()
        out = self._out()
        self.assertIn("aac-split", out)
        self.assertNotIn("aac-split-right", out)

    def test_split_right_layout_adds_both_classes(self):
        self.branding.login_spa_enabled = False
        self.branding.login_layout = "split-right"
        self.branding.save()
        out = self._out()
        self.assertIn("aac-split", out)
        self.assertIn("aac-split-right", out)

    def test_spa_enabled_suppresses_split_classes_on_split_layout(self):
        """SPA mode: no split classes even when layout is 'split'."""
        self.branding.login_spa_enabled = True
        self.branding.login_layout = "split"
        self.branding.save()
        out = self._out()
        self.assertNotIn("aac-split", out)

    def test_spa_enabled_suppresses_split_right_classes(self):
        """SPA mode: no split-right classes even when layout is 'split-right'."""
        self.branding.login_spa_enabled = True
        self.branding.login_layout = "split-right"
        self.branding.save()
        out = self._out()
        self.assertNotIn("aac-split", out)
        self.assertNotIn("aac-split-right", out)

    def test_spa_enabled_card_layout_is_plain_root(self):
        """SPA mode with card layout: plain aac-login-root, no split noise."""
        self.branding.login_spa_enabled = True
        self.branding.login_layout = "card"
        self.branding.save()
        out = self._out()
        self.assertIn('class="aac-login-root"', out)
        self.assertNotIn("aac-split", out)
