"""Release surface: production settings, health probes, error pages, logging."""
import importlib
import os
import re
import subprocess
import sys
from pathlib import Path
from unittest.mock import patch

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.template import Context, engines
from django.test import SimpleTestCase, TestCase, override_settings
from django.urls import path, reverse

from config.settings import base as base_settings

RELEASE = f"{settings.RELEASE_NAME} v{settings.RELEASE_VERSION}"
# Ephemeral, non-secret material used only to exercise the production settings
# guards. It is long and varied enough to satisfy the deployment check itself.
EPHEMERAL_SECRET = "CcareRc1EphemeralProbeKey0123456789abcdefABCDEF" * 2


def production_settings(**env):
    """Import production settings under an ephemeral secret, never a real one.

    Simulates a production host: explicit environment variables and no
    development ``.env`` file on disk. ``None`` removes a variable so the
    missing-configuration guards can be exercised.
    """
    cleared = ("DJANGO_TRUSTED_PROXY_HEADER", "DJANGO_HSTS_INCLUDE_SUBDOMAINS", "DJANGO_HSTS_PRELOAD",
        "DJANGO_SECURE_REFERRER_POLICY", "DJANGO_LOG_LEVEL")
    with patch.dict(os.environ), \
            patch("dotenv.load_dotenv"), \
            patch("dotenv.dotenv_values", return_value={}):
        for key in cleared:
            os.environ.pop(key, None)
        os.environ["DJANGO_SECRET_KEY"] = EPHEMERAL_SECRET
        os.environ["DJANGO_ALLOWED_HOSTS"] = "ccare.example.invalid"
        for key, value in env.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value
        # base is cached from the first import, so both modules must be reloaded
        # for the requested environment to take effect.
        importlib.reload(base_settings)
        module = importlib.reload(importlib.import_module("config.settings.production"))
    return module


def temporary_urlconf(name, *patterns):
    """Install a throwaway URLconf so error handlers can be exercised for real."""
    module = type(sys)(name)
    module.urlpatterns = list(patterns)
    sys.modules[name] = module
    return name


class ProductionSettingsTests(SimpleTestCase):
    def tearDown(self):
        production_settings()

    def test_production_hardening_is_unconditional(self):
        module = production_settings()
        self.assertFalse(module.DEBUG)
        self.assertEqual(module.SECURE_SSL_REDIRECT, True)
        self.assertEqual(module.SESSION_COOKIE_SECURE, True)
        self.assertEqual(module.SESSION_COOKIE_HTTPONLY, True)
        self.assertEqual(module.CSRF_COOKIE_SECURE, True)
        self.assertEqual(module.SECURE_CONTENT_TYPE_NOSNIFF, True)
        self.assertEqual(module.X_FRAME_OPTIONS, "DENY")
        self.assertEqual(module.SECURE_HSTS_SECONDS, 31536000)
        self.assertEqual(module.READY_STATEMENT_TIMEOUT_MS, 2000)

    def test_weak_secret_wildcard_host_and_missing_host_are_refused(self):
        for env in ({"DJANGO_SECRET_KEY": "django-insecure-" + "x" * 60},
                    {"DJANGO_SECRET_KEY": "short"},
                    # Long enough, but a single repeated character is not a secret.
                    {"DJANGO_SECRET_KEY": "P" * 64},
                    {"DJANGO_ALLOWED_HOSTS": "*"},
                    {"DJANGO_ALLOWED_HOSTS": ""},
                    {"DJANGO_ALLOWED_HOSTS": None}):
            with self.subTest(env=sorted(env)), self.assertRaises(ImproperlyConfigured):
                production_settings(**env)

    def test_placeholder_secret_is_refused(self):
        with self.assertRaises(ImproperlyConfigured):
            production_settings(DJANGO_SECRET_KEY="replace-me")

    def test_proxy_ssl_header_is_opt_in_and_validated(self):
        self.assertIsNone(getattr(production_settings(), "SECURE_PROXY_SSL_HEADER", None))
        self.assertEqual(production_settings(DJANGO_TRUSTED_PROXY_HEADER="X-FORWARDED-PROTO").SECURE_PROXY_SSL_HEADER,
            ("HTTP_X_FORWARDED_PROTO", "https"))
        self.assertEqual(production_settings(DJANGO_TRUSTED_PROXY_HEADER="X-FORWARDED-SSL").SECURE_PROXY_SSL_HEADER,
            ("HTTP_X_FORWARDED_SSL", "on"))
        with self.assertRaises(ImproperlyConfigured):
            production_settings(DJANGO_TRUSTED_PROXY_HEADER="x-forwarded-proto, evil")

    def test_whole_domain_hsts_commitments_remain_operator_opt_in(self):
        default = production_settings()
        self.assertFalse(default.SECURE_HSTS_INCLUDE_SUBDOMAINS)
        self.assertFalse(default.SECURE_HSTS_PRELOAD)
        enabled = production_settings(DJANGO_HSTS_INCLUDE_SUBDOMAINS="True", DJANGO_HSTS_PRELOAD="True")
        self.assertTrue(enabled.SECURE_HSTS_INCLUDE_SUBDOMAINS)
        self.assertTrue(enabled.SECURE_HSTS_PRELOAD)

    def test_referrer_policy_and_log_level_are_validated_not_guessed(self):
        self.assertEqual(production_settings().SECURE_REFERRER_POLICY, "same-origin")
        self.assertEqual(production_settings(DJANGO_SECURE_REFERRER_POLICY="no-referrer").SECURE_REFERRER_POLICY, "no-referrer")
        with self.assertRaises(ImproperlyConfigured):
            production_settings(DJANGO_SECURE_REFERRER_POLICY="unsafe")
        with self.assertRaises(ImproperlyConfigured):
            production_settings(DJANGO_LOG_LEVEL="VERBOSE")

    def test_development_settings_never_become_production_defaults(self):
        module = production_settings()
        self.assertTrue(module.COMMUNICATIONS_PRODUCTION)
        self.assertEqual(module.EMAIL_TIMEOUT, 10)


