import os
import tempfile
from pathlib import Path
from unittest.mock import patch

from django.core import mail
from django.core.cache import cache
from django.core.management import call_command
from django.contrib.auth import get_user_model
from django.test import TestCase, override_settings
from django_otp.oath import totp
from django_otp.plugins.otp_static.models import StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice
from wagtail.models import Locale, Site

from .models import (
    ContentPage, EditableText, HomePage, Lawyer, MenuItem, OfficeSettings,
    PracticeArea, SecurityRateLimit,
)
from .security import make_contact_form_token


class PublicSiteTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        call_command("seed_site", domain="testserver", verbosity=0)

    def setUp(self):
        cache.clear()
        SecurityRateLimit.objects.all().delete()

    def test_public_pages_render(self):
        self.assertEqual(Locale.objects.count(), 4)
        self.assertEqual(HomePage.objects.count(), 4)
        self.assertEqual(PracticeArea.objects.count(), 48)
        self.assertEqual(Lawyer.objects.count(), 4)
        self.assertEqual(MenuItem.objects.count(), 6)
        self.assertGreaterEqual(EditableText.objects.count(), 30)
        pages = [
            ("/", "Pravna pomoč"),
            ("/en/", "Legal assistance"),
            ("/hr/", "Pravna pomoć"),
            ("/de/", "Rechtsbeistand"),
            ("/kontakt/", "Pošljite sporočilo"),
            ("/en/contact/", "Send a message"),
            ("/hr/kontakt/", "Pošaljite poruku"),
            ("/de/kontakt/", "Nachricht senden"),
            ("/zasebnost/", "Upravljavec podatkov"),
            ("/en/privacy/", "data controller"),
            ("/hr/privatnost/", "Voditelj obrade"),
            ("/de/datenschutz/", "Verantwortlicher"),
        ]
        for path, expected in pages:
            with self.subTest(path=path):
                response = self.client.get(path)
                self.assertEqual(response.status_code, 200)
                self.assertContains(response, expected)
                self.assertContains(response, 'rel="alternate"')
                self.assertContains(response, 'hreflang="x-default"')
                self.assertContains(response, "Odvetniška pisarna Weindorfer")

    def test_legal_aid_pages_redirect_to_one_page_section(self):
        redirects = {
            "/brezplacna-pravna-pomoc/": "/#bpp",
            "/en/free-legal-aid/": "/en/#bpp",
            "/hr/besplatna-pravna-pomoc/": "/hr/#bpp",
            "/de/kostenlose-rechtshilfe/": "/de/#bpp",
        }
        for path, target in redirects.items():
            with self.subTest(path=path):
                response = self.client.get(path)
                self.assertEqual(response.status_code, 301)
                self.assertEqual(response["Location"], target)

    def test_legacy_indexed_urls_are_permanently_redirected(self):
        redirects = {
            "/predstavitev": "/#o-nas",
            "/podrocja-dela": "/#podrocja",
            "/odvetniki": "/#odvetnik",
            "/kje-se-nahajamo": "/#kontakt",
            "/lokacija": "/#kontakt",
            "/spletne-povezave": "/",
        }
        for path, target in redirects.items():
            with self.subTest(path=path):
                response = self.client.get(path)
                self.assertEqual(response.status_code, 301)
                self.assertEqual(response["Location"], target)

    def test_home_uses_one_page_navigation_and_auto_loaded_map(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)
        for anchor in ("#o-nas", "#podrocja", "#odvetnik", "#bpp", "#lokacija", "#kontakt"):
            self.assertContains(response, anchor)
        self.assertContains(response, "www.google.com/maps?q=")
        self.assertContains(response, "output=embed")
        self.assertContains(response, 'loading="lazy"')
        self.assertContains(response, 'class="officeVisual"')
        self.assertIsNotNone(HomePage.objects.get(locale__language_code="sl").about_image_id)

    def test_home_legal_aid_uses_page_body_and_current_site_url(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertContains(
            response,
            "je uvrščen na seznam odvetnikov za izvajanje brezplačne pravne pomoči",
        )
        self.assertContains(
            response,
            "Brezplačna pravna pomoč je namenjena osebam",
        )
        self.assertContains(
            response,
            "O upravičenosti do BPP odloča pristojni organ",
        )
        self.assertContains(response, "data-legal-aid-body")
        self.assertContains(response, "data-legal-aid-toggle")
        self.assertContains(response, 'href="/#bpp"')
        self.assertNotContains(response, 'href="/brezplacna-pravna-pomoc/"')
        self.assertNotContains(
            response,
            "Odvetnik Igor Weindorfer izvaja brezplačno pravno pomoč in zastopanje",
        )

    def test_menu_content_order_visibility_and_languages_are_editable(self):
        about = MenuItem.objects.get(anchor="o-nas")
        about.label_sl = "Predstavitev pisarne"
        about.label_en = "Our office"
        about.sort_order = 999
        about.save()
        MenuItem.objects.filter(anchor="lokacija").update(active=False)

        response_sl = self.client.get("/")
        self.assertContains(response_sl, "Predstavitev pisarne")
        self.assertNotContains(response_sl, 'href="/#lokacija"')
        self.assertGreater(
            response_sl.content.decode().find("Področja dela"),
            -1,
        )
        self.assertGreater(
            response_sl.content.decode().find("Predstavitev pisarne"),
            response_sl.content.decode().find("Področja dela"),
        )

        response_en = self.client.get("/en/")
        self.assertContains(response_en, "Our office")

    def test_short_page_labels_are_editable_in_all_languages(self):
        meeting = EditableText.objects.get(key="meeting")
        meeting.text_sl = "Rezervirajte termin"
        meeting.text_en = "Book an appointment"
        meeting.save()

        self.assertContains(self.client.get("/"), "Rezervirajte termin")
        self.assertContains(self.client.get("/en/"), "Book an appointment")

    def test_page_copy_brand_and_content_image_are_editable(self):
        home = HomePage.objects.get(locale__language_code="sl")
        home.hero_title = "Uredljiv naslov naslovnice"
        home.about_image_alt = "Pravna literatura na delovni mizi"
        home.save_revision().publish()

        office = OfficeSettings.objects.get()
        office.brand_top_line = "PRAVNA PISARNA"
        office.brand_bottom_line = "WEINDORFER TEST"
        office.save()

        response = self.client.get("/")
        self.assertContains(response, "Uredljiv naslov naslovnice")
        self.assertContains(response, "Pravna literatura na delovni mizi")
        self.assertContains(response, "PRAVNA PISARNA")
        self.assertContains(response, "WEINDORFER TEST")

    def test_original_logo_favicon_is_used(self):
        response = self.client.get("/")
        self.assertContains(response, "home/img/favicon.png")
        self.assertContains(response, "home/img/favicon.ico")
        self.assertNotContains(response, "home/v2/favicon.svg")

    @override_settings(
        EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
        CONTACT_FORM_MIN_SECONDS=0,
    )
    def test_contact_form_sends_email_without_storing_submission(self):
        response = self.client.post("/kontakt/", {
            "name": "Janez Novak",
            "email": "janez@example.com",
            "phone": "040 000 000",
            "subject": "Termin za posvet",
            "message": "Prosim za termin za uvodni pravni posvet v naslednjem tednu.",
            "consent": "on",
            "form_token": make_contact_form_token(),
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Hvala za vaše sporočilo")
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].reply_to, ["janez@example.com"])

    @override_settings(
        EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
        CONTACT_FORM_MIN_SECONDS=0,
    )
    def test_english_contact_form_sends_localized_confirmation(self):
        response = self.client.post("/en/contact/", {
            "name": "John Smith",
            "email": "john@example.com",
            "subject": "Consultation",
            "message": "Please contact me regarding an initial legal consultation.",
            "consent": "on",
            "form_token": make_contact_form_token(),
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Thank you for your message")
        self.assertEqual(len(mail.outbox), 1)

    @override_settings(CONTACT_FORM_MIN_SECONDS=0)
    def test_contact_form_requires_signed_time_token(self):
        response = self.client.post("/kontakt/", {
            "name": "Janez Novak",
            "email": "janez@example.com",
            "subject": "Termin",
            "message": "Prosim za termin za pravni posvet v naslednjem tednu.",
            "consent": "on",
            "form_token": "neveljaven-token",
        })
        self.assertEqual(response.status_code, 400)
        self.assertContains(response, "Obrazec je potekel", status_code=400)

    @override_settings(
        EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
        CONTACT_FORM_MIN_SECONDS=0,
        CONTACT_RATE_SHORT_LIMIT=3,
    )
    def test_contact_form_uses_shared_database_rate_limit(self):
        data = {
            "name": "Janez Novak",
            "email": "janez@example.com",
            "subject": "Termin",
            "message": "Prosim za termin za pravni posvet v naslednjem tednu.",
            "consent": "on",
        }
        for _ in range(3):
            response = self.client.post("/kontakt/", {**data, "form_token": make_contact_form_token()})
            self.assertEqual(response.status_code, 200)
        response = self.client.post("/kontakt/", {**data, "form_token": make_contact_form_token()})
        self.assertEqual(response.status_code, 429)
        self.assertIn("Retry-After", response)

    def test_public_pages_have_browser_security_policy(self):
        response = self.client.get("/")
        self.assertEqual(response.status_code, 200)
        self.assertIn("Content-Security-Policy", response)
        self.assertIn("frame-ancestors 'none'", response["Content-Security-Policy"])
        self.assertIn("Permissions-Policy", response)

    def test_search_limits_and_escapes_user_input(self):
        response = self.client.get("/search/", {"query": '<script>alert("x")</script>'})
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, '<script>alert("x")</script>')
        response = self.client.get("/search/", {"query": "x" * 121})
        self.assertEqual(response.status_code, 400)

    @override_settings(REQUIRE_ADMIN_2FA=True)
    def test_cms_requires_two_factor_setup_for_staff(self):
        User = get_user_model()
        user = User.objects.create_superuser("ales", "ales@example.com", "zelo-varno-testno-geslo")
        self.client.force_login(user)
        response = self.client.get("/cms/")
        self.assertEqual(response.status_code, 302)
        self.assertTrue(response["Location"].startswith("/cms/2fa/setup/"))

    @override_settings(REQUIRE_ADMIN_2FA=True)
    def test_two_factor_can_be_configured_and_creates_recovery_codes(self):
        User = get_user_model()
        user = User.objects.create_superuser("igor", "igor@example.com", "zelo-varno-testno-geslo")
        self.client.force_login(user)
        response = self.client.get("/cms/2fa/setup/")
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "QR-koda")
        device = TOTPDevice.objects.get(user=user, confirmed=False)
        token = totp(
            device.bin_key,
            step=device.step,
            t0=device.t0,
            digits=device.digits,
            drift=device.drift,
        )
        response = self.client.post("/cms/2fa/setup/", {
            "token": str(token).zfill(device.digits),
            "next": "/cms/",
        })
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Shranite obnovitvene kode")
        device.refresh_from_db()
        self.assertTrue(device.confirmed)
        self.assertEqual(StaticToken.objects.filter(device__user=user).count(), 10)

    @override_settings(LOGIN_RATE_LIMIT=5)
    def test_cms_login_is_rate_limited(self):
        User = get_user_model()
        User.objects.create_superuser("ales", "ales@example.com", "pravilno-testno-geslo")
        for _ in range(5):
            self.client.post("/cms/login/", {"username": "ales", "password": "napacno"})
        response = self.client.post("/cms/login/", {"username": "ales", "password": "napacno"})
        self.assertEqual(response.status_code, 429)

    @override_settings(PASSWORD_RESET_RATE_LIMIT=3)
    def test_password_reset_email_is_rate_limited(self):
        for _ in range(3):
            self.client.post("/cms/password_reset/", {"email": "nobody@example.com"})
        response = self.client.post("/cms/password_reset/", {"email": "nobody@example.com"})
        self.assertEqual(response.status_code, 429)

    def test_maintenance_mode_returns_503_but_cms_remains_available(self):
        site = Site.objects.get(is_default_site=True)
        office = OfficeSettings.for_site(site)
        office.maintenance_mode = True
        office.save()
        response = self.client.get("/")
        self.assertEqual(response.status_code, 503)
        self.assertContains(response, "Vzdrževalna dela", status_code=503)
        response = self.client.get("/de/")
        self.assertEqual(response.status_code, 503)
        self.assertContains(response, "Wartungsarbeiten", status_code=503)
        self.assertNotEqual(self.client.get("/cms/").status_code, 503)


class CPanelSetupTests(TestCase):
    @override_settings(
        ALLOWED_HOSTS=["testserver"],
        CONTACT_RECIPIENT="admin@example.com",
        CSRF_COOKIE_SECURE=True,
        DEBUG=False,
        EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
        ENABLE_DJANGO_ADMIN=False,
        REQUIRE_ADMIN_2FA=True,
        SECRET_KEY="test-only-" + "a7Z!" * 20,
        SECURE_HSTS_SECONDS=31_536_000,
        SECURE_SSL_REDIRECT=True,
        SESSION_COOKIE_SECURE=True,
        TURNSTILE_REQUIRED=False,
        WAGTAILADMIN_BASE_URL="https://testserver",
    )
    def test_setup_is_repeatable_and_does_not_overwrite_cms_content(self):
        setup_environment = {
            "INITIAL_ADMIN_USERNAME": "cpanel-admin",
            "INITIAL_ADMIN_EMAIL": "admin@example.com",
            "INITIAL_ADMIN_PASSWORD": "Zelo-Varno-Cpanel-Geslo-2026!",
        }
        with tempfile.TemporaryDirectory() as temporary_directory:
            temporary_path = Path(temporary_directory)
            with (
                patch.dict(os.environ, setup_environment, clear=False),
                override_settings(
                    STATIC_ROOT=temporary_path / "staticfiles",
                    MEDIA_ROOT=temporary_path / "media",
                ),
            ):
                call_command("cpanel_setup", domain="testserver", verbosity=0)

                User = get_user_model()
                self.assertTrue(User.objects.filter(
                    username="cpanel-admin",
                    is_superuser=True,
                ).exists())
                self.assertTrue((temporary_path / "media" / "documents" / ".htaccess").exists())

                home = HomePage.objects.first()
                home.intro = "<p>Vsebina, urejena v CMS-u.</p>"
                home.save_revision().publish()

                call_command("cpanel_setup", domain="testserver", verbosity=0)
                home.refresh_from_db()
                self.assertEqual(home.intro, "<p>Vsebina, urejena v CMS-u.</p>")
                self.assertEqual(ContentPage.objects.filter(page_kind="legal_aid", live=True).count(), 4)
