from __future__ import annotations

import unittest

from cryptography.fernet import Fernet
from pydantic import ValidationError

from govoplan_core.core.install_config import env_template, validate_runtime_configuration
from govoplan_core.settings import Settings


class InstallConfigTests(unittest.TestCase):
    def test_archive_preview_safety_defaults_are_validated(self) -> None:
        defaults = Settings()
        self.assertEqual(defaults.file_archive_max_entries, 10_000)
        self.assertEqual(
            defaults.file_archive_max_expanded_bytes,
            2 * 1024 * 1024 * 1024,
        )
        self.assertEqual(defaults.file_archive_max_expansion_ratio, 100)
        self.assertEqual(defaults.file_archive_preview_ttl_seconds, 30 * 60)

        for values in (
            {"FILE_ARCHIVE_MAX_ENTRIES": "0"},
            {"FILE_ARCHIVE_MAX_EXPANDED_BYTES": "0"},
            {"FILE_ARCHIVE_MAX_EXPANSION_RATIO": "0"},
            {"FILE_ARCHIVE_PREVIEW_TTL_SECONDS": "59"},
        ):
            with self.subTest(values=values), self.assertRaises(ValidationError):
                Settings(**values)

    def test_calendar_outbox_retention_setting_is_validated(self) -> None:
        self.assertEqual(Settings().calendar_outbox_terminal_retention_days, 90)
        self.assertEqual(
            Settings(
                CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS="0"
            ).calendar_outbox_terminal_retention_days,
            0,
        )
        with self.assertRaises(ValidationError):
            Settings(CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS="-1")

    def test_platform_event_outbox_limits_are_validated(self) -> None:
        defaults = Settings()
        self.assertEqual(defaults.platform_event_outbox_max_attempts, 8)
        self.assertEqual(
            defaults.platform_event_outbox_terminal_retention_days,
            90,
        )
        configured = Settings(
            PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS="12",
            PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS="30",
        )
        self.assertEqual(configured.platform_event_outbox_max_attempts, 12)
        self.assertEqual(
            configured.platform_event_outbox_terminal_retention_days,
            30,
        )
        for values in (
            {"PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS": "0"},
            {"PLATFORM_EVENT_OUTBOX_MAX_ATTEMPTS": "101"},
            {"PLATFORM_EVENT_OUTBOX_TERMINAL_RETENTION_DAYS": "-1"},
        ):
            with self.subTest(values=values), self.assertRaises(ValidationError):
                Settings(**values)

    def test_scheduling_cancellation_notice_setting_is_bounded(self) -> None:
        self.assertEqual(Settings().scheduling_cancellation_notice_days, 30)
        self.assertEqual(
            Settings(
                SCHEDULING_CANCELLATION_NOTICE_DAYS="14"
            ).scheduling_cancellation_notice_days,
            14,
        )
        for invalid in ("0", "91"):
            with self.subTest(invalid=invalid), self.assertRaises(ValidationError):
                Settings(SCHEDULING_CANCELLATION_NOTICE_DAYS=invalid)

    def test_self_hosted_validation_reports_actionable_missing_settings(self) -> None:
        result = validate_runtime_configuration({}, profile="self-hosted")

        self.assertFalse(result.ok)
        errors = {issue.key: issue for issue in result.errors}
        self.assertIn("APP_ENV", errors)
        self.assertIn("DATABASE_URL", errors)
        self.assertIn("MASTER_KEY_B64", errors)
        self.assertIn("ENABLED_MODULES", errors)
        self.assertIn("CORS_ORIGINS", errors)
        self.assertIn("Set DATABASE_URL", errors["DATABASE_URL"].action)

    def test_self_hosted_validation_accepts_explicit_postgres_configuration(self) -> None:
        result = validate_runtime_configuration(
            {
                "APP_ENV": "production",
                "GOVOPLAN_INSTALL_PROFILE": "self-hosted",
                "DATABASE_URL": "postgresql+psycopg://govoplan:secret@db.example.internal:5432/govoplan",
                "GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://govoplan:secret@db.example.internal:5432/govoplan",
                "MASTER_KEY_B64": Fernet.generate_key().decode("ascii"),
                "ENABLED_MODULES": "access,admin,policy,audit",
                "CELERY_ENABLED": "true",
                "REDIS_URL": "redis://redis.example.internal:6379/0",
                "CORS_ORIGINS": "https://govoplan.example.org",
                "GOVOPLAN_TRUSTED_HOSTS": "govoplan.example.org",
                "AUTH_COOKIE_SECURE": "true",
                "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "false",
                "FILE_STORAGE_BACKEND": "local",
                "FILE_STORAGE_LOCAL_ROOT": "/var/lib/govoplan/files",
            },
            profile="self-hosted",
        )

        self.assertTrue(result.ok, result.to_text())
        self.assertEqual([], [issue.key for issue in result.errors])

    def test_production_validation_rejects_dev_bootstrap_and_sqlite(self) -> None:
        result = validate_runtime_configuration(
            {
                "APP_ENV": "production",
                "DATABASE_URL": "sqlite:////tmp/govoplan.db",
                "MASTER_KEY_B64": Fernet.generate_key().decode("ascii"),
                "ENABLED_MODULES": "access,admin",
                "DEV_BOOTSTRAP_ENABLED": "true",
                "CORS_ORIGINS": "https://govoplan.example.org",
                "AUTH_COOKIE_SECURE": "true",
                "FILE_STORAGE_BACKEND": "local",
                "FILE_STORAGE_LOCAL_ROOT": "/var/lib/govoplan/files",
            },
            profile="self-hosted",
        )

        error_keys = {issue.key for issue in result.errors}
        self.assertIn("DATABASE_URL", error_keys)
        self.assertIn("DEV_BOOTSTRAP_ENABLED", error_keys)

    def test_production_validation_rejects_wildcard_host_and_proxy_trust(self) -> None:
        result = validate_runtime_configuration(
            {
                "APP_ENV": "production",
                "GOVOPLAN_TRUSTED_HOSTS": "*",
                "FORWARDED_ALLOW_IPS": "*",
                "GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "false",
            },
            profile="self-hosted",
        )

        error_keys = {issue.key for issue in result.errors}
        self.assertIn("GOVOPLAN_TRUSTED_HOSTS", error_keys)
        self.assertIn("FORWARDED_ALLOW_IPS", error_keys)

    def test_managed_s3_trust_is_limited_to_installer_garage(self) -> None:
        base = {
            "APP_ENV": "development",
            "FILE_STORAGE_BACKEND": "s3",
            "FILE_STORAGE_S3_REGION": "garage",
            "FILE_STORAGE_S3_ACCESS_KEY_ID": "access",
            "FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret",
            "FILE_STORAGE_S3_BUCKET": "files",
            "FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "true",
        }
        accepted = validate_runtime_configuration(
            {
                **base,
                "FILE_STORAGE_S3_ENDPOINT_URL": "http://garage:3900",
            },
            profile="development",
        )
        rejected = validate_runtime_configuration(
            {
                **base,
                "FILE_STORAGE_S3_ENDPOINT_URL": "http://other-s3:3900",
            },
            profile="development",
        )

        self.assertNotIn(
            "FILE_STORAGE_S3_ENDPOINT_URL",
            {issue.key for issue in accepted.errors},
        )
        self.assertIn(
            "FILE_STORAGE_S3_ENDPOINT_URL",
            {issue.key for issue in rejected.errors},
        )
        self.assertTrue(
            Settings(
                FILE_STORAGE_S3_DEPLOYMENT_MANAGED="true"
            ).file_storage_s3_deployment_managed
        )

    def test_external_s3_requires_explicit_https_deployment_trust(self) -> None:
        base = {
            "APP_ENV": "development",
            "FILE_STORAGE_BACKEND": "s3",
            "FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
            "FILE_STORAGE_S3_REGION": "eu-test-1",
            "FILE_STORAGE_S3_ACCESS_KEY_ID": "access",
            "FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret",
            "FILE_STORAGE_S3_BUCKET": "files",
            "FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "false",
        }
        rejected = validate_runtime_configuration(base, profile="development")
        accepted = validate_runtime_configuration(
            {**base, "FILE_STORAGE_S3_ENDPOINT_TRUSTED": "true"},
            profile="development",
        )
        insecure = validate_runtime_configuration(
            {
                **base,
                "FILE_STORAGE_S3_ENDPOINT_URL": "http://s3.example.test",
                "FILE_STORAGE_S3_ENDPOINT_TRUSTED": "true",
            },
            profile="development",
        )

        self.assertIn(
            "FILE_STORAGE_S3_ENDPOINT_TRUSTED",
            {issue.key for issue in rejected.errors},
        )
        self.assertNotIn(
            "FILE_STORAGE_S3_ENDPOINT_TRUSTED",
            {issue.key for issue in accepted.errors},
        )
        self.assertIn(
            "FILE_STORAGE_S3_ENDPOINT_URL",
            {issue.key for issue in insecure.errors},
        )

    def test_connector_deployment_allowlists_require_exact_names_and_absolute_paths(self) -> None:
        result = validate_runtime_configuration(
            {
                "APP_ENV": "development",
                "GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST": "VALID_SECRET,invalid-name",
                "GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST": "relative/connector-ca.pem",
            },
            profile="development",
        )

        error_keys = {issue.key for issue in result.errors}
        self.assertIn("GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST", error_keys)
        self.assertIn("GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST", error_keys)

    def test_env_templates_surface_install_profiles(self) -> None:
        self_hosted = env_template(profile="self-hosted")
        production_like = env_template(profile="production-like", generate_secrets=True)

        self.assertIn("GOVOPLAN_INSTALL_PROFILE=self-hosted", self_hosted)
        self.assertIn("MASTER_KEY_B64=<generate", self_hosted)
        self.assertIn("CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90", self_hosted)
        self.assertIn("SCHEDULING_CANCELLATION_NOTICE_DAYS=30", self_hosted)
        self.assertIn("GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false", self_hosted)
        self.assertIn("GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST=", self_hosted)
        self.assertIn("GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST=", self_hosted)
        self.assertIn("GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org", self_hosted)
        self.assertIn("GOVOPLAN_HTTP_HSTS_SECONDS=31536000", self_hosted)
        self.assertIn("AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10", self_hosted)
        self.assertIn("FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false", self_hosted)
        self.assertIn("FILE_STORAGE_S3_ENDPOINT_TRUSTED=false", self_hosted)
        self.assertIn("GOVOPLAN_INSTALL_PROFILE=production-like", production_like)
        self.assertNotIn("MASTER_KEY_B64=<generate", production_like)
        self.assertIn("CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90", production_like)
        self.assertIn("SCHEDULING_CANCELLATION_NOTICE_DAYS=30", production_like)
        self.assertIn("GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=true", production_like)
        self.assertIn("GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST=", production_like)
        self.assertIn("GOVOPLAN_CONNECTOR_CA_BUNDLE_ALLOWLIST=", production_like)
        self.assertIn("GOVOPLAN_TRUSTED_HOSTS=127.0.0.1,localhost,testserver", production_like)
        self.assertIn("GOVOPLAN_HTTP_HSTS_SECONDS=0", production_like)
        self.assertIn("AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10", production_like)
        self.assertIn(
            "FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false",
            production_like,
        )
        self.assertIn(
            "FILE_STORAGE_S3_ENDPOINT_TRUSTED=false",
            production_like,
        )


if __name__ == "__main__":
    unittest.main()