class EnvironmentSecretTests(SimpleTestCase):
    def test_process_environment_supplies_the_database_password(self):
        """A container or service manager must be able to supply the credential."""
        environment = {"DB_PASSWORD": "environment-supplied-value"}
        with patch.dict(os.environ, environment):
            self.assertEqual(base_settings.required_env("DB_PASSWORD", local_only=True), "environment-supplied-value")

    def test_local_env_remains_a_fallback_and_placeholders_are_refused(self):
        with patch.dict(os.environ, {"DB_PASSWORD": ""}):
            self.assertEqual(base_settings.required_env("DB_PASSWORD", local_only=True),
                base_settings.dotenv_values(base_settings.BASE_DIR / ".env", interpolate=False)["DB_PASSWORD"])
        with patch.dict(os.environ, {"DB_PASSWORD": "replace-me"}):
            with self.assertRaises(ImproperlyConfigured):
                base_settings.required_env("DB_PASSWORD")


class HealthProbeTests(TestCase):
    def test_liveness_never_touches_the_database(self):
        with patch("config.views.transaction.atomic", side_effect=AssertionError("liveness must not query")):
            response = self.client.get(reverse("health"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"status": "ok"})
        self.assertEqual(response["Cache-Control"], "max-age=0, no-cache, no-store, must-revalidate, private")

    def test_readiness_reports_ready_without_disclosing_anything(self):
        response = self.client.get(reverse("ready"))
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"status": "ready"})
        body = response.content.decode()
        for leak in ("password", "SECRET", "dbname", "ccare_dev", "127.0.0.1", "Traceback", "psycopg"):
            self.assertNotIn(leak, body)

    def test_readiness_reports_not_ready_when_the_database_is_unavailable(self):
        from django.db import DatabaseError
        with patch("config.views.connections") as connections:
            connections.__getitem__.return_value.cursor.return_value.__enter__.return_value.execute.side_effect = DatabaseError("down")
            # The failure is recorded server-side; only a coarse status is returned.
            with self.assertLogs("django.request", level="ERROR"):
                response = self.client.get(reverse("ready"))
        self.assertEqual(response.status_code, 503)
        self.assertEqual(response.json(), {"status": "not_ready"})

    def test_probes_are_safe_methods_only(self):
        for name in ("health", "ready"):
            self.assertEqual(self.client.post(reverse(name)).status_code, 405)

    def test_readiness_timeout_is_bounded_inside_a_transaction(self):
        from unittest.mock import MagicMock
        cursor = MagicMock()
        cursor.__enter__.return_value = cursor
        connection = MagicMock()
        connection.cursor.return_value = cursor
        with patch("config.views.connections", {"default": connection}), \
                patch("config.views.transaction.atomic"):
            response = self.client.get(reverse("ready"))
        self.assertEqual(response.status_code, 200)
        statements = [call.args[0] for call in cursor.execute.call_args_list]
        self.assertTrue(any("SET LOCAL statement_timeout" in str(statement) for statement in statements), statements)
        self.assertTrue(any(str(statement).strip() == "SELECT 1" for statement in statements), statements)


class ErrorPageTests(TestCase):
    """Error pages must render from an empty context and leak nothing."""

    templates = ("400.html", "403.html", "404.html", "500.html")

    @override_settings(DEBUG=False, ALLOWED_HOSTS=["ccare.example.invalid"], SECURE_SSL_REDIRECT=True,
                       SECRET_KEY="rc-remediation-synthetic-secret")
    def test_disallowed_hosts_are_400_before_authentication(self):
        from django.test import Client
        client = Client(raise_request_exception=False)
        for host in ("unknown.example.invalid", "203.0.113.7"):
            for secure in (False, True):
                with self.subTest(host=host, secure=secure), self.assertLogs("django.security.DisallowedHost", level="ERROR"):
                    response = client.get("/health/", secure=secure, HTTP_HOST=host)
                self.assertEqual(response.status_code, 400)
                self.assertIsNone(response.exc_info)
                self.assertContains(response, "Request rejected", status_code=400)
                for leak in (host, "Traceback", "AttributeError", "SELECT ", "SECRET_KEY",
                             settings.SECRET_KEY, str(settings.BASE_DIR), "site-packages", "psycopg"):
                    self.assertNotContains(response, leak, status_code=400)
        self.assertEqual(settings.ALLOWED_HOSTS, ["ccare.example.invalid"])
        self.assertEqual(client.get("/health/", secure=True, HTTP_HOST="ccare.example.invalid").status_code, 200)
        redirect = client.get("/health/", HTTP_HOST="ccare.example.invalid")
        self.assertEqual(redirect.status_code, 301)
        self.assertEqual(redirect["Location"], "https://ccare.example.invalid/health/")

    def test_missing_user_context_does_not_resolve_capabilities(self):
        from django.test import RequestFactory
        from apps.operations.context import shell
        request = RequestFactory().get("/")
        with patch("apps.operations.context.q.capability_map") as resolver:
            self.assertEqual(shell(request), {"workspace": None})
        resolver.assert_not_called()

    @override_settings(DEBUG=False, ALLOWED_HOSTS=["ccare.example.invalid"], SECURE_SSL_REDIRECT=True)
    def test_allowed_host_authenticated_workspace_still_renders(self):
        from django.contrib.auth import get_user_model
        user = get_user_model().objects.create_user(username="rc-error-rendering")
        self.client.force_login(user)
        response = self.client.get(reverse("operations:dashboard"), secure=True, HTTP_HOST="ccare.example.invalid")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.wsgi_request.user.pk, user.pk)
        self.assertIsNotNone(response.context["workspace"])

    def test_500_wording_does_not_promise_rollback(self):
        rendered = engines["django"].engine.get_template("500.html").render(Context({}))
        self.assertIn("Check its status before trying again", rendered)
        self.assertNotIn("Nothing was changed", rendered)

    def test_every_error_template_renders_with_an_empty_context(self):
        engine = engines["django"].engine
        for name in self.templates:
            with self.subTest(template=name):
                template = engine.get_template(name)
                rendered = template.render(Context({}))
                self.assertIn("<!doctype html>", rendered.lower())
                self.assertIn('name="robots" content="noindex"', rendered)

    def test_error_templates_never_reference_the_shell_or_fail_on_missing_context(self):
        for name in self.templates:
            with self.subTest(template=name):
                source = Path(settings.BASE_DIR / "templates" / name).read_text(encoding="utf-8")
                for forbidden in ("{% extends", "{% url", "{% csrf_token", "{% static", "{{ request.", "{% if"):
                    self.assertNotIn(forbidden, source)

    def test_error_templates_disclose_no_exception_or_infrastructure_detail(self):
        forbidden = ("Traceback", "Exception", "SECRET_KEY", "DATABASES", "psycopg", "Traceback (most recent call last)",
            "settings.", "MIDDLEWARE", "request.", "ccare_dev", "localhost", "127.0.0.1")
        for name in self.templates:
            with self.subTest(template=name):
                rendered = engines["django"].engine.get_template(name).render(Context({}))
                for text in forbidden:
                    self.assertNotIn(text, rendered)

    def test_missing_page_returns_404_template_without_debug_output(self):
        response = self.client.get("/no-such-address-5f2a/")
        self.assertEqual(response.status_code, 404)
        self.assertIn("Not found", response.content.decode())
        self.assertNotIn("URLconf", response.content.decode())

    def test_disallowed_method_returns_405_without_debug_output(self):
        response = self.client.post(reverse("health"))
        self.assertEqual(response.status_code, 405)
        self.assertNotIn("Traceback", response.content.decode())

    def test_permission_denied_uses_the_403_template(self):
        def refuse(request):
            raise PermissionDenied
        module = temporary_urlconf("tests.urlconf_forbidden", path("denied/", refuse))
        try:
            with override_settings(ROOT_URLCONF=module, DEBUG=False):
                response = self.client.get("/denied/")
        finally:
            del sys.modules[module]
        self.assertEqual(response.status_code, 403)
        body = response.content.decode()
        self.assertIn("Not permitted", body)
        self.assertNotIn("Traceback", body)

    def test_bad_request_uses_the_400_template(self):
        def disallow(request):
            from django.core.exceptions import SuspiciousOperation
            raise SuspiciousOperation("internal-detail-xyz")
        module = temporary_urlconf("tests.urlconf_suspicious", path("suspicious/", disallow))
        try:
            with override_settings(ROOT_URLCONF=module, DEBUG=False):
                response = self.client.get("/suspicious/")
        finally:
            del sys.modules[module]
        self.assertEqual(response.status_code, 400)
        body = response.content.decode()
        self.assertIn("Request rejected", body)
        self.assertNotIn("Traceback", body)
        self.assertNotIn("internal-detail-xyz", body)

    def test_unhandled_exception_renders_500_without_disclosure(self):
        def explode(request):
            raise RuntimeError("sensitive-internal-detail")
        module = temporary_urlconf("tests.urlconf_boom", path("boom/", explode))
        try:
            with override_settings(ROOT_URLCONF=module, DEBUG=False):
                self.client.raise_request_exception = False
                # The exception is logged for the operator but never returned.
                with self.assertLogs("django.request", level="ERROR"):
                    response = self.client.get("/boom/")
        finally:
            self.client.raise_request_exception = True
            del sys.modules[module]
        self.assertEqual(response.status_code, 500)
        body = response.content.decode()
        self.assertIn("Service error", body)
        for leak in ("sensitive-internal-detail", "RuntimeError", "Traceback", "tests.urlconf_boom"):
            self.assertNotIn(leak, body)

    def test_successful_response_is_not_affected_by_error_templates(self):
        self.assertEqual(self.client.get(reverse("health")).status_code, 200)


class LoggingSurfaceTests(SimpleTestCase):
    def test_logging_writes_only_to_the_console_never_to_a_file_or_mail(self):
        handlers = list(settings.LOGGING["handlers"].values())
        self.assertEqual({handler["class"] for handler in handlers},
            {"logging.StreamHandler"})
        configured = settings.LOGGING["handlers"]["console"].get("filename")
        self.assertIsNone(configured, "production logging must not write files directly")

    def test_security_and_request_channels_are_raised_to_error_or_warning(self):
        self.assertEqual(settings.LOGGING["loggers"]["django.request"]["level"], "ERROR")
        self.assertEqual(settings.LOGGING["loggers"]["django.security"]["level"], "WARNING")
        self.assertEqual(settings.LOGGING["loggers"]["django.db.backends"]["level"], "WARNING")

    def test_admins_are_unset_so_error_mail_cannot_leak_configuration(self):
        self.assertEqual(getattr(settings, "ADMINS", []), [])
        self.assertEqual(getattr(settings, "MANAGERS", []), [])

    def test_secret_setting_names_are_never_passed_to_a_logger(self):
        secret_names = ("EMAIL_HOST_PASSWORD", "SECRET_KEY", "DB_PASSWORD", "DEFAULT_FROM_EMAIL", "CSRF_TRUSTED_ORIGINS")
        offenders = []
        for source in (Path(settings.BASE_DIR / "apps").rglob("*.py")):
            for number, line in enumerate(source.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
                if not re.search(r"\blogger\.\w+\(", line):
                    continue
                if any(name in line for name in secret_names):
                    offenders.append(f"{source.relative_to(settings.BASE_DIR)}:{number}")
        self.assertEqual(offenders, [], f"secret setting names reached a log call: {offenders}")


class ReleaseVersionTests(SimpleTestCase):
    def test_release_identity_is_declared_once(self):
        self.assertEqual(settings.RELEASE_NAME, "C-CARE")
        self.assertEqual(settings.RELEASE_VERSION, "1.0.0-rc2")
        self.assertEqual(RELEASE, "C-CARE v1.0.0-rc2")

    def test_release_identity_is_not_exposed_over_http(self):
        response = self.client.get(reverse("health"))
        self.assertNotIn(settings.RELEASE_VERSION, response.content.decode())


class ProductionBootTests(SimpleTestCase):
    """The documented production entry points must boot with an ephemeral secret."""

    def test_wsgi_and_asgi_default_to_production_settings(self):
        for name in ("config.wsgi", "config.asgi"):
            with self.subTest(module=name):
                source = Path(settings.BASE_DIR / f"{name.replace('.', '/')}.py").read_text(encoding="utf-8")
                self.assertIn("config.settings.production", source)

    def test_manage_default_is_development_not_production(self):
        source = Path(settings.BASE_DIR / "manage.py").read_text(encoding="utf-8")
        self.assertIn("config.settings.development", source)

    def test_deployment_check_is_clean_for_a_fully_configured_operator(self):
        environment = dict(os.environ)
        environment.update({
            "DJANGO_SETTINGS_MODULE": "config.settings.production",
            "DJANGO_SECRET_KEY": EPHEMERAL_SECRET,
            "DJANGO_ALLOWED_HOSTS": "ccare.example.invalid",
            "DJANGO_HSTS_INCLUDE_SUBDOMAINS": "True",
            "DJANGO_HSTS_PRELOAD": "True",
        })
        result = subprocess.run([sys.executable, "manage.py", "check", "--deploy"], cwd=settings.BASE_DIR,
            env=environment, capture_output=True, text=True, timeout=180)
        self.assertEqual(result.returncode, 0, result.stdout + result.stderr)

    def test_default_production_keeps_only_the_whole_domain_hsts_advisories(self):
        environment = dict(os.environ)
        environment.update({
            "DJANGO_SETTINGS_MODULE": "config.settings.production",
            "DJANGO_SECRET_KEY": EPHEMERAL_SECRET,
            "DJANGO_ALLOWED_HOSTS": "ccare.example.invalid",
        })
        result = subprocess.run([sys.executable, "manage.py", "check", "--deploy"], cwd=settings.BASE_DIR,
            env=environment, capture_output=True, text=True, timeout=180)
        # Django reports deployment warnings on stderr, not stdout.
        codes = sorted(part.split(")")[0] for part in (result.stdout + result.stderr).split("(security.")[1:])
        self.assertEqual(codes, ["W005", "W021"], result.stdout + result.stderr)
