from __future__ import annotations

import io
import json
import os
import shutil
import tempfile
import time
import unittest
import zipfile
from datetime import datetime, timezone
from email import policy
from email.parser import BytesParser
from pathlib import Path
from unittest.mock import patch

import pyzipper

# Settings are instantiated while importing the application. Configure an
# isolated test database and local storage before importing app modules.
_TEST_ROOT = Path(tempfile.mkdtemp(prefix="multi-seal-mail-tests-"))
os.environ["APP_ENV"] = "test"
os.environ["DATABASE_URL"] = f"sqlite:///{_TEST_ROOT / 'test.db'}"
os.environ["FILE_STORAGE_BACKEND"] = "local"
os.environ["FILE_STORAGE_LOCAL_ROOT"] = str(_TEST_ROOT / "files")
os.environ["MOCK_MAILBOX_DIR"] = str(_TEST_ROOT / "mock-mailbox")
os.environ["DEV_BOOTSTRAP_ENABLED"] = "false"

from govoplan_core.settings import Settings, settings as core_settings

# unittest discovery imports other contract modules before this smoke module.
# Refresh the shared settings object in place so modules that already imported
# it observe this test's isolated environment before the default app is built.
_isolated_settings = Settings()
for _field_name in Settings.model_fields:
    setattr(core_settings, _field_name, getattr(_isolated_settings, _field_name))

from alembic import command
from fastapi.testclient import TestClient

from govoplan_core.db.base import Base
from govoplan_core.db.bootstrap import bootstrap_dev_data
from govoplan_core.db.migrations import alembic_config
from govoplan_core.db.session import configure_database, set_database
from govoplan_core.core.change_sequence import decode_sequence_watermark, prune_sequence_entries
from govoplan_core.core.pagination import encode_keyset_cursor, keyset_query_fingerprint
from govoplan_core.tenancy.scope import create_scope_tables, scope_registry
from govoplan_access.backend.permissions.catalog import permission_catalog as access_permission_catalog

_database = configure_database(os.environ["DATABASE_URL"])
engine = _database.engine
SessionLocal = _database.SessionLocal

from govoplan_core.server.app import app
from govoplan_campaign.backend.sending.execution import SNAPSHOT_VERSION


class ApiSmokeTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.client = TestClient(app)

    @classmethod
    def tearDownClass(cls) -> None:
        cls.client.close()
        engine.dispose()
        shutil.rmtree(_TEST_ROOT, ignore_errors=True)

    def setUp(self) -> None:
        set_database(_database)
        self.client.cookies.clear()
        scope_registry.metadata.drop_all(bind=engine)
        Base.metadata.drop_all(bind=engine)
        create_scope_tables(engine)
        Base.metadata.create_all(bind=engine)
        # This suite builds the current ORM schema directly. Record the matching
        # release heads so module-lifecycle tests can safely run Alembic without
        # misclassifying the create_all database as a historic dev revision.
        if not getattr(type(self), "_migration_heads_stamped", False):
            command.stamp(
                alembic_config(database_url=os.environ["DATABASE_URL"]),
                "heads",
                purge=True,
            )
            type(self)._migration_heads_stamped = True
        shutil.rmtree(_TEST_ROOT / "files", ignore_errors=True)
        shutil.rmtree(_TEST_ROOT / "mock-mailbox", ignore_errors=True)
        with SessionLocal() as session:
            bootstrap_dev_data(
                session,
                api_key_secret="test-api-key",
                user_password="test-admin",
            )

    def _login(self) -> tuple[dict[str, str], dict[str, object]]:
        response = self.client.post(
            "/api/v1/auth/login",
            json={"email": "admin@example.local", "password": "test-admin"},
        )
        self.assertEqual(response.status_code, 200, response.text)
        payload = response.json()
        return {"Authorization": f"Bearer {payload['access_token']}"}, payload

    def _create_test_mail_profile(
        self,
        headers: dict[str, str],
        *,
        name: str,
        include_imap: bool = False,
    ) -> str:
        credentials = {
            "smtp": {"username": "sender@example.org", "password": "test-secret"},
        }
        payload: dict[str, object] = {
            "name": name,
            "smtp": {
                "host": "mock.smtp",
                "port": 2525,
                "security": "starttls",
            },
            "credentials": credentials,
        }
        if include_imap:
            payload["imap"] = {
                "host": "mock.imap",
                "port": 993,
                "security": "tls",
                "sent_folder": "Sent",
            }
            credentials["imap"] = {"username": "sender@example.org", "password": "test-secret"}
        response = self.client.post("/api/v1/mail/profiles", headers=headers, json=payload)
        self.assertEqual(response.status_code, 201, response.text)
        return str(response.json()["id"])

    def _campaign_version_precondition(
        self,
        headers: dict[str, str],
        campaign_id: str,
        version_id: str,
    ) -> tuple[dict[str, str], int]:
        response = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=headers,
        )
        self.assertEqual(response.status_code, 200, response.text)
        etag = response.headers.get("etag")
        self.assertIsNotNone(etag)
        return (
            {**headers, "If-Match": str(etag)},
            int(response.json()["edit_revision"]),
        )

    def test_recipient_import_mapping_profiles_are_db_backed(self) -> None:
        headers, _ = self._login()
        payload = {
            "name": "ERP export",
            "columnCount": 3,
            "headers": ["email", "name", "field.customer_id"],
            "normalizedHeaders": ["email", "name", "field_customer_id"],
            "orderedHeaderFingerprint": "ordered-123",
            "unorderedHeaderFingerprint": "unordered-123",
            "delimiter": ";",
            "headerRows": 1,
            "quoted": True,
            "valueSeparators": ",;|",
            "mappings": [
                {"columnIndex": 0, "kind": "to"},
                {"columnIndex": 1, "kind": "name"},
                {"columnIndex": 2, "kind": "new_field", "newFieldName": "customer_id"},
            ],
        }

        created = self.client.post("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers, json=payload)
        self.assertEqual(created.status_code, 201, created.text)
        created_body = created.json()
        self.assertEqual(created_body["name"], "ERP export")
        self.assertEqual(created_body["columnCount"], 3)
        self.assertEqual(created_body["mappings"][2]["newFieldName"], "customer_id")
        profile_id = created_body["id"]

        listed = self.client.get("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers)
        self.assertEqual(listed.status_code, 200, listed.text)
        self.assertEqual([profile["id"] for profile in listed.json()["profiles"]], [profile_id])

        updated_payload = {**payload, "name": "ERP export updated", "valueSeparators": "|"}
        updated = self.client.put(
            f"/api/v1/campaigns/recipient-import/mapping-profiles/{profile_id}",
            headers=headers,
            json=updated_payload,
        )
        self.assertEqual(updated.status_code, 200, updated.text)
        self.assertEqual(updated.json()["name"], "ERP export updated")
        self.assertEqual(updated.json()["valueSeparators"], "|")

        deleted = self.client.delete(f"/api/v1/campaigns/recipient-import/mapping-profiles/{profile_id}", headers=headers)
        self.assertEqual(deleted.status_code, 204, deleted.text)

        listed_after_delete = self.client.get("/api/v1/campaigns/recipient-import/mapping-profiles", headers=headers)
        self.assertEqual(listed_after_delete.status_code, 200, listed_after_delete.text)
        self.assertEqual(listed_after_delete.json()["profiles"], [])

    def test_campaign_entries_store_recipient_import_provenance(self) -> None:
        headers, _ = self._login()
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "import-provenance", "name": "Import provenance", "mode": "test"},
            "fields": [{"name": "department", "type": "string", "required": False}],
            "global_values": {},
            "server": {},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Hello", "text": "Hello"},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {
                "inline": [
                    {
                        "id": "recipient-1",
                        "to": [{"email": "recipient@example.org", "type": "to"}],
                        "fields": {"department": "Finance"},
                    }
                ],
                "imports": [
                    {
                        "id": "recipient-import-test",
                        "imported_at": "2026-06-26T12:00:00Z",
                        "mode": "replace",
                        "source_type": "xlsx",
                        "filename": "recipients.xlsx",
                        "sheet_name": "Recipients",
                        "encoding": None,
                        "delimiter": None,
                        "header_rows": 1,
                        "quoted": None,
                        "value_separators": ",;|",
                        "rows_total": 1,
                        "valid_rows": 1,
                        "invalid_rows": 0,
                        "imported_rows": 1,
                        "field_names_created": [],
                        "attachment_patterns": 0,
                        "mapping": [
                            {"column_index": 0, "header": "email", "kind": "to"},
                            {"column_index": 1, "header": "field.department", "kind": "field", "field_name": "department"},
                        ],
                    }
                ],
            },
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"rate_limit": {"messages_per_minute": 60}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        detail = self.client.get(f"/api/v1/campaigns/{campaign_id}/versions/{version_id}", headers=headers)
        self.assertEqual(detail.status_code, 200, detail.text)
        stored_import = detail.json()["raw_json"]["entries"]["imports"][0]
        self.assertEqual(stored_import["source_type"], "xlsx")
        self.assertEqual(stored_import["sheet_name"], "Recipients")
        self.assertEqual(stored_import["mapping"][1]["field_name"], "department")

        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)

    def _create_built_delivery_campaign(
        self,
        headers: dict[str, str],
        *,
        external_id: str,
        recipient_count: int = 1,
    ) -> tuple[str, str]:
        mail_profile_id = self._create_test_mail_profile(headers, name=f"{external_id} delivery")
        entries = [
            {
                "id": f"recipient-{index + 1}",
                "to": [{"email": f"recipient-{index + 1}@example.org", "type": "to"}],
                "fields": {"first_name": f"Recipient {index + 1}"},
            }
            for index in range(recipient_count)
        ]
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": external_id, "name": external_id, "mode": "test"},
            "fields": [{"name": "first_name", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {
                "subject": "Hello ${local::first_name}",
                "text": "Hello ${local::first_name}.",
            },
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {"inline": entries},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {
                "rate_limit": {"messages_per_minute": 60},
                "retry": {"max_attempts": 3, "backoff_seconds": [1, 5, 30]},
                "imap_append_sent": {"enabled": False},
            },
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]
        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": False},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/build",
            headers=headers,
            json={"write_eml": True},
        )
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["built_count"], recipient_count, built.text)
        return campaign_id, version_id

    def _create_role(self, headers: dict[str, str], *, slug: str, permissions: list[str]) -> dict[str, object]:
        response = self.client.post(
            "/api/v1/admin/roles",
            headers=headers,
            json={"slug": slug, "name": slug.replace("-", " ").title(), "description": "Test role", "permissions": permissions},
        )
        self.assertEqual(response.status_code, 201, response.text)
        return response.json()

    def _create_user(
        self,
        headers: dict[str, str],
        *,
        email: str,
        password: str,
        role_ids: list[str],
        group_ids: list[str] | None = None,
    ) -> dict[str, object]:
        response = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": email,
                "display_name": email.split("@", 1)[0].replace("-", " ").title(),
                "password": password,
                "password_reset_required": False,
                "is_active": True,
                "group_ids": group_ids or [],
                "role_ids": role_ids,
            },
        )
        self.assertEqual(response.status_code, 201, response.text)
        return response.json()["user"]

    def _login_as(self, *, email: str, password: str, tenant_slug: str = "default") -> tuple[dict[str, str], dict[str, object]]:
        response = self.client.post(
            "/api/v1/auth/login",
            json={"email": email, "password": password, "tenant_slug": tenant_slug},
        )
        self.assertEqual(response.status_code, 200, response.text)
        payload = response.json()
        return {"Authorization": f"Bearer {payload['access_token']}"}, payload

    def _create_system_approver(self, headers: dict[str, str], *, tenant_id: str, email: str = "approver@example.local") -> dict[str, str]:
        roles = self.client.get("/api/v1/admin/system/roles", headers=headers)
        self.assertEqual(roles.status_code, 200, roles.text)
        system_admin = next(item for item in roles.json()["roles"] if item["slug"] == "system_admin")
        maintenance_operator = next(item for item in roles.json()["roles"] if item["slug"] == "maintenance_operator")
        created = self.client.post(
            "/api/v1/admin/system/accounts",
            headers=headers,
            json={
                "email": email,
                "display_name": "Configuration Approver",
                "password": "approver-password",
                "password_reset_required": False,
                "is_active": True,
                "role_ids": [system_admin["id"], maintenance_operator["id"]],
                "memberships": [{
                    "tenant_id": tenant_id,
                    "is_active": True,
                    "role_ids": [],
                    "group_ids": [],
                }],
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        approver_headers, _ = self._login_as(email=email, password="approver-password")
        return approver_headers

    def _approved_configuration_change(
        self,
        headers: dict[str, str],
        approver_headers: dict[str, str],
        *,
        key: str,
        value: object,
        target: dict[str, object] | None = None,
    ) -> str:
        created = self.client.post(
            "/api/v1/admin/configuration-change-requests",
            headers=headers,
            json={"key": key, "value": value, "dry_run": True, "target": target or {}, "reason": "test approval"},
        )
        self.assertEqual(created.status_code, 201, created.text)
        request_id = created.json()["request"]["id"]
        approved = self.client.post(
            f"/api/v1/admin/configuration-change-requests/{request_id}/approve",
            headers=approver_headers,
            json={"reason": "approved by second system administrator"},
        )
        self.assertEqual(approved.status_code, 200, approved.text)
        self.assertEqual(approved.json()["request"]["status"], "approved")
        return request_id


    def test_cookie_session_requires_csrf_for_mutations(self) -> None:
        response = self.client.post(
            "/api/v1/auth/login",
            json={"email": "admin@example.local", "password": "test-admin"},
        )
        self.assertEqual(response.status_code, 200, response.text)
        csrf = response.cookies.get("govoplan_csrf")
        self.assertTrue(csrf)

        session_summary = self.client.get("/api/v1/auth/session")
        self.assertEqual(session_summary.status_code, 200, session_summary.text)
        session_payload = session_summary.json()
        self.assertTrue(session_payload["authenticated"])
        self.assertEqual(session_payload["auth_method"], "session")
        self.assertNotIn("scopes", session_payload)
        self.assertNotIn("roles", session_payload)
        self.assertNotIn("groups", session_payload)

        shell_summary = self.client.get("/api/v1/auth/shell")
        self.assertEqual(shell_summary.status_code, 200, shell_summary.text)
        shell_payload = shell_summary.json()
        self.assertEqual(shell_payload["principal"]["auth_method"], "session")
        self.assertFalse(shell_payload["profile_loaded"])
        self.assertFalse(shell_payload["roles_loaded"])
        self.assertFalse(shell_payload["groups_loaded"])
        self.assertIsInstance(shell_payload["scopes"], list)
        self.assertTrue(shell_payload["scopes"])
        self.assertTrue(shell_payload["tenants"])
        self.assertTrue(all(membership["roles"] == [] for membership in shell_payload["tenants"]))
        self.assertNotIn("roles", shell_payload)
        self.assertNotIn("groups", shell_payload)
        self.assertNotIn("available_languages", shell_payload)
        self.assertNotIn("default_language", shell_payload)

        profile_summary = self.client.get("/api/v1/auth/profile")
        self.assertEqual(profile_summary.status_code, 200, profile_summary.text)
        profile_payload = profile_summary.json()
        self.assertTrue(profile_payload["profile_loaded"])
        self.assertEqual(profile_payload["user"]["id"], session_payload["user"]["id"])
        self.assertIn("available_languages", profile_payload)
        self.assertIn("default_language", profile_payload)
        self.assertNotIn("roles", profile_payload)
        self.assertNotIn("groups", profile_payload)

        roles_summary = self.client.get("/api/v1/auth/roles")
        self.assertEqual(roles_summary.status_code, 200, roles_summary.text)
        roles_payload = roles_summary.json()
        self.assertTrue(roles_payload["roles_loaded"])
        self.assertTrue(roles_payload["roles"])
        self.assertNotIn("groups", roles_payload)

        groups_summary = self.client.get("/api/v1/auth/groups")
        self.assertEqual(groups_summary.status_code, 200, groups_summary.text)
        groups_payload = groups_summary.json()
        self.assertTrue(groups_payload["groups_loaded"])
        self.assertIsInstance(groups_payload["groups"], list)
        self.assertNotIn("roles", groups_payload)

        api_key_session = self.client.get("/api/v1/auth/session", headers={"X-API-Key": "test-api-key"})
        self.assertEqual(api_key_session.status_code, 200, api_key_session.text)
        self.assertEqual(api_key_session.json()["auth_method"], "api_key")

        api_key_shell = self.client.get("/api/v1/auth/shell", headers={"X-API-Key": "test-api-key"})
        self.assertEqual(api_key_shell.status_code, 200, api_key_shell.text)
        api_key_shell_payload = api_key_shell.json()
        self.assertEqual(api_key_shell_payload["principal"]["auth_method"], "api_key")
        self.assertFalse(api_key_shell_payload["profile_loaded"])
        self.assertFalse(api_key_shell_payload["roles_loaded"])
        self.assertFalse(api_key_shell_payload["groups_loaded"])
        self.assertIsInstance(api_key_shell_payload["scopes"], list)

        me = self.client.get("/api/v1/auth/me")
        self.assertEqual(me.status_code, 200, me.text)
        me_payload = me.json()
        self.assertEqual(session_payload["user"]["id"], me_payload["user"]["id"])
        self.assertEqual(session_payload["tenant"]["id"], me_payload["tenant"]["id"])
        self.assertEqual(shell_payload["user"]["id"], me_payload["user"]["id"])
        self.assertEqual(shell_payload["tenant"]["id"], me_payload["tenant"]["id"])
        self.assertTrue(me_payload["profile_loaded"])
        self.assertEqual(me_payload["principal"]["account_id"], me_payload["user"]["account_id"])
        self.assertEqual(me_payload["principal"]["membership_id"], me_payload["user"]["id"])
        self.assertEqual(me_payload["principal"]["tenant_id"], me_payload["tenant"]["id"])
        self.assertEqual(me_payload["principal"]["auth_method"], "session")
        self.assertTrue(set(me_payload["principal"]["scopes"]).issuperset(set(me_payload["scopes"])))

        blocked = self.client.patch("/api/v1/auth/profile", json={"display_name": "Blocked"})
        self.assertEqual(blocked.status_code, 403, blocked.text)

        allowed = self.client.patch(
            "/api/v1/auth/profile",
            headers={"X-CSRF-Token": csrf or ""},
            json={"display_name": "Cookie Admin"},
        )
        self.assertEqual(allowed.status_code, 200, allowed.text)
        self.assertEqual(allowed.json()["user"]["display_name"], "Cookie Admin")

    def test_mailbox_message_listing_reports_total_count(self) -> None:
        headers, login = self._login()
        created_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Readable mailbox",
                "smtp": {
                    "host": "mock.smtp",
                    "port": 2525,
                    "security": "starttls",
                },
                "imap": {
                    "host": "mock.imap",
                    "port": 993,
                    "security": "tls",
                    "sent_folder": "Sent",
                },
                "credentials": {
                    "smtp": {"username": "sender@example.org", "password": "smtp-secret"},
                    "imap": {"username": "sender@example.org", "password": "imap-secret"},
                },
            },
        )
        self.assertEqual(created_profile.status_code, 201, created_profile.text)
        profile_id = created_profile.json()["id"]

        from govoplan_mail.backend.dev.mock_mailbox import record_imap_append

        for index in range(3):
            record_imap_append(
                f"Subject: Mock message {index + 1}\nFrom: sender@example.org\nTo: recipient@example.org\n\nBody {index + 1}".encode("utf-8"),
                folder="INBOX",
                imap_host="mock.imap",
            )

        bootstrapped = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/bootstrap",
            headers=headers,
            params={"folder": "INBOX", "limit": 2},
        )
        self.assertEqual(bootstrapped.status_code, 200, bootstrapped.text)
        bootstrap_payload = bootstrapped.json()
        self.assertEqual(bootstrap_payload["folder"], "INBOX")
        self.assertTrue(bootstrap_payload["folders"]["ok"])
        self.assertFalse(bootstrap_payload["folders"]["from_cache"])
        self.assertFalse(bootstrap_payload["messages"]["from_cache"])
        self.assertEqual(bootstrap_payload["messages"]["total_count"], 3)
        self.assertEqual(len(bootstrap_payload["messages"]["messages"]), 2)

        listed = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
            headers=headers,
            params={"folder": "INBOX", "limit": 2},
        )
        self.assertEqual(listed.status_code, 200, listed.text)
        payload = listed.json()
        self.assertEqual(payload["folder"], "INBOX")
        self.assertEqual(payload["total_count"], 3)
        self.assertEqual(len(payload["messages"]), 2)
        self.assertTrue(payload["from_cache"])
        self.assertTrue(all(message["folder"] == "INBOX" for message in payload["messages"]))
        self.assertTrue(payload["cursor_stable"])
        self.assertFalse(payload["full"])
        self.assertTrue(payload["next_cursor"])

        next_page = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
            headers=headers,
            params={"folder": "INBOX", "cursor": payload["next_cursor"]},
        )
        self.assertEqual(next_page.status_code, 200, next_page.text)
        next_payload = next_page.json()
        self.assertEqual(next_payload["offset"], 2)
        self.assertEqual(len(next_payload["messages"]), 1)
        self.assertFalse(next_payload["next_cursor"])

        cached_next_page = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
            headers=headers,
            params={"folder": "INBOX", "cursor": payload["next_cursor"]},
        )
        self.assertEqual(cached_next_page.status_code, 200, cached_next_page.text)
        self.assertTrue(cached_next_page.json()["from_cache"])

        stale_cursor = encode_keyset_cursor(
            "mail.mailbox.messages.v1",
            fingerprint=keyset_query_fingerprint(
                "mail.mailbox.messages.v1",
                {
                    "tenant_id": login["tenant"]["id"],
                    "profile_id": profile_id,
                    "folder": "INBOX",
                    "limit": 2,
                    "sort": "imap.uid.desc",
                },
            ),
            values={
                "offset": 2,
                "limit": 2,
                "uidvalidity": "stale-mock-uidvalidity",
                "after_uid": payload["messages"][-1]["uid"],
            },
        )
        stale_page = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
            headers=headers,
            params={"folder": "INBOX", "cursor": stale_cursor},
        )
        self.assertEqual(stale_page.status_code, 200, stale_page.text)
        stale_payload = stale_page.json()
        self.assertTrue(stale_payload["full"])
        self.assertEqual(stale_payload["offset"], 0)
        self.assertEqual(len(stale_payload["messages"]), 2)
        self.assertTrue(stale_payload["next_cursor"])

        mismatched_cursor = self.client.get(
            f"/api/v1/mail/profiles/{profile_id}/mailbox/messages",
            headers=headers,
            params={"folder": "Sent", "cursor": payload["next_cursor"]},
        )
        self.assertEqual(mismatched_cursor.status_code, 400, mismatched_cursor.text)

    def test_mail_settings_delta_tracks_profiles_and_policy(self) -> None:
        headers, _ = self._login()
        full = self.client.get(
            "/api/v1/mail/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_inactive": "true"},
        )
        self.assertEqual(full.status_code, 200, full.text)
        full_payload = full.json()
        self.assertTrue(full_payload["full"])

        created_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Delta mail settings",
                "smtp": {
                    "host": "mock.smtp",
                    "port": 2525,
                    "security": "starttls",
                },
            },
        )
        self.assertEqual(created_profile.status_code, 201, created_profile.text)
        profile_id = created_profile.json()["id"]

        profile_delta = self.client.get(
            "/api/v1/mail/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_inactive": "true", "since": full_payload["watermark"]},
        )
        self.assertEqual(profile_delta.status_code, 200, profile_delta.text)
        profile_delta_payload = profile_delta.json()
        self.assertFalse(profile_delta_payload["full"])
        self.assertIn("profiles", profile_delta_payload["changed_sections"])
        self.assertEqual({profile["id"] for profile in profile_delta_payload["profiles"]}, {profile_id})

        updated_policy = self.client.put(
            "/api/v1/mail/policies/tenant",
            headers=headers,
            json={"policy": {"allow_user_profiles": False}},
        )
        self.assertEqual(updated_policy.status_code, 200, updated_policy.text)

        policy_delta = self.client.get(
            "/api/v1/mail/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_inactive": "true", "since": profile_delta_payload["watermark"]},
        )
        self.assertEqual(policy_delta.status_code, 200, policy_delta.text)
        policy_delta_payload = policy_delta.json()
        self.assertFalse(policy_delta_payload["full"])
        self.assertIn("policy", policy_delta_payload["changed_sections"])
        self.assertEqual(policy_delta_payload["policy"]["effective_policy"]["allow_user_profiles"], False)

    def test_encrypted_mail_profile_can_back_campaign_without_exposing_secrets(self) -> None:
        headers, _ = self._login()
        created_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Primary sender",
                "smtp": {
                    "host": "mock.smtp",
                    "port": 2525,
                    "security": "starttls",
                },
                "imap": {
                    "host": "mock.imap",
                    "port": 993,
                    "security": "tls",
                    "sent_folder": "Sent",
                },
                "credentials": {
                    "smtp": {"username": "sender@example.org", "password": "smtp-secret"},
                    "imap": {"username": "sender@example.org", "password": "imap-secret"},
                },
            },
        )
        self.assertEqual(created_profile.status_code, 201, created_profile.text)
        profile_payload = created_profile.json()
        self.assertNotIn("password", profile_payload["smtp"])
        self.assertNotIn("username", profile_payload["smtp"])
        self.assertNotIn("password", profile_payload["imap"])
        self.assertNotIn("username", profile_payload["imap"])
        self.assertEqual(profile_payload["credentials"]["smtp"]["username"], "sender@example.org")
        self.assertEqual(profile_payload["credentials"]["imap"]["username"], "sender@example.org")
        profile_id = profile_payload["id"]

        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "profile-backed", "name": "Profile backed", "mode": "test"},
            "fields": [{"name": "first_name", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "to": [{"email": "recipient@example.org", "type": "to"}],
            },
            "template": {"subject": "Hello ${local::first_name}", "text": "Hello ${local::first_name}."},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {"inline": [{"id": "recipient-1", "fields": {"first_name": "Recipient"}}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {
                "rate_limit": {"messages_per_minute": 60},
                "retry": {"max_attempts": 3, "backoff_seconds": [1, 5, 30]},
                "imap_append_sent": {"enabled": False},
            },
            "status_tracking": {"enabled": True},
        }
        created_campaign = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created_campaign.status_code, 200, created_campaign.text)
        version_id = created_campaign.json()["version"]["id"]

        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": False})
        self.assertEqual(built.status_code, 200, built.text)

        from govoplan_campaign.backend.db.models import CampaignVersion
        from govoplan_mail.backend.db.models import MailServerProfile

        with SessionLocal() as session:
            profile = session.get(MailServerProfile, profile_id)
            self.assertIsNotNone(profile)
            assert profile is not None
            self.assertNotEqual(profile.smtp_password_encrypted, "smtp-secret")
            self.assertNotEqual(profile.imap_password_encrypted, "imap-secret")
            self.assertEqual(profile.smtp_username, "sender@example.org")
            self.assertEqual(profile.imap_username, "sender@example.org")
            self.assertNotIn("password", profile.smtp_config)
            self.assertNotIn("username", profile.smtp_config)
            self.assertNotIn("password", profile.imap_config or {})
            self.assertNotIn("username", profile.imap_config or {})

            version = session.get(CampaignVersion, version_id)
            self.assertIsNotNone(version)
            assert version is not None
            self.assertEqual(version.raw_json["server"], {"mail_profile_id": profile_id})
            snapshot = version.execution_snapshot or {}
            self.assertEqual(snapshot["snapshot_version"], SNAPSHOT_VERSION)
            self.assertEqual(snapshot["mail_profile_id"], profile_id)
            self.assertNotIn("smtp", snapshot)
            self.assertNotIn("imap", snapshot)
            self.assertTrue(snapshot["smtp_transport_revision"])
            self.assertTrue(snapshot["imap_transport_revision"])

        deleted = self.client.delete(f"/api/v1/mail/profiles/{profile_id}", headers=headers)
        self.assertEqual(deleted.status_code, 200, deleted.text)
        self.assertFalse(deleted.json()["is_active"])
        # Reusable hierarchy credentials outlive a deactivated profile. The
        # profile's retired legacy secret columns are still scrubbed below.
        self.assertTrue(deleted.json()["smtp_password_configured"])
        self.assertTrue(deleted.json()["imap_password_configured"])

        from govoplan_audit.backend.db.models import AuditLog

        with SessionLocal() as session:
            profile = session.get(MailServerProfile, profile_id)
            self.assertIsNotNone(profile)
            assert profile is not None
            self.assertIsNone(profile.smtp_password_encrypted)
            self.assertIsNone(profile.imap_password_encrypted)
            audit = (
                session.query(AuditLog)
                .filter(
                    AuditLog.action == "mail.profile_credentials_deleted",
                    AuditLog.object_id == profile_id,
                )
                .one()
            )
            self.assertEqual(audit.details["deleted_protocols"], ["smtp", "imap"])
            self.assertEqual(audit.details["deletion_reason"], "profile_deactivated")
            self.assertNotIn("secret", repr(audit.details))

    def test_campaign_runtime_does_not_receive_mail_owned_profile_credentials(self) -> None:
        headers, _ = self._login()
        created_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Shared host local credentials",
                "smtp": {
                    "host": "mock.smtp",
                    "port": 2525,
                    "security": "starttls",
                },
                "imap": {
                    "host": "mock.imap",
                    "port": 993,
                    "security": "tls",
                    "sent_folder": "Sent",
                },
                "credentials": {
                    "smtp": {"username": "profile-smtp@example.org", "password": "profile-smtp-secret"},
                    "imap": {"username": "profile-imap@example.org", "password": "profile-imap-secret"},
                },
            },
        )
        self.assertEqual(created_profile.status_code, 201, created_profile.text)
        profile_id = created_profile.json()["id"]

        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "profile-local-creds", "name": "Profile local creds", "mode": "test"},
            "fields": [{"name": "first_name", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "to": [{"email": "recipient@example.org", "type": "to"}],
            },
            "template": {"subject": "Hello ${local::first_name}", "text": "Hello ${local::first_name}."},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {"inline": [{"id": "recipient-1", "fields": {"first_name": "Recipient"}}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {
                "rate_limit": {"messages_per_minute": 60},
                "retry": {"max_attempts": 3, "backoff_seconds": [1, 5, 30]},
                "imap_append_sent": {"enabled": False},
            },
            "status_tracking": {"enabled": True},
        }
        created_campaign = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created_campaign.status_code, 200, created_campaign.text)
        version_id = created_campaign.json()["version"]["id"]

        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": False})
        self.assertEqual(built.status_code, 200, built.text)

        from govoplan_campaign.backend.db.models import CampaignVersion
        from govoplan_campaign.backend.integrations import mail_integration
        from govoplan_mail.backend.db.models import MailServerProfile

        with SessionLocal() as session:
            version = session.get(CampaignVersion, version_id)
            self.assertIsNotNone(version)
            assert version is not None
            snapshot_payload = version.execution_snapshot or {}
            self.assertEqual(
                snapshot_payload["snapshot_version"],
                SNAPSHOT_VERSION,
            )
            self.assertEqual(snapshot_payload["mail_profile_id"], profile_id)
            self.assertNotIn("smtp", snapshot_payload)
            self.assertNotIn("imap", snapshot_payload)
            self.assertTrue(snapshot_payload["smtp_transport_revision"])
            self.assertTrue(snapshot_payload["imap_transport_revision"])

            profile = session.get(MailServerProfile, profile_id)
            self.assertIsNotNone(profile)
            assert profile is not None
            integration = mail_integration()
            summary = integration.campaign_profile_delivery_summary(
                session,
                tenant_id=profile.tenant_id,
                campaign_id=version.campaign_id,
                profile_id=profile_id,
            )
            self.assertTrue(summary["smtp_available"])
            self.assertTrue(summary["imap_available"])
            self.assertNotIn("mock.smtp", repr(summary))
            self.assertNotIn("profile-smtp", repr(summary))
            self.assertNotIn("secret", repr(summary))
            for name in (
                "smtp_config_from_profile",
                "imap_config_from_profile",
                "send_email_bytes",
                "send_email_message",
            ):
                self.assertFalse(hasattr(integration, name), name)

    def test_health_schema_and_dev_mailbox_gates(self) -> None:
        public_health = self.client.get("/health")
        self.assertEqual(public_health.status_code, 200, public_health.text)
        self.assertEqual(public_health.json(), {"status": "ok"})

        unauthenticated_details = self.client.get("/health/details")
        self.assertEqual(unauthenticated_details.status_code, 401, unauthenticated_details.text)

        headers, _ = self._login()
        details = self.client.get("/health/details", headers=headers)
        self.assertEqual(details.status_code, 200, details.text)
        self.assertIn("storage", details.json())

        schema = self.client.get("/api/v1/schemas/campaign", headers=headers)
        self.assertEqual(schema.status_code, 200, schema.text)
        self.assertEqual(schema.json()["title"], "GovOPlaN Campaign")

        dev_mailbox = self.client.get("/api/v1/dev/mailbox/messages", headers=headers)
        self.assertEqual(dev_mailbox.status_code, 404, dev_mailbox.text)

    def test_docs_context_and_ops_status_are_available(self) -> None:
        headers, _ = self._login()

        docs = self.client.get("/api/v1/docs/context", headers=headers)
        self.assertEqual(docs.status_code, 200, docs.text)
        docs_payload = docs.json()
        module_ids = {item["id"] for item in docs_payload["layers"]["configured"]["modules"]}
        self.assertIn("docs", module_ids)
        self.assertIn("ops", module_ids)
        self.assertGreaterEqual(docs_payload["summary"]["visible_route_count"], 1)
        self.assertGreaterEqual(docs_payload["summary"]["documentation_topic_count"], 1)
        documentation_topic_ids = {item["id"] for item in docs_payload["layers"]["always"]["documentation"]}
        self.assertIn("docs.configured-system-documentation", documentation_topic_ids)
        workflow_topic_ids = {item["id"] for item in docs_payload["topic_groups"]["workflow"]}
        reference_topic_ids = {item["id"] for item in docs_payload["topic_groups"]["reference"]}
        pattern_topic_ids = {item["id"] for item in docs_payload["topic_groups"]["pattern"]}
        self.assertIn("access.workflow.grant-user-access", workflow_topic_ids)
        self.assertIn("access.reference.admin-access-fields", reference_topic_ids)
        self.assertIn("docs.pattern.field-help", pattern_topic_ids)
        workflow_topic = next(item for item in docs_payload["topic_groups"]["workflow"] if item["id"] == "access.workflow.grant-user-access")
        self.assertEqual(workflow_topic["anchor_id"], "docs-topic-access-access-workflow-grant-user-access")

        user_docs = self.client.get("/api/v1/docs/context?type=user&locale=de", headers=headers)
        self.assertEqual(user_docs.status_code, 200, user_docs.text)
        user_docs_payload = user_docs.json()
        self.assertEqual(user_docs_payload["actor"]["documentation_type"], "user")
        self.assertEqual(user_docs_payload["actor"]["locale"], "de")
        user_always = {item["id"]: item for item in user_docs_payload["layers"]["always"]["documentation"]}
        self.assertEqual(user_always["docs.configured-system-documentation"]["translation_locale"], "de")

        sources = self.client.get("/api/v1/docs/sources", headers=headers)
        self.assertEqual(sources.status_code, 200, sources.text)
        source_payload = sources.json()
        self.assertEqual(source_payload["total"], len(source_payload["items"]))
        manifest_source = next(
            item
            for item in source_payload["items"]
            if item["id"] == "docs.manifest"
        )
        self.assertEqual(manifest_source["kind"], "manifest")
        self.assertNotIn("inspection", manifest_source)

        inspected_source = self.client.get(
            manifest_source["inspection_url"],
            headers=headers,
        )
        self.assertEqual(inspected_source.status_code, 200, inspected_source.text)
        self.assertEqual(
            inspected_source.json()["inspection"]["module_id"],
            "docs",
        )
        unknown_source = self.client.get(
            "/api/v1/docs/sources/docs.unknown",
            headers=headers,
        )
        self.assertEqual(unknown_source.status_code, 404, unknown_source.text)

        platform_status = self.client.get("/api/v1/platform/status", headers=headers)
        self.assertEqual(platform_status.status_code, 200, platform_status.text)
        i18n_status = platform_status.json()["i18n"]
        self.assertIn("en", i18n_status["enabled_languages"])
        self.assertIn("de", {item["code"] for item in i18n_status["available_languages"]})

        ops = self.client.get("/api/v1/ops/status", headers=headers)
        self.assertEqual(ops.status_code, 200, ops.text)
        ops_payload = ops.json()
        self.assertIn(ops_payload["summary"]["active_profile"], {"local-dev", "production-like-dev", "single-process", "split-worker"})
        self.assertEqual(ops_payload["summary"]["app_env"], "test")
        self.assertIn("redis_url", ops_payload["summary"])
        self.assertIn("celery_queues", ops_payload["summary"])
        self.assertIn("readiness", ops_payload)
        self.assertIn("module_registry", {item["id"] for item in ops_payload["checks"]})
        self.assertIn("redis_broker", {item["id"] for item in ops_payload["checks"]})
        self.assertIn("worker_split", {item["id"] for item in ops_payload["checks"]})
        self.assertIn("deployment_security", {item["id"] for item in ops_payload["checks"]})
        self.assertGreaterEqual(len(ops_payload["sizing"]), 1)

        readiness = self.client.get("/api/v1/ops/readiness", headers=headers)
        self.assertEqual(readiness.status_code, 200, readiness.text)
        self.assertTrue(readiness.json()["ready"])

    def test_language_packages_tenant_enablement_and_user_preference(self) -> None:
        headers, _ = self._login()

        settings_response = self.client.get("/api/v1/admin/system/settings", headers=headers)
        self.assertEqual(settings_response.status_code, 200, settings_response.text)
        settings_payload = settings_response.json()
        installed_languages = settings_payload["available_languages"]
        if "fr" not in {item["code"] for item in installed_languages}:
            installed_languages = [
                *installed_languages,
                {"code": "fr", "label": "French", "native_label": "Francais"},
            ]
        enabled_codes = list(dict.fromkeys([*settings_payload["enabled_language_codes"], "fr"]))
        system_update = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": settings_payload["default_locale"],
                "allow_tenant_custom_groups": settings_payload["allow_tenant_custom_groups"],
                "allow_tenant_custom_roles": settings_payload["allow_tenant_custom_roles"],
                "allow_tenant_api_keys": settings_payload["allow_tenant_api_keys"],
                "available_languages": installed_languages,
                "enabled_language_codes": enabled_codes,
            },
        )
        self.assertEqual(system_update.status_code, 200, system_update.text)
        self.assertIn("fr", {item["code"] for item in system_update.json()["available_languages"]})
        self.assertIn("fr", system_update.json()["enabled_language_codes"])

        platform_status = self.client.get("/api/v1/platform/status", headers=headers)
        self.assertEqual(platform_status.status_code, 200, platform_status.text)
        self.assertIn("fr", platform_status.json()["i18n"]["enabled_languages"])

        tenant_update = self.client.patch(
            "/api/v1/admin/tenant/settings",
            headers=headers,
            json={"default_locale": "fr", "enabled_language_codes": ["en", "fr"]},
        )
        self.assertEqual(tenant_update.status_code, 200, tenant_update.text)
        self.assertEqual(tenant_update.json()["default_locale"], "fr")
        self.assertEqual(tenant_update.json()["enabled_language_codes"], ["en", "fr"])

        profile_update = self.client.patch(
            "/api/v1/auth/profile",
            headers=headers,
            json={"preferred_language": "fr"},
        )
        self.assertEqual(profile_update.status_code, 200, profile_update.text)
        profile_payload = profile_update.json()
        self.assertEqual(profile_payload["user"]["preferred_language"], "fr")
        self.assertEqual(profile_payload["default_language"], "fr")
        self.assertEqual(profile_payload["enabled_language_codes"], ["en", "fr"])

        refreshed_profile = self.client.get("/api/v1/auth/me", headers=headers)
        self.assertEqual(refreshed_profile.status_code, 200, refreshed_profile.text)
        self.assertEqual(refreshed_profile.json()["user"]["preferred_language"], "fr")

    def test_managed_file_runtime_flow(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        tenant_id = login["tenant"]["id"]

        spaces = self.client.get("/api/v1/files/spaces", headers=headers)
        self.assertEqual(spaces.status_code, 200, spaces.text)
        self.assertEqual(spaces.json()["spaces"][0]["owner_id"], user_id)

        folder = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "inbox"},
        )
        self.assertEqual(folder.status_code, 200, folder.text)

        first = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": "inbox"},
            files=[("files", ("report.txt", b"first report", ""))],
        )
        self.assertEqual(first.status_code, 200, first.text)
        first_file = first.json()["files"][0]
        self.assertEqual(first_file["display_path"], "inbox/report.txt")
        self.assertEqual(first_file["content_type"], "text/plain")
        self.assertIsNone(first_file["source_provenance"])
        self.assertIsNone(first_file["source_revision"])

        connector_policy_sources = [
            {
                "scope_type": "system",
                "label": "System",
                "policy": {"allow": {"providers": ["seafile"]}},
            },
            {
                "scope_type": "tenant",
                "scope_id": tenant_id,
                "label": "Tenant",
                "policy": {"deny": {"external_paths": ["/reports/private/*"]}},
            },
        ]
        connector_policy_json = json.dumps({"sources": connector_policy_sources})
        allowed_source = {
            "source_type": "connector",
            "connector_id": "seafile-main",
            "provider": "seafile",
            "external_id": "repo-1:file-1",
            "external_path": "/reports/imported.txt",
            "metadata": {"library": "Reports"},
        }
        denied_policy = self.client.post(
            "/api/v1/files/connector-policy/evaluate",
            headers=headers,
            json={
                "operation": "import",
                "source_provenance": {**allowed_source, "external_path": "/reports/private/secrets.txt"},
                "policy_sources": connector_policy_sources,
            },
        )
        self.assertEqual(denied_policy.status_code, 200, denied_policy.text)
        denied_decision = denied_policy.json()["decision"]
        self.assertFalse(denied_decision["allowed"])
        self.assertIn("connector_policy_denylist", denied_decision["requirements"])
        self.assertEqual(denied_decision["details"]["deny_matches"][0]["scope"]["path"], f"tenant:{tenant_id}")

        blocked_upload = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "inbox/blocked",
                "source_revision": "nextcloud-rev-1",
                "source_provenance_json": json.dumps({**allowed_source, "connector_id": "nextcloud-main", "provider": "nextcloud"}),
                "connector_policy_json": connector_policy_json,
            },
            files=[("files", ("blocked.txt", b"blocked", "text/plain"))],
        )
        self.assertEqual(blocked_upload.status_code, 403, blocked_upload.text)
        self.assertFalse(blocked_upload.json()["detail"]["allowed"])
        self.assertIn("connector_policy_allowlist", blocked_upload.json()["detail"]["requirements"])

        imported = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "inbox/imported",
                "source_revision": "seafile-rev-1",
                "source_provenance_json": json.dumps(allowed_source),
                "connector_policy_json": connector_policy_json,
            },
            files=[("files", ("imported.txt", b"imported report", "text/plain"))],
        )
        self.assertEqual(imported.status_code, 200, imported.text)
        imported_file = imported.json()["files"][0]
        self.assertEqual(imported_file["source_revision"], "seafile-rev-1")
        self.assertEqual(imported_file["source_provenance"]["connector_id"], "seafile-main")
        self.assertEqual(imported_file["source_provenance"]["revision"], "seafile-rev-1")
        self.assertEqual(imported_file["source_provenance"]["metadata"]["library"], "Reports")

        # Exercises request-model conversion to FileConflictResolution and the
        # explicit rename path rather than silently overwriting an asset.
        second = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "inbox",
                "conflict_resolutions_json": json.dumps(
                    [
                        {
                            "target_path": "inbox/report.txt",
                            "action": "rename",
                            "new_path": "inbox/report-copy.txt",
                        }
                    ]
                ),
            },
            files=[("files", ("report.txt", b"second report", "text/plain"))],
        )
        self.assertEqual(second.status_code, 200, second.text)
        second_file = second.json()["files"][0]
        self.assertEqual(second_file["display_path"], "inbox/report-copy.txt")

        listing = self.client.get(
            "/api/v1/files",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "path_prefix": "inbox"},
        )
        self.assertEqual(listing.status_code, 200, listing.text)
        self.assertEqual(len(listing.json()["files"]), 3)
        listed_import = next(item for item in listing.json()["files"] if item["id"] == imported_file["id"])
        self.assertEqual(listed_import["source_revision"], "seafile-rev-1")

        downloaded_import = self.client.get(f"/api/v1/files/{imported_file['id']}/download", headers=headers)
        self.assertEqual(downloaded_import.status_code, 200, downloaded_import.text)
        self.assertEqual(downloaded_import.content, b"imported report")

        connector_audit = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={"limit": 500, "filter_action": "files.connector"},
        )
        self.assertEqual(connector_audit.status_code, 200, connector_audit.text)
        connector_events = {item["action"]: item for item in connector_audit.json()["items"]}
        imported_event = connector_events["files.connector.imported"]
        accessed_event = connector_events["files.connector.accessed"]
        self.assertEqual(imported_event["object_id"], imported_file["id"])
        self.assertEqual(imported_event["details"]["source_revision"], "seafile-rev-1")
        self.assertEqual(imported_event["details"]["source_provenance"]["connector_id"], "seafile-main")
        self.assertEqual(accessed_event["object_id"], imported_file["id"])
        self.assertEqual(accessed_event["details"]["operation"], "download")
        self.assertEqual(accessed_event["details"]["source_revision"], "seafile-rev-1")

        resolved = self.client.post(
            "/api/v1/files/resolve-patterns",
            headers=headers,
            json={
                "patterns": ["**/*.txt"],
                "owner_type": "user",
                "owner_id": user_id,
                "include_unmatched": True,
            },
        )
        self.assertEqual(resolved.status_code, 200, resolved.text)
        self.assertEqual(len(resolved.json()["patterns"][0]["matches"]), 3)
        self.assertEqual(resolved.json()["unmatched"], [])

        # Exercises RenamePlanItem construction without mutating persisted paths.
        rename_preview = self.client.post(
            "/api/v1/files/bulk-rename",
            headers=headers,
            json={
                "file_ids": [first_file["id"]],
                "owner_type": "user",
                "owner_id": user_id,
                "mode": "prefix",
                "prefix": "archived-",
                "dry_run": True,
            },
        )
        self.assertEqual(rename_preview.status_code, 200, rename_preview.text)
        self.assertEqual(rename_preview.json()["items"][0]["new_path"], "inbox/archived-report.txt")

        ownerless_rename = self.client.post(
            "/api/v1/files/bulk-rename",
            headers=headers,
            json={"file_ids": [first_file["id"]], "mode": "prefix", "prefix": "archived-", "dry_run": True},
        )
        self.assertEqual(ownerless_rename.status_code, 422, ownerless_rename.text)

        legacy_role = self._create_role(headers, slug="legacy-file-organizer", permissions=["files:organize"])
        legacy_user = self._create_user(
            headers,
            email="legacy-file-organizer@example.local",
            password="legacy-file-organizer-password",
            role_ids=[str(legacy_role["id"])],
        )
        legacy_read_share = self.client.post(
            f"/api/v1/files/{first_file['id']}/shares",
            headers=headers,
            json={"target_type": "user", "target_id": legacy_user["id"], "permission": "read"},
        )
        self.assertEqual(legacy_read_share.status_code, 200, legacy_read_share.text)

        from govoplan_files.backend.db.models import FileShare
        from govoplan_files.backend.storage.common import FileStorageError
        from govoplan_files.backend.storage.transfers import legacy_rename_selection_without_owner_context

        with SessionLocal() as session:
            with self.assertRaises(FileStorageError):
                legacy_rename_selection_without_owner_context(
                    session,
                    tenant_id=str(login["tenant"]["id"]),
                    user_id=str(legacy_user["id"]),
                    file_ids=[first_file["id"]],
                    mode="prefix",
                    prefix="blocked-",
                    dry_run=True,
                )
            share = session.query(FileShare).filter(FileShare.id == legacy_read_share.json()["id"]).one()
            share.permission = "write"
            session.add(share)
            session.commit()

        with SessionLocal() as session:
            legacy_plan = legacy_rename_selection_without_owner_context(
                session,
                tenant_id=str(login["tenant"]["id"]),
                user_id=str(legacy_user["id"]),
                file_ids=[first_file["id"]],
                mode="prefix",
                prefix="shared-",
                dry_run=True,
            )
        self.assertEqual(legacy_plan[0].new_path, "inbox/shared-report.txt")

        invalid_share = self.client.post(
            f"/api/v1/files/{first_file['id']}/shares",
            headers=headers,
            json={"target_type": "user", "target_id": "missing-user", "permission": "read"},
        )
        self.assertEqual(invalid_share.status_code, 400, invalid_share.text)

        campaign = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "bulk-file-share", "name": "Bulk file share"},
        )
        self.assertEqual(campaign.status_code, 200, campaign.text)
        campaign_id = campaign.json()["campaign"]["id"]

        bulk_share = self.client.post(
            "/api/v1/files/bulk-shares",
            headers=headers,
            json={
                "file_ids": [first_file["id"], second_file["id"], first_file["id"]],
                "target_type": "campaign",
                "target_id": campaign_id,
                "permission": "read",
            },
        )
        self.assertEqual(bulk_share.status_code, 200, bulk_share.text)
        self.assertEqual(bulk_share.json()["shared_count"], 2)
        self.assertEqual(len(bulk_share.json()["shares"]), 2)

        repeated_bulk_share = self.client.post(
            "/api/v1/files/bulk-shares",
            headers=headers,
            json={
                "file_ids": [first_file["id"], second_file["id"]],
                "target_type": "campaign",
                "target_id": campaign_id,
                "permission": "read",
            },
        )
        self.assertEqual(repeated_bulk_share.status_code, 200, repeated_bulk_share.text)
        self.assertEqual(repeated_bulk_share.json()["shared_count"], 2)

        campaign_files = self.client.get("/api/v1/files", headers=headers, params={"campaign_id": campaign_id})
        self.assertEqual(campaign_files.status_code, 200, campaign_files.text)
        self.assertEqual({item["id"] for item in campaign_files.json()["files"]}, {first_file["id"], second_file["id"]})
        self.assertTrue(all(item["shares"] for item in campaign_files.json()["files"]))


        download = self.client.get(f"/api/v1/files/{first_file['id']}/download", headers=headers)
        self.assertEqual(download.status_code, 200, download.text)
        self.assertIn("filename*=UTF-8''report.txt", download.headers.get("content-disposition", ""))
        self.assertEqual(download.content, b"first report")

        archive = self.client.post(
            "/api/v1/files/archive.zip",
            headers=headers,
            json={"file_ids": [first_file["id"], second_file["id"]], "filename": "reports.zip"},
        )
        self.assertEqual(archive.status_code, 200, archive.text)
        with zipfile.ZipFile(io.BytesIO(archive.content)) as bundle:
            self.assertEqual(sorted(bundle.namelist()), ["inbox/report-copy.txt", "inbox/report.txt"])
            self.assertEqual(bundle.read("inbox/report.txt"), b"first report")

        # A historical soft-deleted folder row can coexist with the active row.
        # Moving/copying through that hierarchy must choose the active row rather
        # than raising MultipleResultsFound.
        target_folder = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "archive"},
        )
        self.assertEqual(target_folder.status_code, 200, target_folder.text)
        from govoplan_files.backend.db.models import FileFolder
        from govoplan_files.backend.storage.common import utcnow
        with SessionLocal() as session:
            session.add(FileFolder(
                tenant_id=next(iter({folder.tenant_id for folder in session.query(FileFolder).filter(FileFolder.owner_user_id == user_id).all()})),
                owner_type="user",
                owner_user_id=user_id,
                path="archive",
                created_by_user_id=user_id,
                deleted_at=utcnow(),
                metadata_={},
            ))
            session.commit()
        transferred = self.client.post(
            "/api/v1/files/transfer",
            headers=headers,
            json={
                "operation": "copy",
                "file_ids": [first_file["id"]],
                "folder_paths": [],
                "source_owner_type": "user",
                "source_owner_id": user_id,
                "target_owner_type": "user",
                "target_owner_id": user_id,
                "target_folder": "archive/2026",
                "conflict_strategy": "reject",
            },
        )
        self.assertEqual(transferred.status_code, 200, transferred.text)
        self.assertEqual(transferred.json()["files"], 1)

    def test_files_delta_tracks_uploads_folders_and_delete_tombstones(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]

        full = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id},
        )
        self.assertEqual(full.status_code, 200, full.text)
        self.assertTrue(full.json()["full"])
        watermark = full.json()["watermark"]

        folder = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "delta"},
        )
        self.assertEqual(folder.status_code, 200, folder.text)
        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": "delta"},
            files=[("files", ("delta.txt", b"delta payload", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)
        file_id = uploaded.json()["files"][0]["id"]

        delta = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "since": watermark},
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertEqual({item["id"] for item in delta_payload["files"]}, {file_id})
        self.assertEqual({item["path"] for item in delta_payload["folders"]}, {"delta"})
        self.assertEqual(delta_payload["deleted"], [])

        deleted = self.client.delete(f"/api/v1/files/{file_id}", headers=headers)
        self.assertEqual(deleted.status_code, 200, deleted.text)
        after_delete = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "since": delta_payload["watermark"]},
        )
        self.assertEqual(after_delete.status_code, 200, after_delete.text)
        deleted_items = after_delete.json()["deleted"]
        self.assertEqual(len(deleted_items), 1)
        self.assertEqual(deleted_items[0]["id"], file_id)
        self.assertEqual(deleted_items[0]["resource_type"], "file")
        self.assertTrue(str(deleted_items[0]["revision"]).startswith("seq:"))

    def test_files_and_folders_support_cursor_windows(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]

        for folder_path in ("cursor-a", "cursor-b", "cursor-c"):
            folder = self.client.post(
                "/api/v1/files/folders",
                headers=headers,
                json={"owner_type": "user", "owner_id": user_id, "path": folder_path},
            )
            self.assertEqual(folder.status_code, 200, folder.text)

        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": "cursor-a"},
            files=[
                ("files", ("a.txt", b"a", "text/plain")),
                ("files", ("b.txt", b"b", "text/plain")),
                ("files", ("c.txt", b"c", "text/plain")),
            ],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)

        first_files = self.client.get(
            "/api/v1/files",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "page_size": 2},
        )
        self.assertEqual(first_files.status_code, 200, first_files.text)
        first_files_payload = first_files.json()
        self.assertEqual(len(first_files_payload["files"]), 2)
        self.assertTrue(first_files_payload["next_cursor"])

        second_files = self.client.get(
            "/api/v1/files",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "cursor": first_files_payload["next_cursor"]},
        )
        self.assertEqual(second_files.status_code, 200, second_files.text)
        self.assertEqual(len(second_files.json()["files"]), 1)

        mismatched_files = self.client.get(
            "/api/v1/files",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "path_prefix": "cursor-b", "cursor": first_files_payload["next_cursor"]},
        )
        self.assertEqual(mismatched_files.status_code, 400, mismatched_files.text)

        first_folders = self.client.get(
            "/api/v1/files/folders",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "page_size": 2},
        )
        self.assertEqual(first_folders.status_code, 200, first_folders.text)
        first_folders_payload = first_folders.json()
        self.assertEqual(len(first_folders_payload["folders"]), 2)
        self.assertTrue(first_folders_payload["next_cursor"])

        second_folders = self.client.get(
            "/api/v1/files/folders",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "cursor": first_folders_payload["next_cursor"]},
        )
        self.assertEqual(second_folders.status_code, 200, second_folders.text)
        self.assertEqual(len(second_folders.json()["folders"]), 1)

        mismatched_folders = self.client.get(
            "/api/v1/files/folders",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "page_size": 3, "cursor": first_folders_payload["next_cursor"]},
        )
        self.assertEqual(mismatched_folders.status_code, 400, mismatched_folders.text)

    def test_files_delta_reports_active_window_tombstones_for_moves_and_folder_deletes(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]

        source_folder = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "window-source"},
        )
        self.assertEqual(source_folder.status_code, 200, source_folder.text)
        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": "window-source"},
            files=[("files", ("moving.txt", b"moving", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)
        moving_file_id = uploaded.json()["files"][0]["id"]

        source_window = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "path_prefix": "window-source"},
        )
        self.assertEqual(source_window.status_code, 200, source_window.text)
        moved = self.client.post(
            "/api/v1/files/transfer",
            headers=headers,
            json={
                "operation": "move",
                "file_ids": [moving_file_id],
                "folder_paths": [],
                "source_owner_type": "user",
                "source_owner_id": user_id,
                "target_owner_type": "user",
                "target_owner_id": user_id,
                "target_folder": "window-target",
                "conflict_strategy": "reject",
            },
        )
        self.assertEqual(moved.status_code, 200, moved.text)

        after_move = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={
                "owner_type": "user",
                "owner_id": user_id,
                "path_prefix": "window-source",
                "since": source_window.json()["watermark"],
            },
        )
        self.assertEqual(after_move.status_code, 200, after_move.text)
        move_deleted = after_move.json()["deleted"]
        self.assertIn(moving_file_id, {item["id"] for item in move_deleted if item["resource_type"] == "file"})
        self.assertEqual(after_move.json()["files"], [])

        delete_root = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "window-delete"},
        )
        self.assertEqual(delete_root.status_code, 200, delete_root.text)
        nested = self.client.post(
            "/api/v1/files/folders",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "window-delete/nested"},
        )
        self.assertEqual(nested.status_code, 200, nested.text)
        uploaded_nested = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": "window-delete/nested"},
            files=[("files", ("deleted.txt", b"deleted", "text/plain"))],
        )
        self.assertEqual(uploaded_nested.status_code, 200, uploaded_nested.text)
        nested_file_id = uploaded_nested.json()["files"][0]["id"]
        delete_window = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={"owner_type": "user", "owner_id": user_id, "path_prefix": "window-delete"},
        )
        self.assertEqual(delete_window.status_code, 200, delete_window.text)

        deleted_folder = self.client.post(
            "/api/v1/files/folders/delete",
            headers=headers,
            json={"owner_type": "user", "owner_id": user_id, "path": "window-delete", "recursive": True},
        )
        self.assertEqual(deleted_folder.status_code, 200, deleted_folder.text)
        after_folder_delete = self.client.get(
            "/api/v1/files/delta",
            headers=headers,
            params={
                "owner_type": "user",
                "owner_id": user_id,
                "path_prefix": "window-delete",
                "since": delete_window.json()["watermark"],
            },
        )
        self.assertEqual(after_folder_delete.status_code, 200, after_folder_delete.text)
        folder_delete_items = after_folder_delete.json()["deleted"]
        self.assertIn(delete_root.json()["id"], {item["id"] for item in folder_delete_items if item["resource_type"] == "folder"})
        self.assertIn(nested_file_id, {item["id"] for item in folder_delete_items if item["resource_type"] == "file"})

    def test_files_delta_omits_private_tombstones_from_broad_reader_feed(self) -> None:
        owner_headers, owner_login = self._login()
        owner_id = owner_login["user"]["id"]

        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=owner_headers,
            data={"owner_type": "user", "owner_id": owner_id, "path": "private"},
            files=[("files", ("private-delta.txt", b"private delta", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)
        file_id = uploaded.json()["files"][0]["id"]

        reader_role = self._create_role(
            owner_headers,
            slug="delta-broad-reader",
            permissions=["files:read"],
        )
        self._create_user(
            owner_headers,
            email="delta-broad-reader@example.local",
            password="delta-broad-reader-password",
            role_ids=[str(reader_role["id"])],
        )
        reader_headers, _ = self._login_as(
            email="delta-broad-reader@example.local",
            password="delta-broad-reader-password",
        )

        full = self.client.get("/api/v1/files/delta", headers=reader_headers)
        self.assertEqual(full.status_code, 200, full.text)
        full_payload = full.json()
        self.assertTrue(full_payload["full"])
        self.assertNotIn(file_id, {item["id"] for item in full_payload["files"]})

        deleted = self.client.delete(f"/api/v1/files/{file_id}", headers=owner_headers)
        self.assertEqual(deleted.status_code, 200, deleted.text)

        incremental = self.client.get(
            "/api/v1/files/delta",
            headers=reader_headers,
            params={"since": full_payload["watermark"]},
        )
        self.assertEqual(incremental.status_code, 200, incremental.text)
        incremental_payload = incremental.json()
        self.assertFalse(incremental_payload["full"])
        self.assertNotIn(file_id, {item["id"] for item in incremental_payload["files"]})
        self.assertNotIn(file_id, {item["id"] for item in incremental_payload["deleted"]})

    def test_calendar_event_window_delta_tracks_moves_deletes_and_stale_watermarks(self) -> None:
        headers, _ = self._login()
        calendars = self.client.get("/api/v1/calendar/calendars", headers=headers)
        self.assertEqual(calendars.status_code, 200, calendars.text)
        if calendars.json()["calendars"]:
            calendar_id = calendars.json()["calendars"][0]["id"]
        else:
            created_calendar = self.client.post(
                "/api/v1/calendar/calendars",
                headers=headers,
                json={"name": "Delta calendar", "slug": "delta-calendar", "is_default": True},
            )
            self.assertEqual(created_calendar.status_code, 201, created_calendar.text)
            calendar_id = created_calendar.json()["id"]

        window_start = "2030-01-01T00:00:00+00:00"
        window_end = "2030-01-31T23:59:59+00:00"
        initial = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end},
        )
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])

        created = self.client.post(
            "/api/v1/calendar/events",
            headers=headers,
            json={
                "calendar_id": calendar_id,
                "summary": "Window delta",
                "start_at": "2030-01-10T10:00:00+00:00",
                "end_at": "2030-01-10T11:00:00+00:00",
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        event_id = created.json()["id"]

        created_delta = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end, "since": initial_payload["watermark"]},
        )
        self.assertEqual(created_delta.status_code, 200, created_delta.text)
        created_delta_payload = created_delta.json()
        self.assertFalse(created_delta_payload["full"])
        self.assertIn(event_id, {item["id"] for item in created_delta_payload["events"]})

        moved = self.client.patch(
            f"/api/v1/calendar/events/{event_id}",
            headers=headers,
            json={"start_at": "2030-03-01T10:00:00+00:00", "end_at": "2030-03-01T11:00:00+00:00"},
        )
        self.assertEqual(moved.status_code, 200, moved.text)
        moved_delta = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end, "since": created_delta_payload["watermark"]},
        )
        self.assertEqual(moved_delta.status_code, 200, moved_delta.text)
        moved_delta_payload = moved_delta.json()
        self.assertFalse(moved_delta_payload["full"])
        self.assertTrue(any(item["id"] == event_id and item["resource_type"] == "calendar_event" for item in moved_delta_payload["deleted"]))
        self.assertNotIn(event_id, {item["id"] for item in moved_delta_payload["events"]})

        visible = self.client.post(
            "/api/v1/calendar/events",
            headers=headers,
            json={
                "calendar_id": calendar_id,
                "summary": "Delete delta",
                "start_at": "2030-01-12T10:00:00+00:00",
                "end_at": "2030-01-12T11:00:00+00:00",
            },
        )
        self.assertEqual(visible.status_code, 201, visible.text)
        visible_id = visible.json()["id"]
        visible_delta = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end, "since": moved_delta_payload["watermark"]},
        )
        self.assertEqual(visible_delta.status_code, 200, visible_delta.text)
        self.assertIn(visible_id, {item["id"] for item in visible_delta.json()["events"]})

        deleted = self.client.delete(f"/api/v1/calendar/events/{visible_id}", headers=headers)
        self.assertEqual(deleted.status_code, 204, deleted.text)
        deleted_delta = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end, "since": visible_delta.json()["watermark"]},
        )
        self.assertEqual(deleted_delta.status_code, 200, deleted_delta.text)
        deleted_delta_payload = deleted_delta.json()
        self.assertTrue(any(item["id"] == visible_id and item["resource_type"] == "calendar_event" for item in deleted_delta_payload["deleted"]))

        with SessionLocal() as session:
            prune_sequence_entries(
                session,
                before_or_equal_id=decode_sequence_watermark(deleted_delta_payload["watermark"]),
                tenant_id=created.json()["tenant_id"],
                module_id="calendar",
                collections=("calendar.events",),
            )
            session.commit()

        stale = self.client.get(
            "/api/v1/calendar/events/delta",
            headers=headers,
            params={"calendar_id": calendar_id, "start_at": window_start, "end_at": window_end, "since": initial_payload["watermark"]},
        )
        self.assertEqual(stale.status_code, 200, stale.text)
        self.assertTrue(stale.json()["full"])

    def test_file_connector_settings_delta_tracks_credentials_profiles_and_policy(self) -> None:
        headers, _login = self._login()
        full = self.client.get(
            "/api/v1/files/connectors/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_disabled": "true"},
        )
        self.assertEqual(full.status_code, 200, full.text)
        full_payload = full.json()
        self.assertTrue(full_payload["full"])

        credential = self.client.post(
            "/api/v1/files/connectors/credentials",
            headers=headers,
            json={
                "id": "delta-webdav-credentials",
                "label": "Delta WebDAV Credentials",
                "provider": "webdav",
                "scope_type": "tenant",
                "credential_mode": "basic",
                "credentials": {"username": "govoplan", "password": "delta-secret"},
            },
        )
        self.assertEqual(credential.status_code, 201, credential.text)

        credential_delta = self.client.get(
            "/api/v1/files/connectors/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_disabled": "true", "since": full_payload["watermark"]},
        )
        self.assertEqual(credential_delta.status_code, 200, credential_delta.text)
        credential_delta_payload = credential_delta.json()
        self.assertFalse(credential_delta_payload["full"])
        self.assertIn("credentials", credential_delta_payload["changed_sections"])
        self.assertEqual({item["id"] for item in credential_delta_payload["credentials"]}, {"delta-webdav-credentials"})

        profile = self.client.post(
            "/api/v1/files/connectors/profiles",
            headers=headers,
            json={
                "id": "delta-webdav",
                "label": "Delta WebDAV",
                "provider": "webdav",
                "endpoint_url": "http://127.0.0.1:9083",
                "scope_type": "tenant",
                "credential_profile_id": "delta-webdav-credentials",
                "capabilities": ["browse"],
            },
        )
        self.assertEqual(profile.status_code, 201, profile.text)

        profile_delta = self.client.get(
            "/api/v1/files/connectors/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_disabled": "true", "since": credential_delta_payload["watermark"]},
        )
        self.assertEqual(profile_delta.status_code, 200, profile_delta.text)
        profile_delta_payload = profile_delta.json()
        self.assertFalse(profile_delta_payload["full"])
        self.assertIn("profiles", profile_delta_payload["changed_sections"])
        self.assertEqual({item["id"] for item in profile_delta_payload["profiles"]}, {"delta-webdav"})

        updated_credential = self.client.patch(
            "/api/v1/files/connectors/credentials/delta-webdav-credentials",
            headers=headers,
            json={"label": "Updated Delta WebDAV Credentials"},
        )
        self.assertEqual(updated_credential.status_code, 200, updated_credential.text)

        dependency_delta = self.client.get(
            "/api/v1/files/connectors/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_disabled": "true", "since": profile_delta_payload["watermark"]},
        )
        self.assertEqual(dependency_delta.status_code, 200, dependency_delta.text)
        dependency_delta_payload = dependency_delta.json()
        self.assertIn("credentials", dependency_delta_payload["changed_sections"])
        self.assertIn("profiles", dependency_delta_payload["changed_sections"])
        self.assertEqual(dependency_delta_payload["profiles"][0]["credential_profile_label"], "Updated Delta WebDAV Credentials")

        policy = self.client.put(
            "/api/v1/files/connectors/policies/tenant",
            headers=headers,
            json={"policy": {"deny": {"providers": ["nfs"]}}},
        )
        self.assertEqual(policy.status_code, 200, policy.text)
        policy_delta = self.client.get(
            "/api/v1/files/connectors/settings/delta",
            headers=headers,
            params={"scope_type": "tenant", "include_disabled": "true", "since": dependency_delta_payload["watermark"]},
        )
        self.assertEqual(policy_delta.status_code, 200, policy_delta.text)
        policy_delta_payload = policy_delta.json()
        self.assertFalse(policy_delta_payload["full"])
        self.assertIn("policy", policy_delta_payload["changed_sections"])
        self.assertEqual(policy_delta_payload["policy"]["effective_policy"]["deny"]["providers"], ["nfs"])

    def test_file_connector_profiles_are_governed_and_redacted(self) -> None:
        headers, login = self._login()
        user_id = str(login["user"]["id"])
        tenant_id = str(login["tenant"]["id"])
        env_keys = [
            "GOVOPLAN_FILES_CONNECTOR_PROFILES_JSON",
            "GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST",
            "GOVOPLAN_TEST_SEAFILE_PASSWORD",
            "GOVOPLAN_TEST_NEXTCLOUD_TOKEN",
            "GOVOPLAN_TEST_SMB_PASSWORD",
        ]
        previous_env = {key: os.environ.get(key) for key in env_keys}
        try:
            os.environ["GOVOPLAN_CONNECTOR_SECRET_ENV_ALLOWLIST"] = ",".join(
                (
                    "GOVOPLAN_TEST_SEAFILE_PASSWORD",
                    "GOVOPLAN_TEST_WEBDAV_PASSWORD",
                    "GOVOPLAN_TEST_NEXTCLOUD_TOKEN",
                    "GOVOPLAN_TEST_SMB_PASSWORD",
                )
            )
            os.environ["GOVOPLAN_TEST_SEAFILE_PASSWORD"] = "super-secret-seafile"
            os.environ["GOVOPLAN_TEST_NEXTCLOUD_TOKEN"] = "super-secret-nextcloud"
            os.environ["GOVOPLAN_TEST_SMB_PASSWORD"] = "super-secret-smb"
            os.environ["GOVOPLAN_FILES_CONNECTOR_PROFILES_JSON"] = json.dumps(
                {
                    "profiles": [
                        {
                            "id": "seafile-dev",
                            "label": "Seafile Dev",
                            "provider": "seafile",
                            "endpoint_url": "http://127.0.0.1:9082",
                            "scope_type": "system",
                            "credential_mode": "basic",
                            "username": "admin@example.local",
                            "password_env": "GOVOPLAN_TEST_SEAFILE_PASSWORD",
                            "capabilities": ["browse", "import"],
                            "metadata": {
                                "static_listing": {
                                    "": [
                                        {"kind": "library", "name": "Reports", "path": "reports", "external_id": "lib-reports"},
                                        {"kind": "folder", "name": "Inbox", "path": "inbox"},
                                        {"kind": "file", "name": "readme.txt", "path": "readme.txt", "size_bytes": 12, "content_type": "text/plain"},
                                    ],
                                    "inbox": [
                                        {"kind": "file", "name": "imported.txt", "path": "inbox/imported.txt", "size_bytes": 15, "etag": "etag-1"}
                                    ],
                                }
                            },
                            "policy": {"allow": {"providers": ["seafile"]}, "deny": {"external_paths": ["blocked"]}},
                        },
                        {
                            "id": "tenant-webdav",
                            "provider": "webdav",
                            "base_url": "http://127.0.0.1:9083",
                            "scope_type": "tenant",
                            "scope_id": tenant_id,
                            "credentials": {
                                "type": "basic",
                                "username": "govoplan",
                                "password_env": "GOVOPLAN_TEST_WEBDAV_PASSWORD",
                            },
                        },
                        {
                            "id": "user-nextcloud",
                            "provider": "nextcloud",
                            "url": "http://127.0.0.1:9081",
                            "scope_type": "user",
                            "scope_id": user_id,
                            "credential_mode": "token",
                            "token_env": "GOVOPLAN_TEST_NEXTCLOUD_TOKEN",
                        },
                        {
                            "id": "tenant-smb",
                            "provider": "smb",
                            "endpoint_url": "smb://127.0.0.1:1445/files/root",
                            "scope_type": "tenant",
                            "scope_id": tenant_id,
                            "credential_mode": "basic",
                            "username": "govoplan",
                            "password_env": "GOVOPLAN_TEST_SMB_PASSWORD",
                            "capabilities": ["browse", "import"],
                            "policy": {"allow": {"providers": ["smb"]}},
                        },
                        {
                            "id": "other-tenant-smb",
                            "provider": "smb",
                            "endpoint_url": "smb://127.0.0.1:1445/files",
                            "scope_type": "tenant",
                            "scope_id": "other-tenant",
                            "credential_mode": "secret_ref",
                            "secret_ref": "vault://govoplan/files/smb",
                        },
                        {
                            "id": "disabled-seafile",
                            "provider": "seafile",
                            "endpoint_url": "http://127.0.0.1:9082",
                            "enabled": False,
                        },
                    ]
                }
            )

            provider_response = self.client.get("/api/v1/files/connectors/providers", headers=headers)
            self.assertEqual(provider_response.status_code, 200, provider_response.text)
            providers = {item["provider"]: item for item in provider_response.json()["providers"]}
            self.assertTrue({"seafile", "nextcloud", "webdav", "smb", "nfs", "local"}.issubset(providers))
            self.assertTrue(providers["seafile"]["implemented"])
            self.assertTrue(providers["seafile"]["browse_supported"])
            self.assertTrue(providers["seafile"]["import_supported"])
            self.assertEqual(providers["smb"]["optional_dependency"], "smbprotocol")
            self.assertTrue(providers["smb"]["implemented"])
            self.assertTrue(providers["smb"]["browse_supported"])
            self.assertTrue(providers["smb"]["import_supported"])
            self.assertIn("files.connector.imported", providers["nextcloud"]["audit_events"])
            self.assertIn("files.connector.synced", providers["nextcloud"]["audit_events"])

            response = self.client.get("/api/v1/files/connectors/profiles", headers=headers)
            self.assertEqual(response.status_code, 200, response.text)
            profiles = {item["id"]: item for item in response.json()["profiles"]}
            self.assertEqual(set(profiles), {"seafile-dev", "tenant-webdav", "user-nextcloud", "tenant-smb"})
            self.assertTrue(profiles["seafile-dev"]["credentials_configured"])
            self.assertEqual(profiles["seafile-dev"]["credential_source"], "password_env")
            self.assertEqual(profiles["seafile-dev"]["source_path"], "system")
            self.assertEqual(profiles["seafile-dev"]["source_kind"], "settings")
            self.assertEqual(profiles["tenant-webdav"]["source_path"], f"tenant:{tenant_id}")
            self.assertFalse(profiles["tenant-webdav"]["credentials_configured"])
            self.assertEqual(profiles["user-nextcloud"]["source_path"], f"user:{user_id}")
            self.assertEqual(profiles["seafile-dev"]["policy_sources"][0]["policy"]["allow"]["providers"], ["seafile"])
            redacted_payload = json.dumps(response.json()).lower()
            self.assertNotIn("super-secret", redacted_payload)
            self.assertNotIn("govoplan_test_", redacted_payload)
            self.assertNotIn("secret_ref", redacted_payload)

            system_policy = self.client.get("/api/v1/files/connectors/policies/system", headers=headers)
            self.assertEqual(system_policy.status_code, 200, system_policy.text)
            self.assertEqual(system_policy.json()["scope_type"], "system")
            self.assertEqual(system_policy.json()["scope_id"], None)
            self.assertEqual(system_policy.json()["effective_policy_sources"], [])

            tenant_policy = self.client.get("/api/v1/files/connectors/policies/tenant", headers=headers)
            self.assertEqual(tenant_policy.status_code, 200, tenant_policy.text)
            self.assertEqual(tenant_policy.json()["scope_type"], "tenant")
            self.assertEqual(tenant_policy.json()["scope_id"], tenant_id)
            self.assertEqual(tenant_policy.json()["parent_policy_sources"], [])

            locked_system_policy = self.client.put(
                "/api/v1/files/connectors/policies/system",
                headers=headers,
                json={"policy": {"allow_lower_level_limits": {"deny.credentials": False}}},
            )
            self.assertEqual(locked_system_policy.status_code, 200, locked_system_policy.text)
            locked_tenant_policy = self.client.put(
                "/api/v1/files/connectors/policies/tenant",
                headers=headers,
                json={"policy": {"deny": {"credentials": ["forbidden-by-parent"]}}},
            )
            self.assertEqual(locked_tenant_policy.status_code, 400, locked_tenant_policy.text)
            reset_system_policy = self.client.put("/api/v1/files/connectors/policies/system", headers=headers, json={"policy": {}})
            self.assertEqual(reset_system_policy.status_code, 200, reset_system_policy.text)

            tenant_config_policy = self.client.put(
                "/api/v1/files/connectors/policies/tenant",
                headers=headers,
                json={"policy": {"deny": {"providers": ["nfs"]}}},
            )
            self.assertEqual(tenant_config_policy.status_code, 200, tenant_config_policy.text)
            self.assertEqual(tenant_config_policy.json()["policy"]["deny"]["providers"], ["nfs"])
            denied_profile_create = self.client.post(
                "/api/v1/files/connectors/profiles",
                headers=headers,
                json={
                    "id": "denied-nfs",
                    "label": "Denied NFS",
                    "provider": "nfs",
                    "endpoint_url": "nfs://127.0.0.1/export",
                    "scope_type": "tenant",
                },
            )
            self.assertEqual(denied_profile_create.status_code, 403, denied_profile_create.text)
            self.assertIn("connector_policy_denylist", denied_profile_create.json()["detail"]["requirements"])

            central_blocked_credential = self.client.post(
                "/api/v1/files/connectors/credentials",
                headers=headers,
                json={
                    "id": "central-blocked-webdav-credentials",
                    "label": "Central Blocked WebDAV Credentials",
                    "provider": "webdav",
                    "scope_type": "tenant",
                    "credential_mode": "basic",
                    "credentials": {"username": "govoplan", "password": "central-secret-webdav"},
                },
            )
            self.assertEqual(central_blocked_credential.status_code, 201, central_blocked_credential.text)
            central_blocked_profile = self.client.post(
                "/api/v1/files/connectors/profiles",
                headers=headers,
                json={
                    "id": "central-blocked-webdav",
                    "label": "Central Blocked WebDAV",
                    "provider": "webdav",
                    "endpoint_url": "http://127.0.0.1:9083",
                    "scope_type": "tenant",
                    "credential_profile_id": "central-blocked-webdav-credentials",
                    "capabilities": ["browse"],
                    "metadata": {"static_listing": {"": [{"kind": "file", "name": "central-blocked.txt", "path": "central-blocked.txt"}]}},
                },
            )
            self.assertEqual(central_blocked_profile.status_code, 201, central_blocked_profile.text)
            central_runtime_policy = self.client.put(
                "/api/v1/files/connectors/policies/tenant",
                headers=headers,
                json={"policy": {"deny": {"credentials": ["central-blocked-webdav-credentials"]}}},
            )
            self.assertEqual(central_runtime_policy.status_code, 200, central_runtime_policy.text)
            self.assertEqual(central_runtime_policy.json()["effective_policy"]["deny"]["credentials"], ["central-blocked-webdav-credentials"])
            self.assertEqual(central_runtime_policy.json()["effective_policy_sources"][0]["label"], "Tenant connector policy")
            central_blocked_browse = self.client.get("/api/v1/files/connectors/profiles/central-blocked-webdav/browse", headers=headers)
            self.assertEqual(central_blocked_browse.status_code, 403, central_blocked_browse.text)
            self.assertIn("connector_policy_denylist", central_blocked_browse.json()["detail"]["requirements"])
            self.assertEqual(central_blocked_browse.json()["detail"]["source_path"][0]["label"], "Tenant connector policy")

            stored_credential = self.client.post(
                "/api/v1/files/connectors/credentials",
                headers=headers,
                json={
                    "id": "stored-webdav-credentials",
                    "label": "Stored WebDAV Credentials",
                    "provider": "webdav",
                    "scope_type": "tenant",
                    "credential_mode": "basic",
                    "credentials": {"username": "govoplan", "password": "stored-secret-webdav"},
                    "policy": {"allow": {"credentials": ["stored-webdav-credentials"], "providers": ["webdav"]}},
                },
            )
            self.assertEqual(stored_credential.status_code, 201, stored_credential.text)
            stored_credential_payload = stored_credential.json()
            self.assertEqual(stored_credential_payload["credential_secret_source"], "stored")
            self.assertTrue(stored_credential_payload["credentials_configured"])
            self.assertNotIn("stored-secret-webdav", json.dumps(stored_credential_payload))

            stored_profile = self.client.post(
                "/api/v1/files/connectors/profiles",
                headers=headers,
                json={
                    "id": "stored-webdav",
                    "label": "Stored WebDAV",
                    "provider": "webdav",
                    "endpoint_url": "http://127.0.0.1:9083",
                    "scope_type": "tenant",
                    "credential_profile_id": "stored-webdav-credentials",
                    "credential_mode": "basic",
                    "capabilities": ["browse", "import", "sync"],
                    "policy": {"allow": {"providers": ["webdav"]}},
                    "metadata": {
                        "static_listing": {
                            "": [
                                {"kind": "folder", "name": "GovOPlaN", "path": "GovOPlaN"},
                                {"kind": "file", "name": "notice.txt", "path": "notice.txt"},
                            ]
                        }
                    },
                },
            )
            self.assertEqual(stored_profile.status_code, 201, stored_profile.text)
            stored_payload = stored_profile.json()
            self.assertEqual(stored_payload["source_kind"], "database")
            self.assertEqual(stored_payload["credential_source"], "credential_profile")
            self.assertEqual(stored_payload["credential_profile_id"], "stored-webdav-credentials")
            self.assertEqual(stored_payload["credential_profile_label"], "Stored WebDAV Credentials")
            self.assertTrue(stored_payload["credentials_configured"])
            self.assertNotIn("stored-secret-webdav", json.dumps(stored_payload))

            stored_browse = self.client.get("/api/v1/files/connectors/profiles/stored-webdav/browse", headers=headers)
            self.assertEqual(stored_browse.status_code, 200, stored_browse.text)
            self.assertEqual([item["path"] for item in stored_browse.json()["items"]], ["GovOPlaN", "notice.txt"])

            stored_list = self.client.get("/api/v1/files/connectors/profiles?include_disabled=true", headers=headers)
            self.assertEqual(stored_list.status_code, 200, stored_list.text)
            self.assertIn("stored-webdav", {item["id"] for item in stored_list.json()["profiles"]})

            disabled_stored = self.client.delete("/api/v1/files/connectors/profiles/stored-webdav", headers=headers)
            self.assertEqual(disabled_stored.status_code, 200, disabled_stored.text)
            self.assertFalse(disabled_stored.json()["enabled"])

            blocked_credential = self.client.post(
                "/api/v1/files/connectors/credentials",
                headers=headers,
                json={
                    "id": "blocked-webdav-credentials",
                    "label": "Blocked WebDAV Credentials",
                    "provider": "webdav",
                    "scope_type": "tenant",
                    "credential_mode": "basic",
                    "credentials": {"username": "govoplan", "password": "blocked-secret-webdav"},
                    "policy": {"deny": {"credentials": ["blocked-webdav-credentials"]}},
                },
            )
            self.assertEqual(blocked_credential.status_code, 201, blocked_credential.text)
            blocked_credential_profile = self.client.post(
                "/api/v1/files/connectors/profiles",
                headers=headers,
                json={
                    "id": "blocked-credential-webdav",
                    "label": "Blocked Credential WebDAV",
                    "provider": "webdav",
                    "endpoint_url": "http://127.0.0.1:9083",
                    "scope_type": "tenant",
                    "credential_profile_id": "blocked-webdav-credentials",
                    "capabilities": ["browse"],
                    "metadata": {"static_listing": {"": [{"kind": "file", "name": "blocked.txt", "path": "blocked.txt"}]}},
                },
            )
            self.assertEqual(blocked_credential_profile.status_code, 201, blocked_credential_profile.text)
            blocked_credential_browse = self.client.get("/api/v1/files/connectors/profiles/blocked-credential-webdav/browse", headers=headers)
            self.assertEqual(blocked_credential_browse.status_code, 403, blocked_credential_browse.text)
            self.assertIn("connector_policy_denylist", blocked_credential_browse.json()["detail"]["requirements"])

            browse_root = self.client.get("/api/v1/files/connectors/profiles/seafile-dev/browse", headers=headers)
            self.assertEqual(browse_root.status_code, 200, browse_root.text)
            self.assertTrue(browse_root.json()["read_only"])
            self.assertTrue(browse_root.json()["decision"]["allowed"])
            self.assertEqual([item["kind"] for item in browse_root.json()["items"]], ["library", "folder", "file"])
            self.assertEqual([item["name"] for item in browse_root.json()["items"]], ["Reports", "Inbox", "readme.txt"])

            browse_folder = self.client.get("/api/v1/files/connectors/profiles/seafile-dev/browse?path=inbox", headers=headers)
            self.assertEqual(browse_folder.status_code, 200, browse_folder.text)
            self.assertEqual(browse_folder.json()["path"], "inbox")
            self.assertEqual(browse_folder.json()["items"][0]["path"], "inbox/imported.txt")

            browse_denied = self.client.get("/api/v1/files/connectors/profiles/seafile-dev/browse?path=blocked", headers=headers)
            self.assertEqual(browse_denied.status_code, 403, browse_denied.text)
            self.assertIn("connector_policy_denylist", browse_denied.json()["detail"]["requirements"])

            linked_space = self.client.post(
                "/api/v1/files/connector-spaces",
                headers=headers,
                json={
                    "owner_type": "user",
                    "owner_id": user_id,
                    "label": "Reports connector",
                    "connector_profile_id": "seafile-dev",
                    "library_id": "lib-reports",
                    "remote_path": "inbox",
                },
            )
            self.assertEqual(linked_space.status_code, 201, linked_space.text)
            linked_space_payload = linked_space.json()
            self.assertEqual(linked_space_payload["label"], "Reports connector")
            self.assertEqual(linked_space_payload["owner_type"], "user")
            self.assertEqual(linked_space_payload["owner_id"], user_id)
            self.assertEqual(linked_space_payload["connector_profile_id"], "seafile-dev")
            self.assertEqual(linked_space_payload["provider"], "seafile")
            self.assertEqual(linked_space_payload["library_id"], "lib-reports")
            self.assertEqual(linked_space_payload["remote_path"], "inbox")
            self.assertEqual(linked_space_payload["sync_mode"], "manual")
            self.assertTrue(linked_space_payload["read_only"])

            linked_space_list = self.client.get("/api/v1/files/connector-spaces", headers=headers)
            self.assertEqual(linked_space_list.status_code, 200, linked_space_list.text)
            self.assertEqual([item["id"] for item in linked_space_list.json()["spaces"]], [linked_space_payload["id"]])

            all_spaces = self.client.get("/api/v1/files/spaces", headers=headers)
            self.assertEqual(all_spaces.status_code, 200, all_spaces.text)
            connector_space_entry = next(item for item in all_spaces.json()["spaces"] if item.get("connector_space_id") == linked_space_payload["id"])
            self.assertEqual(connector_space_entry["id"], f"connector:{linked_space_payload['id']}")
            self.assertEqual(connector_space_entry["space_type"], "connector")
            self.assertEqual(connector_space_entry["connector_profile_id"], "seafile-dev")
            self.assertEqual(connector_space_entry["remote_path"], "inbox")

            blocked_space = self.client.post(
                "/api/v1/files/connector-spaces",
                headers=headers,
                json={
                    "owner_type": "user",
                    "owner_id": user_id,
                    "label": "Blocked connector",
                    "connector_profile_id": "seafile-dev",
                    "library_id": "lib-reports",
                    "remote_path": "blocked",
                },
            )
            self.assertEqual(blocked_space.status_code, 403, blocked_space.text)
            self.assertIn("connector_policy_denylist", blocked_space.json()["detail"]["requirements"])

            updated_space = self.client.patch(
                f"/api/v1/files/connector-spaces/{linked_space_payload['id']}",
                headers=headers,
                json={"label": "Reports inbox"},
            )
            self.assertEqual(updated_space.status_code, 200, updated_space.text)
            self.assertEqual(updated_space.json()["label"], "Reports inbox")

            blocked_update = self.client.patch(
                f"/api/v1/files/connector-spaces/{linked_space_payload['id']}",
                headers=headers,
                json={"remote_path": "blocked"},
            )
            self.assertEqual(blocked_update.status_code, 403, blocked_update.text)
            self.assertIn("connector_policy_denylist", blocked_update.json()["detail"]["requirements"])

            deleted_space = self.client.delete(f"/api/v1/files/connector-spaces/{linked_space_payload['id']}", headers=headers)
            self.assertEqual(deleted_space.status_code, 200, deleted_space.text)
            self.assertFalse(deleted_space.json()["is_active"])
            self.assertIsNotNone(deleted_space.json()["deleted_at"])

            from govoplan_files.backend.storage.http_client import ConnectorHttpResponse

            def seafile_request(method: str, url: str, **kwargs):
                params = kwargs.get("params") or {}
                if url == "http://127.0.0.1:9082/api2/auth-token/":
                    return ConnectorHttpResponse(200, {}, b'{"token":"token-1"}')
                if url == "http://127.0.0.1:9082/api2/repos/repo-1/file/detail/":
                    self.assertEqual(params.get("p"), "/reports/summary.txt")
                    self.assertEqual(kwargs.get("headers", {}).get("Authorization"), "Token token-1")
                    return ConnectorHttpResponse(
                        200,
                        {},
                        json.dumps(
                            {
                                "id": "file-1",
                                "name": "summary.txt",
                                "size": 14,
                                "type": "file",
                                "mtime": 1783425600,
                                "permission": "r",
                            }
                        ).encode(),
                    )
                if url == "http://127.0.0.1:9082/api2/repos/repo-1/file/":
                    self.assertEqual(params, {"p": "/reports/summary.txt", "reuse": "1"})
                    return ConnectorHttpResponse(200, {}, b'"https://download.example.invalid/summary.txt"')
                if url == "https://download.example.invalid/summary.txt":
                    return ConnectorHttpResponse(200, {"content-type": "text/plain"}, b"summary report")
                return ConnectorHttpResponse(404, {}, b"")

            with patch("govoplan_files.backend.storage.connector_browse.request_connector_bytes", side_effect=seafile_request), patch("govoplan_files.backend.storage.connector_imports.request_connector_bytes", side_effect=seafile_request):
                imported = self.client.post(
                    "/api/v1/files/connectors/profiles/seafile-dev/import",
                    headers=headers,
                    json={
                        "library_id": "repo-1",
                        "path": "reports/summary.txt",
                        "owner_type": "user",
                        "owner_id": user_id,
                        "target_folder": "imports",
                        "metadata": {"case_type": "application"},
                    },
                )
            self.assertEqual(imported.status_code, 200, imported.text)
            imported_file = imported.json()["files"][0]
            self.assertEqual(imported_file["display_path"], "imports/summary.txt")
            self.assertEqual(imported_file["source_revision"], "file-1")
            self.assertEqual(imported_file["source_provenance"]["connector_id"], "seafile-dev")
            self.assertEqual(imported_file["source_provenance"]["provider"], "seafile")
            self.assertEqual(imported_file["source_provenance"]["external_id"], "repo-1:reports/summary.txt")
            self.assertEqual(imported_file["source_provenance"]["external_path"], "reports/summary.txt")
            self.assertEqual(imported_file["source_provenance"]["metadata"]["library_id"], "repo-1")
            self.assertEqual(imported_file["source_provenance"]["metadata"]["case_type"], "application")

            webdav_payload = {"content": b"nextcloud notice", "etag": "\"nextcloud-etag-2\""}

            def webdav_request(method: str, url: str, **kwargs):
                self.assertEqual(method, "GET")
                self.assertEqual(url, "http://127.0.0.1:9081/Shared/notice.txt")
                self.assertEqual(kwargs.get("headers", {}).get("Authorization"), "Bearer super-secret-nextcloud")
                return ConnectorHttpResponse(
                    200,
                    {"content-type": "text/plain", "etag": webdav_payload["etag"], "content-length": str(len(webdav_payload["content"]))},
                    webdav_payload["content"],
                )

            with patch("govoplan_files.backend.storage.connector_imports.request_connector_bytes", side_effect=webdav_request):
                nextcloud_import = self.client.post(
                    "/api/v1/files/connectors/profiles/user-nextcloud/import",
                    headers=headers,
                    json={
                        "library_id": "",
                        "path": "Shared/notice.txt",
                        "owner_type": "user",
                        "owner_id": user_id,
                        "target_folder": "imports",
                    },
                )
            self.assertEqual(nextcloud_import.status_code, 200, nextcloud_import.text)
            nextcloud_file = nextcloud_import.json()["files"][0]
            self.assertEqual(nextcloud_file["display_path"], "imports/notice.txt")
            self.assertEqual(nextcloud_file["source_revision"], "\"nextcloud-etag-2\"")
            self.assertEqual(nextcloud_file["source_provenance"]["provider"], "nextcloud")
            self.assertEqual(nextcloud_file["source_provenance"]["external_path"], "Shared/notice.txt")
            self.assertEqual(nextcloud_file["source_provenance"]["external_id"], "Shared/notice.txt")

            webdav_payload["content"] = b"nextcloud notice updated"
            webdav_payload["etag"] = "\"nextcloud-etag-3\""
            with patch("govoplan_files.backend.storage.connector_imports.request_connector_bytes", side_effect=webdav_request):
                nextcloud_sync = self.client.post(
                    "/api/v1/files/connectors/profiles/user-nextcloud/sync",
                    headers=headers,
                    json={
                        "library_id": "",
                        "path": "Shared/notice.txt",
                        "owner_type": "user",
                        "owner_id": user_id,
                        "target_folder": "imports",
                        "conflict_strategy": "rename",
                    },
                )
            self.assertEqual(nextcloud_sync.status_code, 200, nextcloud_sync.text)
            synced = nextcloud_sync.json()
            self.assertEqual(synced["action"], "updated")
            self.assertEqual(synced["file"]["id"], nextcloud_file["id"])
            self.assertEqual(synced["previous_version_id"], nextcloud_file["version_id"])
            self.assertNotEqual(synced["current_version_id"], nextcloud_file["version_id"])
            self.assertEqual(synced["file"]["source_revision"], "\"nextcloud-etag-3\"")
            self.assertEqual(synced["file"]["size_bytes"], len(webdav_payload["content"]))

            with patch("govoplan_files.backend.storage.connector_imports.request_connector_bytes", side_effect=webdav_request):
                nextcloud_sync_unchanged = self.client.post(
                    "/api/v1/files/connectors/profiles/user-nextcloud/sync",
                    headers=headers,
                    json={
                        "library_id": "",
                        "path": "Shared/notice.txt",
                        "owner_type": "user",
                        "owner_id": user_id,
                        "target_folder": "imports",
                        "conflict_strategy": "rename",
                    },
                )
            self.assertEqual(nextcloud_sync_unchanged.status_code, 200, nextcloud_sync_unchanged.text)
            unchanged = nextcloud_sync_unchanged.json()
            self.assertEqual(unchanged["action"], "unchanged")
            self.assertEqual(unchanged["file"]["id"], nextcloud_file["id"])
            self.assertEqual(unchanged["current_version_id"], synced["current_version_id"])

            with patch("govoplan_files.backend.storage.connector_browse._smbclient_module") as smb_sdk:
                smb_browse = self.client.get("/api/v1/files/connectors/profiles/tenant-smb/browse?path=shared", headers=headers)
            self.assertEqual(smb_browse.status_code, 400, smb_browse.text)
            self.assertIn("redirects/referrals", smb_browse.json()["detail"])
            smb_sdk.assert_not_called()

            with patch("govoplan_files.backend.storage.connector_imports._smbclient_module") as smb_import_sdk:
                smb_import = self.client.post(
                    "/api/v1/files/connectors/profiles/tenant-smb/import",
                    headers=headers,
                    json={
                        "library_id": "",
                        "path": "shared/smb-notice.txt",
                        "owner_type": "user",
                        "owner_id": user_id,
                        "target_folder": "imports",
                    },
                )
            self.assertEqual(smb_import.status_code, 400, smb_import.text)
            self.assertIn("redirects/referrals", smb_import.json()["detail"])
            smb_import_sdk.assert_not_called()

            filtered = self.client.get("/api/v1/files/connectors/profiles?provider=smb", headers=headers)
            self.assertEqual(filtered.status_code, 200, filtered.text)
            self.assertEqual([item["id"] for item in filtered.json()["profiles"]], ["tenant-smb"])

            hidden = self.client.get("/api/v1/files/connectors/profiles/other-tenant-smb", headers=headers)
            self.assertEqual(hidden.status_code, 404, hidden.text)
            hidden_browse = self.client.get("/api/v1/files/connectors/profiles/other-tenant-smb/browse", headers=headers)
            self.assertEqual(hidden_browse.status_code, 404, hidden_browse.text)

            disabled = self.client.get("/api/v1/files/connectors/profiles?include_disabled=true", headers=headers)
            self.assertEqual(disabled.status_code, 200, disabled.text)
            self.assertIn("disabled-seafile", {item["id"] for item in disabled.json()["profiles"]})

            from govoplan_files.backend.storage.connector_browse import (
                parse_webdav_multistatus,
                seafile_directory_items_from_payload,
                seafile_libraries_from_payload,
            )

            webdav_items = parse_webdav_multistatus(
                root_url="http://example.test/remote.php/dav/files/admin/",
                current_path="reports",
                payload="""<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
  <d:response>
    <d:href>/remote.php/dav/files/admin/reports/</d:href>
    <d:propstat><d:prop><d:displayname>reports</d:displayname><d:resourcetype><d:collection /></d:resourcetype></d:prop></d:propstat>
  </d:response>
  <d:response>
    <d:href>/remote.php/dav/files/admin/reports/summary.pdf</d:href>
    <d:propstat><d:prop><d:displayname>summary.pdf</d:displayname><d:getcontentlength>42</d:getcontentlength><d:getcontenttype>application/pdf</d:getcontenttype><d:getetag>&quot;abc&quot;</d:getetag></d:prop></d:propstat>
  </d:response>
</d:multistatus>""",
            )
            self.assertEqual([(item.kind, item.path, item.size_bytes) for item in webdav_items], [("file", "reports/summary.pdf", 42)])
            seafile_libraries = seafile_libraries_from_payload([
                {
                    "id": "repo-1",
                    "name": "Reports",
                    "size": 1024,
                    "mtime": 1783425600,
                    "permission": "r",
                    "encrypted": False,
                    "type": "repo",
                }
            ])
            self.assertEqual(seafile_libraries[0].to_response()["kind"], "library")
            self.assertEqual(seafile_libraries[0].to_response()["path"], "repo-1")
            self.assertEqual(seafile_libraries[0].to_response()["metadata"]["permission"], "r")
            seafile_entries = seafile_directory_items_from_payload(
                [
                    {"type": "dir", "name": "Inbox", "id": "dir-1", "mtime": 1783425600},
                    {"type": "file", "name": "summary.pdf", "id": "file-1", "size": 42, "mtime": 1783425601},
                ],
                path="reports",
            )
            self.assertEqual([(item.kind, item.path, item.size_bytes) for item in seafile_entries], [("folder", "reports/Inbox", None), ("file", "reports/summary.pdf", 42)])
        finally:
            for key, value in previous_env.items():
                if value is None:
                    os.environ.pop(key, None)
                else:
                    os.environ[key] = value

    def test_access_configuration_provider_round_trips_roles_groups_and_assignments(self) -> None:
        headers, login = self._login()
        tenant_id = str(login["tenant"]["id"])

        from govoplan_access.backend.configuration_provider import SqlAccessConfigurationProvider
        from govoplan_access.backend.db.models import Group, GroupRoleAssignment, Role
        from govoplan_core.core.configuration_packages import (
            CONFIGURATION_PROVIDER_CAPABILITY,
            ConfigurationExportSelection,
            ConfigurationPreflightContext,
            apply_configuration_package,
            dry_run_configuration_package,
            export_configuration_package,
        )

        provider = SqlAccessConfigurationProvider()
        package = {
            "package_id": "govoplan.access.test",
            "name": "Access test package",
            "version": "1.0.0",
            "required_modules": [{"module_id": "access"}],
            "required_capabilities": [CONFIGURATION_PROVIDER_CAPABILITY, "access.configuration"],
            "fragments": [
                {
                    "module_id": "access",
                    "fragment_type": "roles",
                    "payload": {
                        "items": [
                            {
                                "slug": "application-clerk",
                                "name": "Application clerk",
                                "description": "Handles applications.",
                                "permissions": ["admin:users:read"],
                            }
                        ]
                    },
                },
                {
                    "module_id": "access",
                    "fragment_type": "groups",
                    "payload": {"items": [{"slug": "front-office", "name": "Front office"}]},
                },
                {
                    "module_id": "access",
                    "fragment_type": "group_role_assignments",
                    "payload": {"items": [{"group": "front-office", "role": "application-clerk"}]},
                },
            ],
        }
        context = ConfigurationPreflightContext(
            tenant_id=tenant_id,
            installed_modules={"access": "0.1.6"},
            capabilities=frozenset({CONFIGURATION_PROVIDER_CAPABILITY, "access.configuration"}),
        )

        dry_run = dry_run_configuration_package(package, [provider], context)
        self.assertFalse([item for item in dry_run.diagnostics if item.severity == "blocker"], [item.to_dict() for item in dry_run.diagnostics])
        self.assertEqual([item.action for item in dry_run.plan], ["create", "create", "bind"])

        applied = apply_configuration_package(package, [provider], context)
        self.assertFalse([item for item in applied.diagnostics if item.severity == "blocker"], [item.to_dict() for item in applied.diagnostics])
        self.assertIn("application-clerk", applied.created_refs)
        self.assertIn("front-office", applied.created_refs)
        self.assertIn("front-office:application-clerk", applied.created_refs)

        with SessionLocal() as session:
            role = session.query(Role).filter(Role.tenant_id == tenant_id, Role.slug == "application-clerk").one()
            group = session.query(Group).filter(Group.tenant_id == tenant_id, Group.slug == "front-office").one()
            assignment = session.query(GroupRoleAssignment).filter(GroupRoleAssignment.tenant_id == tenant_id, GroupRoleAssignment.group_id == group.id, GroupRoleAssignment.role_id == role.id).one_or_none()
            self.assertIsNotNone(assignment)
            self.assertEqual(role.permissions, ["admin:users:read"])

        repeat = apply_configuration_package(package, [provider], context)
        self.assertIn("application-clerk", repeat.updated_refs)
        self.assertNotIn("front-office:application-clerk", repeat.created_refs)

        exported = export_configuration_package([provider], ConfigurationExportSelection(tenant_id=tenant_id, module_ids=("access",)), context)
        fragments = {fragment.fragment_type: fragment for fragment in exported.fragments}
        self.assertIn("application-clerk", {item["slug"] for item in fragments["roles"].payload["items"]})
        self.assertIn("front-office", {item["slug"] for item in fragments["groups"].payload["items"]})
        self.assertIn({"group": "front-office", "role": "application-clerk"}, fragments["group_role_assignments"].payload["items"])

        endpoint_package = {
            **package,
            "package_id": "govoplan.access.endpoint-test",
            "fragments": [
                {
                    "module_id": "access",
                    "fragment_type": "roles",
                    "payload": {"items": [{"slug": "application-reviewer", "name": "Application reviewer", "permissions": ["admin:users:read"]}]},
                },
                {
                    "module_id": "access",
                    "fragment_type": "groups",
                    "payload": {"items": [{"slug": "review-board", "name": "Review board"}]},
                },
                {
                    "module_id": "access",
                    "fragment_type": "group_role_assignments",
                    "payload": {"items": [{"group": "review-board", "role": "application-reviewer"}]},
                },
            ],
        }
        catalog = self.client.get("/api/v1/admin/configuration-packages/catalog", headers=headers)
        self.assertEqual(catalog.status_code, 200, catalog.text)
        self.assertIn("valid", catalog.json()["validation"])

        endpoint_dry_run = self.client.post(
            "/api/v1/admin/configuration-packages/dry-run",
            headers=headers,
            json={"tenant_id": tenant_id, "package": endpoint_package},
        )
        self.assertEqual(endpoint_dry_run.status_code, 200, endpoint_dry_run.text)
        self.assertEqual([item["action"] for item in endpoint_dry_run.json()["plan"]], ["create", "create", "bind"])

        settings_response = self.client.get("/api/v1/admin/system/settings", headers=headers)
        self.assertEqual(settings_response.status_code, 200, settings_response.text)
        maintenance_enabled = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": settings_response.json()["default_locale"],
                "allow_tenant_custom_groups": settings_response.json()["allow_tenant_custom_groups"],
                "allow_tenant_custom_roles": settings_response.json()["allow_tenant_custom_roles"],
                "allow_tenant_api_keys": settings_response.json()["allow_tenant_api_keys"],
                "maintenance_mode": {"enabled": True, "message": "Configuration package apply test"},
            },
        )
        self.assertEqual(maintenance_enabled.status_code, 200, maintenance_enabled.text)
        approver_headers = self._create_system_approver(headers, tenant_id=tenant_id)
        change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="configuration_packages.apply",
            value=endpoint_package,
            target={"tenant_id": tenant_id},
        )
        endpoint_apply = self.client.post(
            "/api/v1/admin/configuration-packages/apply",
            headers=headers,
            json={"tenant_id": tenant_id, "package": endpoint_package, "change_request_id": change_request_id},
        )
        self.assertEqual(endpoint_apply.status_code, 200, endpoint_apply.text)
        self.assertIn("application-reviewer", endpoint_apply.json()["created_refs"])
        changes = self.client.get("/api/v1/admin/configuration-changes", headers=headers)
        self.assertEqual(changes.status_code, 200, changes.text)
        self.assertIn("configuration_packages.apply", {item["key"] for item in changes.json()["history"]})
        applied_request = next(item for item in changes.json()["requests"] if item["id"] == change_request_id)
        self.assertEqual(applied_request["status"], "applied")

        endpoint_export = self.client.post(
            "/api/v1/admin/configuration-packages/export",
            headers=headers,
            json={"tenant_id": tenant_id, "module_ids": ["access"]},
        )
        self.assertEqual(endpoint_export.status_code, 200, endpoint_export.text)
        exported_roles = next(fragment for fragment in endpoint_export.json()["fragments"] if fragment["fragment_type"] == "roles")
        self.assertIn("application-reviewer", {item["slug"] for item in exported_roles["payload"]["items"]})

    def test_configuration_safety_admin_endpoints_explain_guardrails(self) -> None:
        headers, _login = self._login()
        catalog = self.client.get("/api/v1/admin/configuration-safety", headers=headers)
        self.assertEqual(catalog.status_code, 200, catalog.text)
        field_keys = {item["key"] for item in catalog.json()["fields"]}
        self.assertIn("files.connector_profiles", field_keys)
        self.assertNotIn("DATABASE_URL", field_keys)

        blocked = self.client.post(
            "/api/v1/admin/configuration-safety/plan",
            headers=headers,
            json={
                "key": "files.connector_profiles",
                "value": {"password": "plain-secret"},
                "dry_run": False,
                "approval_count": 1,
            },
        )
        self.assertEqual(blocked.status_code, 200, blocked.text)
        plan = blocked.json()["plan"]
        self.assertFalse(plan["allowed"])
        self.assertIn("dry_run_required", plan["blockers"])
        self.assertIn("secret_reference_required", plan["blockers"])
        self.assertEqual(plan["audit_event"], "files.connector_profile.updated")

    def test_module_state_update_uses_change_request_and_maintenance_mode(self) -> None:
        headers, _login = self._login()
        catalog = self.client.get("/api/v1/admin/system/modules", headers=headers)
        self.assertEqual(catalog.status_code, 200, catalog.text)
        enabled_modules = catalog.json()["desired_enabled"]

        blocked = self.client.put(
            "/api/v1/admin/system/modules",
            headers=headers,
            json={"enabled_modules": enabled_modules},
        )
        self.assertEqual(blocked.status_code, 409, blocked.text)
        blocked_detail = blocked.json()["detail"]
        self.assertEqual(blocked_detail["code"], "configuration_request_required")
        self.assertIn("dry_run_required", blocked_detail["plan"]["blockers"])
        self.assertIn("maintenance_mode_required", blocked_detail["plan"]["blockers"])

        created = self.client.post(
            "/api/v1/admin/configuration-change-requests",
            headers=headers,
            json={
                "key": "module_management.desired_enabled",
                "value": {"enabled_modules": enabled_modules},
                "dry_run": True,
                "target": {"scope": "system"},
                "reason": "module-state smoke test",
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        request = created.json()["request"]
        self.assertEqual(request["status"], "approved")
        self.assertTrue(request["plan"]["dry_run_satisfied"])

        settings_response = self.client.get("/api/v1/admin/system/settings", headers=headers)
        self.assertEqual(settings_response.status_code, 200, settings_response.text)
        settings_payload = settings_response.json()
        maintenance_enabled = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": settings_payload["default_locale"],
                "allow_tenant_custom_groups": settings_payload["allow_tenant_custom_groups"],
                "allow_tenant_custom_roles": settings_payload["allow_tenant_custom_roles"],
                "allow_tenant_api_keys": settings_payload["allow_tenant_api_keys"],
                "maintenance_mode": {"enabled": True, "message": "Module state update test"},
            },
        )
        self.assertEqual(maintenance_enabled.status_code, 200, maintenance_enabled.text)

        applied = self.client.put(
            "/api/v1/admin/system/modules",
            headers=headers,
            json={"enabled_modules": enabled_modules, "change_request_id": request["id"]},
        )
        self.assertEqual(applied.status_code, 200, applied.text)
        self.assertEqual(set(applied.json()["desired_enabled"]), set(enabled_modules))

        changes = self.client.get("/api/v1/admin/configuration-changes", headers=headers)
        self.assertEqual(changes.status_code, 200, changes.text)
        applied_request = next(item for item in changes.json()["requests"] if item["id"] == request["id"])
        self.assertEqual(applied_request["status"], "applied")
        self.assertIn("module_management.desired_enabled", {item["key"] for item in changes.json()["history"]})

    def test_zip_upload_spools_archive_instead_of_full_buffering(self) -> None:
        headers, login = self._login()
        user_id = str(login["user"]["id"])
        payload = io.BytesIO()
        with zipfile.ZipFile(payload, mode="w", compression=zipfile.ZIP_DEFLATED) as archive:
            archive.writestr("reports/january.txt", "January report")
            archive.writestr("reports/february.txt", "February report")
        payload.seek(0)

        with patch(
            "govoplan_files.backend.routes.uploads._read_limited_upload",
            side_effect=AssertionError("ZIP upload should not be fully buffered"),
        ):
            response = self.client.post(
                "/api/v1/files/upload-zip",
                headers=headers,
                data={"owner_type": "user", "owner_id": user_id, "path": "imports"},
                files={"file": ("reports.zip", payload.getvalue(), "application/zip")},
            )

        self.assertEqual(response.status_code, 200, response.text)
        paths = sorted(item["display_path"] for item in response.json()["files"])
        self.assertEqual(paths, ["imports/reports/february.txt", "imports/reports/january.txt"])

    def test_group_read_admin_does_not_grant_file_space_admin(self) -> None:
        owner_headers, _ = self._login()
        group = self.client.post(
            "/api/v1/admin/groups",
            headers=owner_headers,
            json={
                "slug": "private-files",
                "name": "Private files",
                "description": "Contains files that are not shared with group readers.",
                "member_ids": [],
                "role_ids": [],
            },
        )
        self.assertEqual(group.status_code, 201, group.text)
        group_id = group.json()["id"]

        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=owner_headers,
            data={"owner_type": "group", "owner_id": group_id, "path": ""},
            files=[("files", ("private.txt", b"private", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)
        file_id = uploaded.json()["files"][0]["id"]

        role = self._create_role(
            owner_headers,
            slug="group-directory-reader",
            permissions=["files:read", "admin:groups:read"],
        )
        self._create_user(
            owner_headers,
            email="group-reader@example.local",
            password="group-reader-password",
            role_ids=[str(role["id"])],
        )
        reader_headers, _ = self._login_as(email="group-reader@example.local", password="group-reader-password")

        groups = self.client.get("/api/v1/admin/groups", headers=reader_headers)
        self.assertEqual(groups.status_code, 200, groups.text)

        spaces = self.client.get("/api/v1/files/spaces", headers=reader_headers)
        self.assertEqual(spaces.status_code, 200, spaces.text)
        self.assertNotIn(group_id, [space["owner_id"] for space in spaces.json()["spaces"]])

        group_listing = self.client.get(
            "/api/v1/files",
            headers=reader_headers,
            params={"owner_type": "group", "owner_id": group_id},
        )
        self.assertEqual(group_listing.status_code, 403, group_listing.text)
        self.assertEqual(self.client.get(f"/api/v1/files/{file_id}", headers=reader_headers).status_code, 404)

    def test_campaign_delta_tracks_create_update_and_delete_tombstone(self) -> None:
        headers, _ = self._login()

        full = self.client.get("/api/v1/campaigns/delta", headers=headers)
        self.assertEqual(full.status_code, 200, full.text)
        self.assertTrue(full.json()["full"])
        watermark = full.json()["watermark"]

        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "delta-campaign", "name": "Delta campaign"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]

        after_create = self.client.get(
            "/api/v1/campaigns/delta",
            headers=headers,
            params={"since": watermark},
        )
        self.assertEqual(after_create.status_code, 200, after_create.text)
        create_payload = after_create.json()
        self.assertFalse(create_payload["full"])
        self.assertIn(campaign_id, {item["id"] for item in create_payload["campaigns"]})

        updated = self.client.put(
            f"/api/v1/campaigns/{campaign_id}",
            headers=headers,
            json={"name": "Delta campaign updated"},
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        after_update = self.client.get(
            "/api/v1/campaigns/delta",
            headers=headers,
            params={"since": create_payload["watermark"]},
        )
        self.assertEqual(after_update.status_code, 200, after_update.text)
        updated_campaigns = {item["id"]: item for item in after_update.json()["campaigns"]}
        self.assertEqual(updated_campaigns[campaign_id]["name"], "Delta campaign updated")

        deleted = self.client.delete(f"/api/v1/campaigns/{campaign_id}", headers=headers)
        self.assertEqual(deleted.status_code, 204, deleted.text)

        after_delete = self.client.get(
            "/api/v1/campaigns/delta",
            headers=headers,
            params={"since": after_update.json()["watermark"]},
        )
        self.assertEqual(after_delete.status_code, 200, after_delete.text)
        deleted_items = after_delete.json()["deleted"]
        self.assertEqual(len(deleted_items), 1)
        self.assertEqual(deleted_items[0]["id"], campaign_id)
        self.assertEqual(deleted_items[0]["resource_type"], "campaign")
        self.assertTrue(str(deleted_items[0]["revision"]).startswith("seq:"))

    def test_campaign_workspace_delta_tracks_version_autosave(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "workspace-delta", "name": "Workspace delta"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        full = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/workspace/delta",
            headers=headers,
            params={"include_summary": False},
        )
        self.assertEqual(full.status_code, 200, full.text)
        full_payload = full.json()
        self.assertTrue(full_payload["full"])
        self.assertEqual(full_payload["current_version"]["id"], version_id)

        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            version_id,
        )
        autosaved = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/autosave",
            headers=mutation_headers,
            json={
                "current_step": "fields",
                "base_revision": base_revision,
            },
        )
        self.assertEqual(autosaved.status_code, 200, autosaved.text)
        self.assertEqual(autosaved.json()["current_step"], "fields")

        delta = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/workspace/delta",
            headers=headers,
            params={"since": full_payload["watermark"], "include_summary": False},
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertEqual(delta_payload["current_version"]["id"], version_id)
        self.assertEqual(delta_payload["current_version"]["current_step"], "fields")
        self.assertIn(version_id, {item["id"] for item in delta_payload["versions"]})

    def test_campaign_jobs_delta_tracks_single_job_update(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="jobs-delta",
            recipient_count=2,
        )

        full = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/delta",
            headers=headers,
            params={"version_id": version_id, "page_size": 50},
        )
        self.assertEqual(full.status_code, 200, full.text)
        full_payload = full.json()
        self.assertTrue(full_payload["full"])
        self.assertEqual(len(full_payload["jobs"]), 2)
        job_id = full_payload["jobs"][0]["id"]

        from govoplan_campaign.backend.db.models import CampaignJob

        with SessionLocal() as session:
            job = session.get(CampaignJob, job_id)
            self.assertIsNotNone(job)
            job.send_status = "outcome_unknown"
            job.last_error = "SMTP outcome needs reconciliation"
            session.add(job)
            session.commit()

        delta = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/delta",
            headers=headers,
            params={"version_id": version_id, "since": full_payload["watermark"], "page_size": 50},
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertEqual([item["id"] for item in delta_payload["jobs"]], [job_id])
        self.assertEqual(delta_payload["jobs"][0]["send_status"], "outcome_unknown")
        self.assertEqual(
            delta_payload["jobs"][0]["last_error"],
            "Delivery outcome requires operator reconciliation.",
        )
        self.assertEqual(delta_payload["counts"]["send"]["outcome_unknown"], 1)
        self.assertTrue(str(delta_payload["watermark"]).startswith("seq:"))

    def test_campaign_jobs_cursor_pagination_anchors_delta_pages(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="jobs-cursor-delta",
            recipient_count=5,
        )

        first_page = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/delta",
            headers=headers,
            params={"version_id": version_id, "page": 1, "page_size": 2},
        )
        self.assertEqual(first_page.status_code, 200, first_page.text)
        first_payload = first_page.json()
        self.assertTrue(first_payload["full"])
        self.assertTrue(first_payload["next_cursor"])

        second_page = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/delta",
            headers=headers,
            params={"version_id": version_id, "page": 2, "page_size": 2, "cursor": first_payload["next_cursor"]},
        )
        self.assertEqual(second_page.status_code, 200, second_page.text)
        second_payload = second_page.json()
        self.assertEqual(second_payload["cursor"], first_payload["next_cursor"])
        second_page_ids = [item["id"] for item in second_payload["jobs"]]
        self.assertEqual(len(second_page_ids), 2)

        mismatched_cursor = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs",
            headers=headers,
            params={"version_id": version_id, "page": 2, "page_size": 3, "cursor": first_payload["next_cursor"]},
        )
        self.assertEqual(mismatched_cursor.status_code, 400, mismatched_cursor.text)

        from govoplan_campaign.backend.db.models import CampaignJob

        first_page_job_id = first_payload["jobs"][0]["id"]
        with SessionLocal() as session:
            job = session.get(CampaignJob, first_page_job_id)
            self.assertIsNotNone(job)
            job.send_status = "outcome_unknown"
            job.last_error = "Cursor page delta should stay on page one"
            session.add(job)
            session.commit()

        cursor_delta = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/delta",
            headers=headers,
            params={
                "version_id": version_id,
                "page": 2,
                "page_size": 2,
                "cursor": first_payload["next_cursor"],
                "since": second_payload["watermark"],
            },
        )
        self.assertEqual(cursor_delta.status_code, 200, cursor_delta.text)
        cursor_delta_payload = cursor_delta.json()
        self.assertFalse(cursor_delta_payload["full"])
        self.assertEqual(cursor_delta_payload["jobs"], [])

    def test_temporary_and_permanent_user_lock_lifecycle(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "lock-lifecycle", "name": "Lock lifecycle"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        temporary = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/lock-temporarily",
            headers=headers,
        )
        self.assertEqual(temporary.status_code, 200, temporary.text)
        self.assertEqual(temporary.json()["user_lock_state"], "temporary")
        self.assertTrue(temporary.json()["user_locked_at"])

        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            version_id,
        )
        blocked_update = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=mutation_headers,
            json={
                "current_step": "fields",
                "base_revision": base_revision,
            },
        )
        self.assertEqual(blocked_update.status_code, 409, blocked_update.text)

        unlocked = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/unlock-user-lock",
            headers=headers,
        )
        self.assertEqual(unlocked.status_code, 200, unlocked.text)
        self.assertIsNone(unlocked.json()["user_lock_state"])

        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            version_id,
        )
        updated = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=mutation_headers,
            json={
                "current_step": "fields",
                "base_revision": base_revision,
            },
        )
        self.assertEqual(updated.status_code, 200, updated.text)
        self.assertEqual(updated.json()["current_step"], "fields")

        relocked = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/lock-temporarily",
            headers=headers,
        )
        self.assertEqual(relocked.status_code, 200, relocked.text)

        permanent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/lock-permanently",
            headers=headers,
        )
        self.assertEqual(permanent.status_code, 200, permanent.text)
        self.assertEqual(permanent.json()["user_lock_state"], "permanent")
        self.assertTrue(permanent.json()["published_at"])

        refused_unlock = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/unlock-user-lock",
            headers=headers,
        )
        self.assertEqual(refused_unlock.status_code, 409, refused_unlock.text)


    def test_only_one_campaign_version_is_writable(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "single-working-version", "name": "Single working version"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        first_version_id = created.json()["version"]["id"]

        parallel = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(parallel.status_code, 409, parallel.text)
        self.assertIn("active working version", parallel.text)

        permanent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/lock-permanently",
            headers=headers,
        )
        self.assertEqual(permanent.status_code, 200, permanent.text)

        copied = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(copied.status_code, 200, copied.text)
        second_version_id = copied.json()["version"]["id"]
        self.assertNotEqual(second_version_id, first_version_id)
        self.assertEqual(copied.json()["campaign"]["current_version_id"], second_version_id)

        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            first_version_id,
        )
        historical_update = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}",
            headers=mutation_headers,
            json={
                "current_step": "fields",
                "base_revision": base_revision,
            },
        )
        self.assertEqual(historical_update.status_code, 409, historical_update.text)
        self.assertIn("Historical campaign versions are read-only", historical_update.text)

        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            second_version_id,
        )
        second_update = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}",
            headers=mutation_headers,
            json={
                "current_step": "fields",
                "base_revision": base_revision,
            },
        )
        self.assertEqual(second_update.status_code, 200, second_update.text)

        second_parallel = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(second_parallel.status_code, 409, second_parallel.text)

        second_permanent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}/lock-permanently",
            headers=headers,
        )
        self.assertEqual(second_permanent.status_code, 200, second_permanent.text)

        historical_branch = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(historical_branch.status_code, 409, historical_branch.text)
        self.assertIn("cannot become a new branch", historical_branch.text)

        third = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(third.status_code, 200, third.text)
        self.assertEqual(third.json()["version"]["version_number"], 3)

    def test_campaign_create_validate_build_and_mock_send(self) -> None:
        headers, _ = self._login()
        mail_profile_id = self._create_test_mail_profile(headers, name="API smoke delivery", include_imap=True)
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "api-smoke", "name": "API smoke campaign", "mode": "test"},
            "fields": [{"name": "first_name", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {
                "subject": "Hello ${local::first_name}",
                "text": "Hello ${local::first_name}, this is a smoke test.",
            },
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {
                "inline": [
                    {
                        "id": "recipient-1",
                        "to": [{"email": "recipient@example.org", "name": "Recipient", "type": "to"}],
                        "fields": {"first_name": "Ada"},
                    }
                ]
            },
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"imap_append_sent": {"enabled": True, "folder": "Sent"}},
            "status_tracking": {"enabled": True},
        }

        created = self.client.post(
            "/api/v1/campaigns",
            headers=headers,
            json={"config": campaign_json},
        )
        self.assertEqual(created.status_code, 200, created.text)
        created_payload = created.json()
        campaign_id = created_payload["campaign"]["id"]
        version_id = created_payload["version"]["id"]

        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": False},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"])

        built = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/build",
            headers=headers,
            json={"write_eml": False},
        )
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["built_count"], 1)

        mocked = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/mock-send",
            headers=headers,
            json={
                "version_id": version_id,
                "send": True,
                "append_sent": True,
                "clear_mailbox": True,
                "check_files": False,
            },
        )
        self.assertEqual(mocked.status_code, 200, mocked.text)
        result = mocked.json()["result"]
        self.assertTrue(result["validation"]["ok"])
        self.assertEqual(result["build"]["built_count"], 1)
        self.assertEqual(result["send"]["sent_count"], 1)
        self.assertEqual(result["send"]["imap_appended_count"], 1)
        self.assertEqual(result["send"]["imap_failed_count"], 0)

    def test_managed_attachment_patterns_preview_build_and_mock_send(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="Managed attachments delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "managed-attachments", "name": "Managed attachments", "mode": "test"},
            "fields": [{"name": "invoice_number", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Invoice", "text": "Please see the attached files."},
            "attachments": {
                "base_path": "invoices",
                "base_paths": [
                    {
                        "id": "personal-invoices",
                        "name": "My invoices",
                        "source": f"managed:user:{user_id}",
                        "path": "invoices",
                        "allow_individual": True,
                    }
                ],
                "global": [],
                "allow_individual": True,
            },
            "entries": {
                "inline": [
                    {
                        "id": "recipient-1",
                        "to": [{"email": "recipient@example.org", "name": "Recipient", "type": "to"}],
                        "fields": {"invoice_number": "202605-010001"},
                        "attachments": [
                            {
                                "id": "nested-pattern",
                                "label": "Nested workbook",
                                "base_path_id": "personal-invoices",
                                "base_dir": "invoices",
                                "file_filter": "**/{{local:invoice_number}}-report.XLSX",
                                "required": True,
                            },
                            {
                                "id": "exact-pattern",
                                "label": "Exact workbook",
                                "base_path_id": "personal-invoices",
                                "base_dir": "invoices",
                                "file_filter": "{{local:invoice_number}}-90100010-9601741.XLSX",
                                "required": True,
                            },
                        ],
                    }
                ]
            },
            "validation_policy": {
                "missing_email": "block",
                "template_error": "block",
                "missing_required_attachment": "block",
            },
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }

        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        for path, filename, content in [
            ("invoices/archive", "202605-010001-report.XLSX", b"nested workbook"),
            ("invoices", "202605-010001-90100010-9601741.XLSX", b"exact workbook"),
        ]:
            uploaded = self.client.post(
                "/api/v1/files/upload",
                headers=headers,
                data={
                    "owner_type": "user",
                    "owner_id": user_id,
                    "path": path,
                    "campaign_id": campaign_id,
                },
                files=[("files", (filename, content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))],
            )
            self.assertEqual(uploaded.status_code, 200, uploaded.text)

        preview = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/attachments/preview",
            headers=headers,
            json={"include_unmatched": True},
        )
        self.assertEqual(preview.status_code, 200, preview.text)
        preview_payload = preview.json()
        self.assertEqual(len(preview_payload["rules"]), 2)
        self.assertEqual([rule["match_count"] for rule in preview_payload["rules"]], [1, 1])
        self.assertEqual(
            {rule["matches"][0]["filename"] for rule in preview_payload["rules"]},
            {"202605-010001-report.XLSX", "202605-010001-90100010-9601741.XLSX"},
        )
        self.assertEqual(preview_payload["unused_shared_files"], [])

        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": True},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)

        built = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/build",
            headers=headers,
            json={"write_eml": False},
        )
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["built_count"], 1)
        self.assertEqual(built.json()["messages"][0]["attachment_count"], 2)
        self.assertTrue(built.json().get("built_at"))
        self.assertTrue(built.json().get("build_token"))
        self.assertEqual(
            sum(len(item["managed_matches"]) for item in built.json()["messages"][0]["attachments"]),
            2,
        )
        resolved_paths = {
            match
            for attachment in built.json()["messages"][0]["attachments"]
            for match in attachment["matches"]
        }
        self.assertEqual(resolved_paths, {
            "invoices/archive/202605-010001-report.XLSX",
            "invoices/202605-010001-90100010-9601741.XLSX",
        })
        self.assertFalse(any("govoplan-managed-build" in value for value in resolved_paths))

        jobs = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs",
            headers=headers,
            params={"version_id": version_id},
        )
        self.assertEqual(jobs.status_code, 200, jobs.text)
        self.assertEqual(len(jobs.json()["jobs"]), 1)
        job_summary = jobs.json()["jobs"][0]
        self.assertEqual(job_summary["campaign_version_id"], version_id)
        self.assertNotIn("resolved_recipients", job_summary)
        detail = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/{job_summary['id']}",
            headers=headers,
        )
        self.assertEqual(detail.status_code, 200, detail.text)
        job = detail.json()["job"]
        self.assertEqual(job["resolved_recipients"]["to"][0]["email"], "recipient@example.org")
        self.assertEqual(
            {
                match
                for attachment in job["attachments"]
                for match in attachment["matches"]
            },
            resolved_paths,
        )

        review_state = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/review-state",
            headers=headers,
            json={"inspection_complete": True, "reviewed_message_keys": ["recipient-1"]},
        )
        self.assertEqual(review_state.status_code, 200, review_state.text)
        stored_review = review_state.json()["editor_state"]["review_send"]
        self.assertTrue(stored_review["inspection_complete"])
        self.assertEqual(stored_review["reviewed_message_keys"], ["recipient-1"])
        self.assertEqual(stored_review["build_token"], built.json()["build_token"])

        reloaded_version = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=headers,
        )
        self.assertEqual(reloaded_version.status_code, 200, reloaded_version.text)
        self.assertTrue(reloaded_version.json()["editor_state"]["review_send"]["inspection_complete"])

        mocked = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/mock-send",
            headers=headers,
            json={
                "version_id": version_id,
                "send": True,
                "append_sent": False,
                "clear_mailbox": True,
                "check_files": False,
            },
        )
        self.assertEqual(mocked.status_code, 200, mocked.text)
        result = mocked.json()["result"]
        self.assertTrue(result["validation"]["ok"], mocked.text)
        self.assertEqual(result["send"]["sent_count"], 1)
        self.assertEqual(result["send"]["results"][0]["attachments"][0]["status"], "ok")

        from govoplan_files.backend.db.models import CampaignAttachmentUse

        with SessionLocal() as session:
            uses = session.query(CampaignAttachmentUse).filter(CampaignAttachmentUse.campaign_id == campaign_id).all()
            self.assertEqual(len(uses), 2)
            self.assertEqual({use.filename_used for use in uses}, {
                "202605-010001-report.XLSX",
                "202605-010001-90100010-9601741.XLSX",
            })

    def test_managed_attachment_unlinked_candidates_can_be_linked_on_validation(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="Unlinked attachments delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "managed-unlinked-attachments", "name": "Managed unlinked attachments", "mode": "test"},
            "fields": [{"name": "invoice_number", "type": "string", "required": True}],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Invoice", "text": "Please see the attached file."},
            "attachments": {
                "base_path": "invoices",
                "base_paths": [
                    {
                        "id": "personal-invoices",
                        "name": "My invoices",
                        "source": f"managed:user:{user_id}",
                        "path": "invoices",
                        "allow_individual": True,
                    }
                ],
                "global": [],
                "allow_individual": True,
            },
            "entries": {
                "inline": [
                    {
                        "id": "recipient-1",
                        "to": [{"email": "recipient@example.org", "name": "Recipient", "type": "to"}],
                        "fields": {"invoice_number": "202605-010002"},
                        "attachments": [
                            {
                                "id": "exact-pattern",
                                "label": "Exact workbook",
                                "base_path_id": "personal-invoices",
                                "base_dir": "invoices",
                                "file_filter": "{{local:invoice_number}}.XLSX",
                                "required": True,
                            },
                        ],
                    }
                ]
            },
            "validation_policy": {
                "missing_email": "block",
                "template_error": "block",
                "missing_required_attachment": "block",
            },
            "delivery": {"imap_append_sent": {"enabled": False}},
        }

        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "invoices",
            },
            files=[("files", ("202605-010002.XLSX", b"unlinked workbook", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)

        preview = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/attachments/preview",
            headers=headers,
            json={"include_unmatched": True, "include_unlinked_candidates": True},
        )
        self.assertEqual(preview.status_code, 200, preview.text)
        preview_payload = preview.json()
        self.assertEqual(preview_payload["matched_file_count"], 1)
        self.assertEqual(preview_payload["unlinked_file_count"], 1)
        self.assertEqual(preview_payload["rules"][0]["unlinked_match_count"], 1)
        self.assertFalse(preview_payload["rules"][0]["matches"][0]["linked_to_campaign"])

        dry_run = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/attachments/link-matches",
            headers=headers,
            json={"dry_run": True},
        )
        self.assertEqual(dry_run.status_code, 200, dry_run.text)
        self.assertEqual(dry_run.json()["linked_file_count"], 0)
        self.assertEqual(len(dry_run.json()["linkable_files"]), 1)

        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": True, "link_unshared_matches": True},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)

        linked_preview = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/attachments/preview",
            headers=headers,
            json={"include_unmatched": True, "include_unlinked_candidates": True},
        )
        self.assertEqual(linked_preview.status_code, 200, linked_preview.text)
        linked_payload = linked_preview.json()
        self.assertEqual(linked_payload["matched_file_count"], 1)
        self.assertEqual(linked_payload["linked_file_count"], 1)
        self.assertEqual(linked_payload["unlinked_file_count"], 0)
        self.assertTrue(linked_payload["rules"][0]["matches"][0]["linked_to_campaign"])

    def test_managed_attachment_send_uses_frozen_build_artifact_after_file_changes(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="Frozen attachment delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "managed-send-freeze", "name": "Managed send freeze", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Frozen evidence", "text": "Please see the evidence."},
            "attachments": {
                "base_path": "evidence",
                "base_paths": [
                    {
                        "id": "managed-evidence",
                        "name": "Managed evidence",
                        "source": f"managed:user:{user_id}",
                        "path": "evidence",
                    }
                ],
                "global": [
                    {
                        "id": "proof",
                        "label": "Proof",
                        "base_path_id": "managed-evidence",
                        "base_dir": "evidence",
                        "file_filter": "proof.txt",
                        "required": True,
                    }
                ],
                "allow_individual": False,
            },
            "entries": {
                "inline": [
                    {
                        "id": "recipient-1",
                        "to": [{"email": "recipient@example.org", "type": "to"}],
                        "fields": {},
                    }
                ]
            },
            "validation_policy": {"missing_email": "block", "template_error": "block", "missing_required_attachment": "block"},
            "delivery": {"rate_limit": {"messages_per_minute": 60}, "imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "evidence",
                "campaign_id": campaign_id,
                "source_revision": "nextcloud-etag-1",
                "source_provenance_json": json.dumps(
                    {
                        "source_type": "connector",
                        "connector_id": "nextcloud-main",
                        "provider": "nextcloud",
                        "external_id": "file-123",
                        "external_path": "/Shared/proof.txt",
                    }
                ),
            },
            files=[("files", ("proof.txt", b"original evidence", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)

        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": True})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)

        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": True})
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["built_count"], 1)
        managed_match = built.json()["messages"][0]["attachments"][0]["managed_matches"][0]
        self.assertEqual(managed_match["source_revision"], "nextcloud-etag-1")
        self.assertEqual(managed_match["source_provenance"]["connector_id"], "nextcloud-main")
        self.assertEqual(managed_match["source_provenance"]["revision"], "nextcloud-etag-1")

        from govoplan_campaign.backend.db.models import CampaignJob
        from govoplan_files.backend.db.models import CampaignAttachmentUse, FileBlob
        from govoplan_files.backend.storage.backends import get_storage_backend
        from govoplan_mail.backend.dev.mock_mailbox import list_records

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertTrue(job.eml_local_path)
            built_use = (
                session.query(CampaignAttachmentUse)
                .filter(
                    CampaignAttachmentUse.campaign_job_id == job.id,
                    CampaignAttachmentUse.use_stage == "built",
                )
                .one()
            )
            original_version_id = built_use.file_version_id
            original_blob_id = built_use.file_blob_id
            original_storage_key = session.get(FileBlob, original_blob_id).storage_key

        get_storage_backend().delete(original_storage_key)
        changed = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={
                "owner_type": "user",
                "owner_id": user_id,
                "path": "evidence",
                "campaign_id": campaign_id,
                "conflict_strategy": "overwrite",
            },
            files=[("files", ("proof.txt", b"changed live content", "text/plain"))],
        )
        self.assertEqual(changed.status_code, 200, changed.text)

        sent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/send-now",
            headers=headers,
            json={
                "version_id": version_id,
                "validate_before_send": False,
                "build_before_send": False,
                "use_rate_limit": False,
                "enqueue_imap_task": False,
            },
        )
        self.assertEqual(sent.status_code, 200, sent.text)
        self.assertEqual(sent.json()["result"]["sent_count"], 1, sent.text)

        records = list_records(kind="smtp")
        self.assertEqual(len(records), 1)
        raw_filename = records[0].get("raw_filename")
        self.assertTrue(raw_filename)
        captured = BytesParser(policy=policy.default).parsebytes((_TEST_ROOT / "mock-mailbox" / "messages" / str(raw_filename)).read_bytes())
        attachments = {part.get_filename(): part.get_payload(decode=True) for part in captured.iter_attachments()}
        self.assertEqual(attachments["proof.txt"], b"original evidence")

        with SessionLocal() as session:
            sent_use = (
                session.query(CampaignAttachmentUse)
                .filter(
                    CampaignAttachmentUse.campaign_id == campaign_id,
                    CampaignAttachmentUse.use_stage == "sent",
                )
                .one()
            )
            self.assertEqual(sent_use.file_version_id, original_version_id)
            self.assertEqual(sent_use.file_blob_id, original_blob_id)


    def test_non_blocking_review_conditions_can_be_accepted_in_bulk(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="Bulk review delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "bulk-review", "name": "Bulk review", "mode": "test"},
            "fields": [], "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {"from": {"email": "sender@example.org", "type": "to"}, "allow_individual_to": True},
            "template": {"subject": "Warning test", "text": "Body"},
            "attachments": {
                "base_path": "review-files",
                "base_paths": [{
                    "id": "managed-review-files",
                    "name": "Review files",
                    "source": f"managed:user:{user_id}",
                    "path": "review-files",
                }],
                "global": [{
                    "id": "optional-missing",
                    "base_path_id": "managed-review-files",
                    "base_dir": "review-files",
                    "file_filter": "not-there.pdf",
                    "required": False,
                    "missing_behavior": "warn",
                    "zip": {"archive_id": "exclude"},
                }],
                "zip": {"enabled": False, "archives": []},
            },
            "entries": {"inline": [{"id": "warning-entry", "to": [{"email": "recipient@example.org", "type": "to"}]}]},
            "validation_policy": {"missing_email": "block", "template_error": "block", "missing_optional_attachment": "warn"},
            "delivery": {"imap_append_sent": {"enabled": False}}, "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]
        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": True})
        self.assertEqual(validated.status_code, 200, validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": True})
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["warning_count"], 1)
        reviewed = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/review-state",
            headers=headers,
            json={"inspection_complete": True, "reviewed_message_keys": []},
        )
        self.assertEqual(reviewed.status_code, 200, reviewed.text)
        review_state = reviewed.json()["editor_state"]["review_send"]
        self.assertTrue(review_state["inspection_complete"])
        self.assertEqual(review_state["reviewed_message_keys"], ["warning-entry"])

    def test_inactive_recipients_are_aggregated_but_not_built_or_reviewed(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="Inactive recipients delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "inactive-recipients", "name": "Inactive recipients", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {"from": {"email": "sender@example.org", "name": "Sender", "type": "to"}, "allow_individual_to": True},
            "template": {"subject": "Hello", "text": "Active recipient only"},
            "attachments": {
                "base_path": "inactive-files",
                "base_paths": [{
                    "id": "managed-inactive-files",
                    "name": "Inactive files",
                    "source": f"managed:user:{user_id}",
                    "path": "inactive-files",
                }],
                "global": [],
                "zip": {"enabled": False, "archives": []},
            },
            "entries": {"inline": [
                {"id": "active", "active": True, "to": [{"email": "active@example.org", "type": "to"}]},
                {
                    "id": "inactive",
                    "active": False,
                    "to": [],
                    "attachments": [{
                        "base_path_id": "managed-inactive-files",
                        "base_dir": "inactive-files",
                        "file_filter": "missing.pdf",
                        "required": True,
                    }],
                },
            ]},
            "validation_policy": {"missing_email": "block", "template_error": "block", "missing_required_attachment": "block"},
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]
        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": True})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": True})
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["inactive_count"], 1)
        self.assertEqual(len(built.json()["messages"]), 1)
        self.assertEqual(built.json()["messages"][0]["entry_id"], "active")
        jobs = self.client.get(f"/api/v1/campaigns/{campaign_id}/jobs", headers=headers)
        self.assertEqual(jobs.status_code, 200, jobs.text)
        self.assertEqual(len(jobs.json()["jobs"]), 1)
        report = self.client.get(f"/api/v1/campaigns/{campaign_id}/report", headers=headers)
        self.assertEqual(report.status_code, 200, report.text)
        self.assertEqual(report.json()["cards"]["inactive"], 1)
        self.assertEqual(report.json()["status_counts"]["validation"]["inactive"], 1)


    def test_recipient_address_fields_and_merge_modes(self) -> None:
        headers, _ = self._login()
        mail_profile_id = self._create_test_mail_profile(headers, name="Recipient merge delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "recipient-address-fields", "name": "Recipient address fields", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": [{"email": "global-from@example.org", "type": "to"}],
                "allow_individual_from": True,
                "to": [{"email": "global-to@example.org", "type": "to"}],
                "allow_individual_to": True,
                "cc": [{"email": "global-cc@example.org", "type": "cc"}],
                "allow_individual_cc": True,
                "bcc": [{"email": "global-bcc@example.org", "type": "bcc"}],
                "allow_individual_bcc": True,
                "reply_to": [{"email": "global-reply@example.org", "type": "reply_to"}],
                "allow_individual_reply_to": True,
            },
            "template": {
                "subject": "{{local:from.email}} | {{local:all_to.email}} | {{local:to[2].email}} | {{local:all_cc.email}}",
                "text": "{{local:all_from}} / {{local:all_reply_to.email}}",
            },
            "attachments": {"base_path": ".", "base_paths": [], "global": [], "zip": {"enabled": False, "archives": []}},
            "entries": {"inline": [{
                "id": "merged-entry",
                "from": [{"email": "local-from@example.org", "type": "to"}],
                "merge_from": True,
                "to": [{"email": "local-to@example.org", "type": "to"}],
                "merge_to": True,
                "cc": [{"email": "local-cc@example.org", "type": "cc"}],
                "merge_cc": True,
                "bcc": [{"email": "local-bcc@example.org", "type": "bcc"}],
                "merge_bcc": True,
                "reply_to": [{"email": "local-reply@example.org", "type": "reply_to"}],
                "merge_reply_to": True,
            }]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]
        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": True})
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["messages"][0]["subject"], "local-from@example.org | global-to@example.org, local-to@example.org | local-to@example.org | global-cc@example.org, local-cc@example.org")

        from govoplan_campaign.backend.db.models import CampaignJob
        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_id == campaign_id).one()
            self.assertEqual([item["email"] for item in job.resolved_recipients["from_all"]], ["local-from@example.org"])
            self.assertEqual([item["email"] for item in job.resolved_recipients["to"]], ["global-to@example.org", "local-to@example.org"])
            message = BytesParser(policy=policy.default).parsebytes(Path(job.eml_local_path).read_bytes())
        self.assertIsNone(message["Sender"])
        self.assertEqual([address.addr_spec for address in message["From"].addresses], ["local-from@example.org"])
        self.assertEqual([address.addr_spec for address in message["To"].addresses], ["global-to@example.org", "local-to@example.org"])
        self.assertEqual([address.addr_spec for address in message["Cc"].addresses], ["global-cc@example.org", "local-cc@example.org"])
        self.assertEqual([address.addr_spec for address in message["Reply-To"].addresses], ["global-reply@example.org", "local-reply@example.org"])
        self.assertIn("local-from@example.org / global-reply@example.org, local-reply@example.org", message.get_body(preferencelist=("plain",)).get_content())

    def test_multiple_from_addresses_are_rejected(self) -> None:
        headers, _ = self._login()
        mail_profile_id = self._create_test_mail_profile(headers, name="Multiple sender validation delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "multiple-from", "name": "Multiple From", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": [
                    {"email": "first@example.org", "type": "to"},
                    {"email": "second@example.org", "type": "to"},
                ],
                "to": [{"email": "recipient@example.org", "type": "to"}],
            },
            "template": {"subject": "Subject", "text": "Body"},
            "attachments": {"base_path": ".", "base_paths": [], "global": [], "zip": {"enabled": False, "archives": []}},
            "entries": {"inline": [{"id": "recipient-1"}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 422, created.text)

    def test_duplicate_zip_archive_filenames_block_validation(self) -> None:
        headers, _ = self._login()
        mail_profile_id = self._create_test_mail_profile(headers, name="Duplicate ZIP validation delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "duplicate-zip-names", "name": "Duplicate ZIP names", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {"from": {"email": "sender@example.org", "type": "to"}, "allow_individual_to": True},
            "template": {"subject": "ZIP validation", "text": "Body"},
            "attachments": {
                "base_path": ".",
                "base_paths": [],
                "global": [],
                "zip": {
                    "enabled": True,
                    "archives": [
                        {"id": "first", "name": "Recipient-Bundle", "standard": True},
                        {"id": "second", "name": "recipient-bundle.zip", "standard": False},
                    ],
                },
            },
            "entries": {"inline": [{"id": "recipient", "to": [{"email": "recipient@example.org", "type": "to"}]}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        version_id = created.json()["version"]["id"]
        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": False},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertFalse(validated.json()["ok"], validated.text)
        duplicate_issues = [
            issue for issue in validated.json()["issues"]
            if issue["code"] == "zip_archive_name_duplicate"
        ]
        self.assertEqual(len(duplicate_issues), 1, validated.text)
        self.assertEqual(duplicate_issues[0]["severity"], "error")

    def test_recipient_zip_archive_with_field_password_and_rule_exclusions(self) -> None:
        headers, login = self._login()
        user_id = login["user"]["id"]
        mail_profile_id = self._create_test_mail_profile(headers, name="ZIP attachment delivery")
        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "zip-attachments", "name": "ZIP attachments", "mode": "test"},
            "fields": [
                {"name": "invoice_number", "type": "string", "required": True},
                {"name": "zip_password", "type": "password", "required": True},
                {"name": "global_zip_password", "type": "password", "required": True},
            ],
            "global_values": {"global_zip_password": "campaign-secret"},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Protected files", "text": "Please see the attached files."},
            "attachments": {
                "base_path": "documents",
                "base_paths": [{
                    "id": "personal-documents",
                    "name": "My documents",
                    "source": f"managed:user:{user_id}",
                    "path": "documents",
                    "allow_individual": True,
                }],
                "zip": {
                    "enabled": True,
                    "archives": [
                        {
                            "id": "recipient-bundle",
                            "name": "bundle-{{local:invoice_number}}.zip",
                            "standard": True,
                            "password_enabled": True,
                            "password_field": "zip_password",
                            "password_scope": "local",
                            "method": "aes"
                        },
                        {
                            "id": "shared-bundle",
                            "name": "shared-files.zip",
                            "standard": False,
                            "password_enabled": True,
                            "password_field": "global_zip_password",
                            "password_scope": "global",
                            "method": "aes"
                        }
                    ]
                },
                "global": [{
                    "id": "guide",
                    "label": "Guide outside ZIP",
                    "base_path_id": "personal-documents",
                    "base_dir": "documents",
                    "file_filter": "guide.pdf",
                    "required": True,
                    "zip": {"mode": "exclude"},
                }],
                "allow_individual": True,
            },
            "entries": {"inline": [{
                "id": "recipient-zip",
                "to": [{"email": "recipient@example.org", "name": "Recipient", "type": "to"}],
                "fields": {"invoice_number": "2026-001", "zip_password": "recipient-secret"},
                "attachments": [
                    {
                        "id": "invoice",
                        "label": "Invoice in ZIP",
                        "base_path_id": "personal-documents",
                        "base_dir": "documents",
                        "file_filter": "invoice.pdf",
                        "required": True,
                        "zip": {"mode": "inherit"},
                    },
                    {
                        "id": "invoice-outside",
                        "label": "Invoice outside ZIP as well",
                        "base_path_id": "personal-documents",
                        "base_dir": "documents",
                        "file_filter": "invoice.pdf",
                        "required": True,
                        "zip": {"archive_id": "exclude"},
                    },
                    {
                        "id": "cover",
                        "label": "Cover in shared ZIP",
                        "base_path_id": "personal-documents",
                        "base_dir": "documents",
                        "file_filter": "cover.txt",
                        "required": True,
                        "zip": {"archive_id": "shared-bundle"},
                    },
                    {
                        "id": "receipt",
                        "label": "Receipt outside ZIP",
                        "base_path_id": "personal-documents",
                        "base_dir": "documents",
                        "file_filter": "receipt.txt",
                        "required": True,
                        "zip": {"archive_id": "exclude"}
                    },
                ],
            }]},
            "validation_policy": {"missing_email": "block", "template_error": "block", "missing_required_attachment": "block"},
            "delivery": {"imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }

        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        for filename, content, content_type in [
            ("guide.pdf", b"global guide", "application/pdf"),
            ("invoice.pdf", b"secret invoice", "application/pdf"),
            ("cover.txt", b"shared cover", "text/plain"),
            ("receipt.txt", b"outside receipt", "text/plain"),
        ]:
            uploaded = self.client.post(
                "/api/v1/files/upload",
                headers=headers,
                data={"owner_type": "user", "owner_id": user_id, "path": "documents", "campaign_id": campaign_id},
                files=[("files", (filename, content, content_type))],
            )
            self.assertEqual(uploaded.status_code, 200, uploaded.text)

        preview = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/attachments/preview",
            headers=headers,
            json={"include_unmatched": True},
        )
        self.assertEqual(preview.status_code, 200, preview.text)
        by_id = {rule["attachment_id"]: rule for rule in preview.json()["rules"]}
        self.assertFalse(by_id["guide"]["zip_included"])
        self.assertTrue(by_id["invoice"]["zip_included"])
        self.assertFalse(by_id["invoice-outside"]["zip_included"])
        self.assertTrue(by_id["cover"]["zip_included"])
        self.assertEqual(by_id["cover"]["zip_archive_id"], "shared-bundle")
        self.assertFalse(by_id["receipt"]["zip_included"])

        validated = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/validate",
            headers=headers,
            json={"check_files": True},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)

        built = self.client.post(
            f"/api/v1/campaigns/versions/{version_id}/build",
            headers=headers,
            json={"write_eml": True},
        )
        self.assertEqual(built.status_code, 200, built.text)
        message_summary = built.json()["messages"][0]
        self.assertEqual(message_summary["attachment_count"], 5)
        summaries = {item["attachment_id"]: item for item in message_summary["attachments"]}
        self.assertEqual(summaries["invoice"]["zip_filename"], "bundle-2026-001.zip")
        self.assertIsNone(summaries["guide"]["zip_filename"])
        self.assertIsNone(summaries["invoice-outside"]["zip_filename"])
        self.assertEqual(summaries["cover"]["zip_filename"], "shared-files.zip")
        self.assertIsNone(summaries["receipt"]["zip_filename"])

        from govoplan_files.backend.db.models import CampaignAttachmentUse
        from govoplan_campaign.backend.db.models import CampaignJob

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_id == campaign_id).one()
            eml_path = Path(job.eml_local_path)
            message = BytesParser(policy=policy.default).parsebytes(eml_path.read_bytes())
            uses = (
                session.query(CampaignAttachmentUse)
                .filter(
                    CampaignAttachmentUse.campaign_job_id == job.id,
                    CampaignAttachmentUse.use_stage == "built",
                )
                .all()
            )
            self.assertEqual(len(uses), 4)
            self.assertEqual(sum(use.filename_used == "invoice.pdf" for use in uses), 1)

        attachments = {part.get_filename(): part.get_payload(decode=True) for part in message.iter_attachments()}
        self.assertEqual(set(attachments), {"guide.pdf", "invoice.pdf", "receipt.txt", "bundle-2026-001.zip", "shared-files.zip"})
        self.assertEqual(attachments["guide.pdf"], b"global guide")
        self.assertEqual(attachments["invoice.pdf"], b"secret invoice")
        self.assertEqual(attachments["receipt.txt"], b"outside receipt")
        with pyzipper.AESZipFile(io.BytesIO(attachments["bundle-2026-001.zip"])) as archive:
            archive.setpassword(b"recipient-secret")
            self.assertEqual(archive.namelist(), ["invoice.pdf"])
            self.assertEqual(archive.read("invoice.pdf"), b"secret invoice")
        with pyzipper.AESZipFile(io.BytesIO(attachments["shared-files.zip"])) as archive:
            archive.setpassword(b"campaign-secret")
            self.assertEqual(archive.namelist(), ["cover.txt"])
            self.assertEqual(archive.read("cover.txt"), b"shared cover")



    def test_execution_snapshot_detects_profile_drift_and_freezes_job_manifest(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="snapshot-freeze",
        )

        from govoplan_campaign.backend.db.models import CampaignJob, CampaignVersion, SendAttempt
        from govoplan_mail.backend.db.models import MailServerEndpoint, MailServerProfile

        with SessionLocal() as session:
            version = session.get(CampaignVersion, version_id)
            self.assertIsNotNone(version)
            assert version is not None
            snapshot = version.execution_snapshot
            self.assertIsInstance(snapshot, dict)
            self.assertEqual(snapshot["snapshot_version"], SNAPSHOT_VERSION)
            self.assertEqual(snapshot["job_count"], 1)
            self.assertEqual(snapshot["queueable_job_count"], 1)
            self.assertTrue(snapshot["job_manifest_sha256"])
            self.assertTrue(snapshot["smtp_transport_revision"])
            self.assertNotIn("smtp", snapshot)
            self.assertNotIn("imap", snapshot)
            self.assertTrue(version.execution_snapshot_hash)
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertTrue(job.eml_sha256)
            self.assertTrue(job.message_id_header)

            profile = session.get(MailServerProfile, snapshot["mail_profile_id"])
            self.assertIsNotNone(profile)
            assert profile is not None
            smtp_server_id = snapshot.get("smtp_server_id")
            self.assertTrue(smtp_server_id)
            smtp_server = session.get(MailServerEndpoint, smtp_server_id)
            self.assertIsNotNone(smtp_server)
            assert smtp_server is not None
            smtp_server.config = {
                **smtp_server.config,
                "host": "smtp.example.invalid",
            }
            smtp_server.transport_revision = "manually-rotated-transport-revision"
            session.add(smtp_server)
            session.commit()

        sent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/send-now",
            headers=headers,
            json={
                "version_id": version_id,
                "validate_before_send": False,
                "build_before_send": False,
                "use_rate_limit": False,
                "enqueue_imap_task": False,
            },
        )
        self.assertEqual(sent.status_code, 422, sent.text)
        self.assertIn("Synchronous preflight stopped before contacting SMTP", sent.json()["detail"])

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertEqual(job.queue_status, "draft")
            self.assertEqual(job.send_status, "not_queued")
            self.assertEqual(
                session.query(SendAttempt).filter(SendAttempt.job_id == job.id).count(),
                0,
            )

    def test_send_now_sends_exact_generated_eml_bytes(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="exact-eml-parity",
        )

        from govoplan_campaign.backend.db.models import CampaignJob
        from govoplan_mail.backend.dev.mock_mailbox import list_records

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertIsNotNone(job.eml_local_path)
            generated_eml = Path(job.eml_local_path).read_bytes()

        sent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/send-now",
            headers=headers,
            json={
                "version_id": version_id,
                "validate_before_send": False,
                "build_before_send": False,
                "use_rate_limit": False,
                "enqueue_imap_task": False,
            },
        )
        self.assertEqual(sent.status_code, 200, sent.text)

        records = list_records(kind="smtp")
        self.assertEqual(len(records), 1)
        raw_filename = records[0].get("raw_filename")
        self.assertTrue(raw_filename)
        captured_eml = (_TEST_ROOT / "mock-mailbox" / "messages" / str(raw_filename)).read_bytes()
        self.assertEqual(captured_eml, generated_eml)

    def test_send_now_rejects_modified_generated_eml_before_delivery(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="tampered-eml",
        )

        from govoplan_campaign.backend.db.models import CampaignJob
        from govoplan_mail.backend.dev.mock_mailbox import list_records

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertIsNotNone(job.eml_local_path)
            eml_path = Path(job.eml_local_path)
            eml_path.write_bytes(eml_path.read_bytes() + b"\r\nX-Tampered: true\r\n")

        sent = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/send-now",
            headers=headers,
            json={
                "version_id": version_id,
                "validate_before_send": False,
                "build_before_send": False,
                "use_rate_limit": False,
                "enqueue_imap_task": False,
            },
        )
        self.assertEqual(sent.status_code, 422, sent.text)
        self.assertIn("Synchronous preflight stopped before contacting SMTP", sent.json()["detail"])
        self.assertEqual(list_records(kind="smtp"), [])

        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertEqual(job.queue_status, "draft")
            self.assertEqual(job.send_status, "not_queued")

    def test_partial_smtp_recipient_refusal_is_recorded_without_retrying_accepted_delivery(self) -> None:
        headers, _ = self._login()
        from govoplan_mail.backend.dev.mock_mailbox import set_failures
        mail_profile_id = self._create_test_mail_profile(headers, name="Partial refusal delivery")

        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "partial-refusal", "name": "Partial refusal", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "allow_individual_to": True,
            },
            "template": {"subject": "Partial", "text": "Partial"},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {
                "inline": [
                    {
                        "id": "one",
                        "to": [
                            {"email": "accepted@example.org", "type": "to"},
                            {"email": "reject-me@example.org", "type": "to"},
                        ],
                        "fields": {},
                    }
                ]
            },
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"rate_limit": {"messages_per_minute": 60}, "imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]
        validated = self.client.post(f"/api/v1/campaigns/versions/{version_id}/validate", headers=headers, json={"check_files": False})
        self.assertEqual(validated.status_code, 200, validated.text)
        built = self.client.post(f"/api/v1/campaigns/versions/{version_id}/build", headers=headers, json={"write_eml": True})
        self.assertEqual(built.status_code, 200, built.text)

        try:
            set_failures(smtp_reject_recipients_containing="reject-me")
            sent = self.client.post(
                f"/api/v1/campaigns/{campaign_id}/send-now",
                headers=headers,
                json={
                    "version_id": version_id,
                    "validate_before_send": False,
                    "build_before_send": False,
                    "use_rate_limit": False,
                    "enqueue_imap_task": False,
                },
            )
            self.assertEqual(sent.status_code, 200, sent.text)
            result = sent.json()["result"]
            self.assertEqual(result["sent_count"], 1, sent.text)
            self.assertEqual(result["results"][0]["status"], "smtp_accepted")
            self.assertNotIn("message", result["results"][0])
        finally:
            set_failures(smtp_reject_recipients_containing=None)

        from govoplan_campaign.backend.db.models import CampaignJob, SendAttempt
        from govoplan_campaign.backend.sending.jobs import send_campaign_job
        with SessionLocal() as session:
            job = session.query(CampaignJob).filter(CampaignJob.campaign_version_id == version_id).one()
            self.assertEqual(job.send_status, "smtp_accepted")
            self.assertIn("reject-me@example.org", job.last_error or "")
            job_id = job.id
            attempt_count = job.attempt_count
            attempt = session.query(SendAttempt).filter(SendAttempt.job_id == job.id).one()
            self.assertEqual(attempt.status, "smtp_accepted_with_refusals")
            self.assertIn("reject-me@example.org", attempt.smtp_response or "")

        retry_accepted = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/jobs/retry",
            headers=headers,
            json={"version_id": version_id, "job_ids": [job_id], "include_permanent": True, "enqueue_celery": False},
        )
        self.assertEqual(retry_accepted.status_code, 200, retry_accepted.text)
        self.assertEqual(retry_accepted.json()["result"]["selected_count"], 0)
        self.assertIn("not an explicit retry candidate", retry_accepted.json()["result"]["skipped"][0]["reason"])

        unattempted_accepted = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/jobs/send-unattempted",
            headers=headers,
            json={"version_id": version_id, "job_ids": [job_id], "enqueue_celery": False},
        )
        self.assertEqual(unattempted_accepted.status_code, 200, unattempted_accepted.text)
        self.assertEqual(unattempted_accepted.json()["result"]["selected_count"], 0)

        with SessionLocal() as session:
            direct_result = send_campaign_job(session, job_id=job_id, use_rate_limit=False)
            self.assertEqual(direct_result.status, "already_accepted")
            job = session.get(CampaignJob, job_id)
            self.assertEqual(job.attempt_count, attempt_count)

    def test_worker_loss_becomes_unknown_and_requires_reconciliation_before_retry(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="worker-loss",
            recipient_count=2,
        )
        queued = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/queue",
            headers=headers,
            json={"version_id": version_id, "enqueue_celery": False},
        )
        self.assertEqual(queued.status_code, 200, queued.text)
        self.assertEqual(queued.json()["queued_count"], 2)

        queued_summary = self.client.get(f"/api/v1/campaigns/{campaign_id}/summary", headers=headers, params={"version_id": version_id})
        self.assertEqual(queued_summary.status_code, 200, queued_summary.text)
        self.assertEqual(queued_summary.json()["cards"]["queued_or_active"], 2)
        self.assertEqual(queued_summary.json()["status_counts"]["send"]["queued"], 2)

        from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignVersion, SendAttempt
        from govoplan_campaign.backend.sending.jobs import send_campaign_job

        with SessionLocal() as session:
            jobs = (
                session.query(CampaignJob)
                .filter(CampaignJob.campaign_version_id == version_id)
                .order_by(CampaignJob.entry_index.asc())
                .all()
            )
            uncertain_job = jobs[0]
            now = datetime.now(timezone.utc)
            uncertain_job.queue_status = "sending"
            uncertain_job.send_status = "sending"
            uncertain_job.claimed_at = now
            uncertain_job.smtp_started_at = now
            uncertain_job.claim_token = "worker-loss-claim"
            uncertain_job.attempt_count = 1
            session.add(
                SendAttempt(
                    job_id=uncertain_job.id,
                    attempt_number=1,
                    status="smtp_in_progress",
                    claim_token=uncertain_job.claim_token,
                    started_at=now,
                )
            )
            session.add(uncertain_job)
            session.commit()
            uncertain_job_id = uncertain_job.id

        with SessionLocal() as session:
            result = send_campaign_job(session, job_id=uncertain_job_id, use_rate_limit=False)
            self.assertEqual(result.status, "outcome_unknown")

        retry_unknown = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/jobs/retry",
            headers=headers,
            json={"version_id": version_id, "job_ids": [uncertain_job_id], "enqueue_celery": False},
        )
        self.assertEqual(retry_unknown.status_code, 200, retry_unknown.text)
        self.assertEqual(retry_unknown.json()["result"]["selected_count"], 0)

        cancelled = self.client.post(f"/api/v1/campaigns/{campaign_id}/cancel", headers=headers)
        self.assertEqual(cancelled.status_code, 200, cancelled.text)
        with SessionLocal() as session:
            campaign = session.get(Campaign, campaign_id)
            version = session.get(CampaignVersion, version_id)
            self.assertEqual(campaign.status, "outcome_unknown")
            self.assertEqual(version.workflow_state, "outcome_unknown")

        page = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs",
            headers=headers,
            params={"version_id": version_id, "page": 1, "page_size": 1},
        )
        self.assertEqual(page.status_code, 200, page.text)
        self.assertEqual(page.json()["total"], 2)
        self.assertEqual(page.json()["pages"], 2)
        self.assertEqual(page.json()["counts"]["send"]["outcome_unknown"], 1)
        self.assertNotIn("resolved_recipients", page.json()["jobs"][0])

        filtered_page = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs",
            headers=headers,
            params={"version_id": version_id, "send_status": "outcome_unknown"},
        )
        self.assertEqual(filtered_page.status_code, 200, filtered_page.text)
        self.assertEqual(filtered_page.json()["total"], 1)
        self.assertEqual(filtered_page.json()["total_unfiltered"], 2)
        self.assertEqual(filtered_page.json()["filtered_counts"]["send"]["outcome_unknown"], 1)

        summary = self.client.get(f"/api/v1/campaigns/{campaign_id}/summary", headers=headers, params={"version_id": version_id})
        self.assertEqual(summary.status_code, 200, summary.text)
        self.assertEqual(summary.json()["cards"]["outcome_unknown"], 1)
        self.assertEqual(summary.json()["status_counts"]["send"]["outcome_unknown"], 1)

        detail = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs/{uncertain_job_id}",
            headers=headers,
        )
        self.assertEqual(detail.status_code, 200, detail.text)
        self.assertIn("resolved_recipients", detail.json()["job"])
        self.assertEqual(detail.json()["attempts"]["smtp"][0]["status"], "outcome_unknown")

        resolved = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/jobs/{uncertain_job_id}/resolve-outcome",
            headers=headers,
            json={"decision": "not_sent", "note": "Verified against the SMTP logs."},
        )
        self.assertEqual(resolved.status_code, 200, resolved.text)
        self.assertEqual(resolved.json()["result"]["send_status"], "failed_temporary")

        reconciled_report = self.client.get(f"/api/v1/campaigns/{campaign_id}/report", headers=headers, params={"version_id": version_id})
        self.assertEqual(reconciled_report.status_code, 200, reconciled_report.text)
        self.assertEqual(reconciled_report.json()["cards"]["outcome_unknown"], 0)
        self.assertEqual(reconciled_report.json()["cards"]["failed"], 1)
        self.assertEqual(reconciled_report.json()["status_counts"]["send"]["failed_temporary"], 1)

        retry = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/jobs/retry",
            headers=headers,
            json={"version_id": version_id, "job_ids": [uncertain_job_id], "enqueue_celery": False},
        )
        self.assertEqual(retry.status_code, 200, retry.text)
        self.assertEqual(retry.json()["result"]["selected_count"], 1)
        with SessionLocal() as session:
            job = session.get(CampaignJob, uncertain_job_id)
            self.assertEqual(job.send_status, "queued")

    def test_mixed_delivery_results_are_explicitly_partially_completed(self) -> None:
        headers, _ = self._login()
        campaign_id, version_id = self._create_built_delivery_campaign(
            headers,
            external_id="partial-result",
            recipient_count=2,
        )

        from govoplan_campaign.backend.db.models import Campaign, CampaignJob, CampaignVersion
        from govoplan_campaign.backend.sending.jobs import _update_campaign_after_job

        with SessionLocal() as session:
            jobs = (
                session.query(CampaignJob)
                .filter(CampaignJob.campaign_version_id == version_id)
                .order_by(CampaignJob.entry_index.asc())
                .all()
            )
            jobs[0].send_status = "smtp_accepted"
            jobs[0].sent_at = datetime.now(timezone.utc)
            jobs[1].send_status = "failed_permanent"
            jobs[1].last_error = "Explicit SMTP rejection"
            for job in jobs:
                job.queue_status = "draft"
                session.add(job)
            _update_campaign_after_job(session, campaign_id, version_id)
            session.commit()

            campaign = session.get(Campaign, campaign_id)
            version = session.get(CampaignVersion, version_id)
            self.assertEqual(campaign.status, "partially_completed")
            self.assertEqual(version.workflow_state, "partially_completed")

        report = self.client.get(f"/api/v1/campaigns/{campaign_id}/report", headers=headers)
        self.assertEqual(report.status_code, 200, report.text)
        cards = report.json()["cards"]
        self.assertEqual(cards["smtp_accepted"], 1)
        self.assertEqual(cards["failed"], 1)
        self.assertTrue(cards["partially_completed"])


    def test_reports_and_job_review_are_scoped_to_the_selected_version(self) -> None:
        headers, _ = self._login()
        campaign_id, first_version_id = self._create_built_delivery_campaign(
            headers,
            external_id="version-scoped-report",
            recipient_count=1,
        )

        locked = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/lock-permanently",
            headers=headers,
        )
        self.assertEqual(locked.status_code, 200, locked.text)
        copied = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{first_version_id}/fork",
            headers=headers,
            json={},
        )
        self.assertEqual(copied.status_code, 200, copied.text)
        second_version_id = copied.json()["version"]["id"]

        version_response = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}",
            headers=headers,
        )
        self.assertEqual(version_response.status_code, 200, version_response.text)
        campaign_json = version_response.json()["raw_json"]
        campaign_json["entries"]["inline"].append(
            {
                "id": "recipient-2",
                "to": [{"email": "recipient-2@example.org", "type": "to"}],
                "fields": {"first_name": "Recipient 2"},
            }
        )
        updated = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{second_version_id}",
            headers={
                **headers,
                "If-Match": str(version_response.headers["etag"]),
            },
            json={
                "campaign_json": campaign_json,
                "base_revision": version_response.json()["edit_revision"],
            },
        )
        self.assertEqual(updated.status_code, 200, updated.text)
        validated = self.client.post(
            f"/api/v1/campaigns/versions/{second_version_id}/validate",
            headers=headers,
            json={"check_files": False},
        )
        self.assertEqual(validated.status_code, 200, validated.text)
        self.assertTrue(validated.json()["ok"], validated.text)
        built = self.client.post(
            f"/api/v1/campaigns/versions/{second_version_id}/build",
            headers=headers,
            json={"write_eml": True},
        )
        self.assertEqual(built.status_code, 200, built.text)
        self.assertEqual(built.json()["built_count"], 2, built.text)

        first_report = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/report",
            headers=headers,
            params={"version_id": first_version_id},
        )
        second_report = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/report",
            headers=headers,
            params={"version_id": second_version_id},
        )
        self.assertEqual(first_report.status_code, 200, first_report.text)
        self.assertEqual(second_report.status_code, 200, second_report.text)
        self.assertEqual(first_report.json()["selected_version_id"], first_version_id)
        self.assertEqual(first_report.json()["cards"]["jobs_total"], 1)
        self.assertEqual(second_report.json()["selected_version_id"], second_version_id)
        self.assertEqual(second_report.json()["cards"]["jobs_total"], 2)

        first_jobs = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/jobs",
            headers=headers,
            params={"version_id": first_version_id, "page_size": 1},
        )
        self.assertEqual(first_jobs.status_code, 200, first_jobs.text)
        self.assertEqual(first_jobs.json()["total"], 1)
        self.assertEqual(first_jobs.json()["total_unfiltered"], 1)
        self.assertEqual(first_jobs.json()["review"]["required_count"], 0)
        self.assertIn("reviewed", first_jobs.json()["jobs"][0])
        self.assertNotIn("resolved_recipients", first_jobs.json()["jobs"][0])

        first_csv = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/report/jobs.csv",
            headers=headers,
            params={"version_id": first_version_id},
        )
        second_csv = self.client.get(
            f"/api/v1/campaigns/{campaign_id}/report/jobs.csv",
            headers=headers,
            params={"version_id": second_version_id},
        )
        self.assertEqual(first_csv.status_code, 200, first_csv.text)
        self.assertEqual(second_csv.status_code, 200, second_csv.text)
        self.assertEqual(len(first_csv.text.strip().splitlines()), 2)
        self.assertEqual(len(second_csv.text.strip().splitlines()), 3)

        emailed = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/report/email",
            headers=headers,
            json={
                "version_id": first_version_id,
                "to": ["auditor@example.org"],
                "attach_jobs_csv": True,
                "dry_run": True,
            },
        )
        self.assertEqual(emailed.status_code, 200, emailed.text)
        self.assertEqual(emailed.json()["result"]["version_id"], first_version_id)
        self.assertTrue(emailed.json()["result"]["attached_jobs_csv"])
        self.assertFalse(emailed.json()["result"]["sent"])


    def test_tenant_user_group_role_and_api_key_administration(self) -> None:
        headers, login = self._login()
        system_headers, _ = self._login()

        overview = self.client.get("/api/v1/admin/overview", headers=headers)
        self.assertEqual(overview.status_code, 200, overview.text)
        self.assertEqual(overview.json()["tenant_count"], 1)

        created_tenant = self.client.post(
            "/api/v1/admin/tenants",
            headers=headers,
            json={
                "slug": "operations",
                "name": "Operations",
                "description": "Operational tenant",
                "default_locale": "de",
                "settings": {},
            },
        )
        self.assertEqual(created_tenant.status_code, 201, created_tenant.text)
        tenant_id = created_tenant.json()["id"]

        # Suspending the tenant that backs the current session is rejected to
        # prevent an administrator from invalidating the only context through
        # which they can repair the instance. Switch first, then suspend.
        reject_active_tenant_suspend = self.client.patch(
            f"/api/v1/admin/tenants/{login['tenant']['id']}",
            headers=system_headers,
            json={"is_active": False},
        )
        self.assertEqual(reject_active_tenant_suspend.status_code, 409, reject_active_tenant_suspend.text)

        # System roles do not bypass tenant context. Tenant administration must
        # use a membership-backed active tenant selected on the session.
        cross_tenant_users = self.client.get(
            "/api/v1/admin/users",
            headers=system_headers,
            params={"tenant_id": tenant_id},
        )
        self.assertEqual(cross_tenant_users.status_code, 409, cross_tenant_users.text)

        switched = self.client.post(
            "/api/v1/auth/switch-tenant",
            headers=headers,
            json={"tenant_id": tenant_id},
        )
        self.assertEqual(switched.status_code, 200, switched.text)
        self.assertEqual(switched.json()["active_tenant"]["id"], tenant_id)
        switched_scopes = switched.json()["scopes"]
        self.assertIn("tenant:*", switched_scopes)
        self.assertIn("system:*", switched_scopes)

        roles = self.client.get("/api/v1/admin/roles", headers=headers)
        self.assertEqual(roles.status_code, 200, roles.text)
        owner_role = next(role for role in roles.json()["roles"] if role["slug"] == "owner")

        custom_role = self.client.post(
            "/api/v1/admin/roles",
            headers=headers,
            json={
                "slug": "file-reader",
                "name": "File reader",
                "description": "Read managed files only",
                "permissions": ["files:read"],
            },
        )
        self.assertEqual(custom_role.status_code, 201, custom_role.text)
        custom_role_id = custom_role.json()["id"]

        group = self.client.post(
            "/api/v1/admin/groups",
            headers=headers,
            json={
                "slug": "file-reviewers",
                "name": "File reviewers",
                "description": "Read-only file access",
                "member_ids": [],
                "role_ids": [custom_role_id],
            },
        )
        self.assertEqual(group.status_code, 201, group.text)
        group_id = group.json()["id"]

        created_user = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": "reader@example.local",
                "display_name": "Reader",
                "password": "reader-password-123",
                "password_reset_required": False,
                "is_active": True,
                "group_ids": [group_id],
                "role_ids": [],
            },
        )
        self.assertEqual(created_user.status_code, 201, created_user.text)
        reader_id = created_user.json()["user"]["id"]
        self.assertEqual(created_user.json()["temporary_password"], "reader-password-123")
        self.assertEqual(created_user.json()["user"]["roles"], [])
        self.assertIn("files:read", created_user.json()["user"]["effective_scopes"])

        reader_login = self.client.post(
            "/api/v1/auth/login",
            json={
                "email": "reader@example.local",
                "password": "reader-password-123",
                "tenant_slug": "operations",
            },
        )
        self.assertEqual(reader_login.status_code, 200, reader_login.text)
        reader_headers = {"Authorization": f"Bearer {reader_login.json()['access_token']}"}

        file_spaces = self.client.get("/api/v1/files/spaces", headers=reader_headers)
        self.assertEqual(file_spaces.status_code, 200, file_spaces.text)
        denied_admin = self.client.get("/api/v1/admin/users", headers=reader_headers)
        self.assertEqual(denied_admin.status_code, 403, denied_admin.text)

        excessive_key = self.client.post(
            "/api/v1/admin/api-keys",
            headers=headers,
            json={
                "name": "Too broad",
                "user_id": reader_id,
                "scopes": ["campaign:send"],
            },
        )
        self.assertEqual(excessive_key.status_code, 422, excessive_key.text)

        reader_key = self.client.post(
            "/api/v1/admin/api-keys",
            headers=headers,
            json={
                "name": "Reader key",
                "user_id": reader_id,
                "scopes": ["files:read"],
            },
        )
        self.assertEqual(reader_key.status_code, 201, reader_key.text)
        api_headers = {"X-API-Key": reader_key.json()["secret"]}
        self.assertEqual(self.client.get("/api/v1/files/spaces", headers=api_headers).status_code, 200)
        self.assertEqual(self.client.get("/api/v1/campaigns", headers=api_headers).status_code, 403)

        # API keys are bounded by the owning membership's live permissions.
        # Removing the group's role must immediately narrow the existing key.
        narrowed_group = self.client.patch(
            f"/api/v1/admin/groups/{group_id}",
            headers=headers,
            json={"role_ids": []},
        )
        self.assertEqual(narrowed_group.status_code, 200, narrowed_group.text)
        self.assertEqual(self.client.get("/api/v1/files/spaces", headers=api_headers).status_code, 403)

        # The active tenant must retain an operational owner. The only owner is
        # the system administrator's automatically created membership.
        remove_last_owner = self.client.patch(
            f"/api/v1/admin/users/{login['user']['id']}",
            headers=headers,
            json={"group_ids": [], "role_ids": []},
        )
        # The original login user id belongs to the default tenant, so the
        # operations tenant correctly hides it rather than crossing tenant scope.
        self.assertEqual(remove_last_owner.status_code, 404, remove_last_owner.text)

        operations_users = self.client.get("/api/v1/admin/users", headers=headers)
        self.assertEqual(operations_users.status_code, 200, operations_users.text)
        operation_admin = next(user for user in operations_users.json()["users"] if user["email"] == "admin@example.local")
        self.assertTrue(any(role["id"] == owner_role["id"] for role in operation_admin["roles"]))
        remove_last_owner = self.client.patch(
            f"/api/v1/admin/users/{operation_admin['id']}",
            headers=headers,
            json={"group_ids": [], "role_ids": []},
        )
        self.assertEqual(remove_last_owner.status_code, 409, remove_last_owner.text)

        tenant_owner = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": "tenant-owner@example.local",
                "display_name": "Tenant Owner",
                "password": "tenant-owner-password",
                "password_reset_required": False,
                "is_active": True,
                "group_ids": [],
                "role_ids": [owner_role["id"]],
            },
        )
        self.assertEqual(tenant_owner.status_code, 201, tenant_owner.text)
        tenant_owner_account_id = tenant_owner.json()["user"]["account_id"]
        tenant_owner_login = self.client.post(
            "/api/v1/auth/login",
            json={
                "email": "tenant-owner@example.local",
                "password": "tenant-owner-password",
                "tenant_slug": "operations",
            },
        )
        self.assertEqual(tenant_owner_login.status_code, 200, tenant_owner_login.text)
        tenant_owner_scopes = tenant_owner_login.json()["scopes"]
        self.assertIn("tenant:*", tenant_owner_scopes)
        self.assertNotIn("system:*", tenant_owner_scopes)
        tenant_owner_headers = {"Authorization": f"Bearer {tenant_owner_login.json()['access_token']}"}
        self.assertEqual(self.client.get("/api/v1/admin/users", headers=tenant_owner_headers).status_code, 200)
        self.assertEqual(self.client.get("/api/v1/admin/tenants", headers=tenant_owner_headers).status_code, 403)
        tenant_settings = self.client.get("/api/v1/admin/tenant/settings", headers=tenant_owner_headers)
        self.assertEqual(tenant_settings.status_code, 200, tenant_settings.text)
        self.assertEqual(tenant_settings.json()["default_locale"], "de")
        updated_tenant_settings = self.client.patch(
            "/api/v1/admin/tenant/settings",
            headers=tenant_owner_headers,
            json={"default_locale": "de-AT"},
        )
        self.assertEqual(updated_tenant_settings.status_code, 200, updated_tenant_settings.text)
        self.assertEqual(updated_tenant_settings.json()["default_locale"], "de-AT")
        refreshed_tenant_owner = self.client.get("/api/v1/auth/me", headers=tenant_owner_headers)
        self.assertEqual(refreshed_tenant_owner.status_code, 200, refreshed_tenant_owner.text)
        self.assertEqual(refreshed_tenant_owner.json()["active_tenant"]["default_locale"], "de-AT")

        # Global account suspension immediately invalidates all of its tenant
        # sessions. Reactivation restores access without changing memberships.
        disabled_tenant_owner = self.client.patch(
            f"/api/v1/admin/system/accounts/{tenant_owner_account_id}",
            headers=system_headers,
            json={"is_active": False},
        )
        self.assertEqual(disabled_tenant_owner.status_code, 200, disabled_tenant_owner.text)
        self.assertEqual(self.client.get("/api/v1/auth/me", headers=tenant_owner_headers).status_code, 401)
        reenabled_tenant_owner = self.client.patch(
            f"/api/v1/admin/system/accounts/{tenant_owner_account_id}",
            headers=system_headers,
            json={"is_active": True},
        )
        self.assertEqual(reenabled_tenant_owner.status_code, 200, reenabled_tenant_owner.text)
        self.assertEqual(self.client.get("/api/v1/auth/me", headers=tenant_owner_headers).status_code, 200)

        system_accounts = self.client.get("/api/v1/admin/system/accounts", headers=system_headers)
        self.assertEqual(system_accounts.status_code, 200, system_accounts.text)
        admin_account = next(account for account in system_accounts.json()["accounts"] if account["email"] == "admin@example.local")
        remove_last_system_owner = self.client.put(
            f"/api/v1/admin/system/accounts/{admin_account['account_id']}/roles",
            headers=system_headers,
            json={"role_ids": []},
        )
        self.assertEqual(remove_last_system_owner.status_code, 409, remove_last_system_owner.text)
        deactivate_last_system_owner = self.client.patch(
            f"/api/v1/admin/system/accounts/{admin_account['account_id']}",
            headers=system_headers,
            json={"is_active": False},
        )
        self.assertEqual(deactivate_last_system_owner.status_code, 409, deactivate_last_system_owner.text)

        suspended = self.client.patch(
            f"/api/v1/admin/tenants/{tenant_id}",
            headers=system_headers,
            json={"is_active": False},
        )
        self.assertEqual(suspended.status_code, 200, suspended.text)
        self.assertEqual(self.client.get("/api/v1/auth/me", headers=headers).status_code, 401)
        reactivated = self.client.patch(
            f"/api/v1/admin/tenants/{tenant_id}",
            headers=system_headers,
            json={"is_active": True},
        )
        self.assertEqual(reactivated.status_code, 200, reactivated.text)
        self.assertEqual(self.client.get("/api/v1/auth/me", headers=headers).status_code, 200)

        audit = self.client.get("/api/v1/admin/audit", headers=headers)
        self.assertEqual(audit.status_code, 200, audit.text)
        actions = {item["action"] for item in audit.json()["items"]}
        self.assertIn("user.created", actions)
        self.assertIn("group.created", actions)
        self.assertIn("role.created", actions)
        self.assertIn("tenant.settings.updated", actions)



    def test_access_admin_users_delta_tracks_create_and_update(self) -> None:
        headers, _ = self._login()

        initial = self.client.get("/api/v1/admin/users/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        created = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": "delta-user@example.local",
                "display_name": "Delta User",
                "password": "delta-user-password",
                "password_reset_required": False,
                "is_active": True,
                "group_ids": [],
                "role_ids": [],
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        user_id = created.json()["user"]["id"]

        created_delta = self.client.get(
            "/api/v1/admin/users/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(created_delta.status_code, 200, created_delta.text)
        created_payload = created_delta.json()
        self.assertFalse(created_payload["full"])
        self.assertEqual(created_payload["deleted"], [])
        self.assertIn("delta-user@example.local", {user["email"] for user in created_payload["users"]})

        updated = self.client.patch(
            f"/api/v1/admin/users/{user_id}",
            headers=headers,
            json={"display_name": "Delta User Updated", "group_ids": [], "role_ids": []},
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        updated_delta = self.client.get(
            "/api/v1/admin/users/delta",
            headers=headers,
            params={"since": created_payload["watermark"]},
        )
        self.assertEqual(updated_delta.status_code, 200, updated_delta.text)
        updated_payload = updated_delta.json()
        self.assertFalse(updated_payload["full"])
        changed_user = next(user for user in updated_payload["users"] if user["id"] == user_id)
        self.assertEqual(changed_user["display_name"], "Delta User Updated")

    def test_access_admin_api_key_delta_returns_tombstone_when_revoked_hidden(self) -> None:
        headers, login = self._login()

        initial = self.client.get("/api/v1/admin/api-keys/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        created = self.client.post(
            "/api/v1/admin/api-keys",
            headers=headers,
            json={
                "name": "Delta API key",
                "user_id": login["user"]["id"],
                "scopes": ["files:read"],
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        key_id = created.json()["id"]

        created_delta = self.client.get(
            "/api/v1/admin/api-keys/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(created_delta.status_code, 200, created_delta.text)
        created_payload = created_delta.json()
        self.assertFalse(created_payload["full"])
        self.assertIn(key_id, {item["id"] for item in created_payload["api_keys"]})

        revoked = self.client.post(f"/api/v1/admin/api-keys/{key_id}/revoke", headers=headers)
        self.assertEqual(revoked.status_code, 200, revoked.text)

        hidden_revoked_delta = self.client.get(
            "/api/v1/admin/api-keys/delta",
            headers=headers,
            params={"since": created_payload["watermark"]},
        )
        self.assertEqual(hidden_revoked_delta.status_code, 200, hidden_revoked_delta.text)
        hidden_payload = hidden_revoked_delta.json()
        self.assertFalse(hidden_payload["full"])
        self.assertTrue(
            any(item["id"] == key_id and item["resource_type"] == "access_api_key" for item in hidden_payload["deleted"]),
            hidden_payload["deleted"],
        )

        visible_revoked_delta = self.client.get(
            "/api/v1/admin/api-keys/delta",
            headers=headers,
            params={"since": created_payload["watermark"], "include_revoked": "true"},
        )
        self.assertEqual(visible_revoked_delta.status_code, 200, visible_revoked_delta.text)
        visible_key = next(item for item in visible_revoked_delta.json()["api_keys"] if item["id"] == key_id)
        self.assertIsNotNone(visible_key["revoked_at"])

    def test_settings_deltas_track_sections_and_system_language_dependency(self) -> None:
        headers, _ = self._login()

        system_initial = self.client.get("/api/v1/admin/system/settings/delta", headers=headers)
        self.assertEqual(system_initial.status_code, 200, system_initial.text)
        system_initial_payload = system_initial.json()
        self.assertTrue(system_initial_payload["full"])
        self.assertTrue(system_initial_payload["watermark"])

        tenant_initial = self.client.get("/api/v1/admin/tenant/settings/delta", headers=headers)
        self.assertEqual(tenant_initial.status_code, 200, tenant_initial.text)
        tenant_initial_payload = tenant_initial.json()
        self.assertTrue(tenant_initial_payload["full"])
        self.assertTrue(tenant_initial_payload["watermark"])

        system_item = system_initial_payload["item"]
        available_languages = list(system_item["available_languages"])
        if "fr" not in {item["code"] for item in available_languages}:
            available_languages.append({"code": "fr", "label": "French", "native_label": "Francais"})
        enabled_codes = list(dict.fromkeys([*system_item["enabled_language_codes"], "fr"]))
        updated_system = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": system_item["default_locale"],
                "allow_tenant_custom_groups": system_item["allow_tenant_custom_groups"],
                "allow_tenant_custom_roles": system_item["allow_tenant_custom_roles"],
                "allow_tenant_api_keys": system_item["allow_tenant_api_keys"],
                "available_languages": available_languages,
                "enabled_language_codes": enabled_codes,
            },
        )
        self.assertEqual(updated_system.status_code, 200, updated_system.text)

        system_delta = self.client.get(
            "/api/v1/admin/system/settings/delta",
            headers=headers,
            params={"since": system_initial_payload["watermark"]},
        )
        self.assertEqual(system_delta.status_code, 200, system_delta.text)
        system_delta_payload = system_delta.json()
        self.assertFalse(system_delta_payload["full"])
        self.assertIn("languages", system_delta_payload["changed_sections"])
        self.assertIn("fr", system_delta_payload["sections"]["languages"]["enabled_language_codes"])

        tenant_delta = self.client.get(
            "/api/v1/admin/tenant/settings/delta",
            headers=headers,
            params={"since": tenant_initial_payload["watermark"]},
        )
        self.assertEqual(tenant_delta.status_code, 200, tenant_delta.text)
        tenant_delta_payload = tenant_delta.json()
        self.assertFalse(tenant_delta_payload["full"])
        self.assertIn("languages", tenant_delta_payload["changed_sections"])
        self.assertIn("fr", tenant_delta_payload["sections"]["languages"]["system_enabled_language_codes"])

    def test_tenant_admin_delta_tracks_create_and_update(self) -> None:
        headers, _ = self._login()

        initial = self.client.get("/api/v1/admin/tenants/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        created = self.client.post(
            "/api/v1/admin/tenants",
            headers=headers,
            json={"slug": "tenant-delta", "name": "Tenant Delta", "settings": {}},
        )
        self.assertEqual(created.status_code, 201, created.text)
        tenant_id = created.json()["id"]

        created_delta = self.client.get(
            "/api/v1/admin/tenants/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(created_delta.status_code, 200, created_delta.text)
        created_payload = created_delta.json()
        self.assertFalse(created_payload["full"])
        self.assertIn("Tenant Delta", {tenant["name"] for tenant in created_payload["tenants"]})
        self.assertFalse(created_payload["deleted"])

        updated = self.client.patch(
            f"/api/v1/admin/tenants/{tenant_id}",
            headers=headers,
            json={"name": "Tenant Delta Updated", "is_active": False},
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        updated_delta = self.client.get(
            "/api/v1/admin/tenants/delta",
            headers=headers,
            params={"since": created_payload["watermark"]},
        )
        self.assertEqual(updated_delta.status_code, 200, updated_delta.text)
        updated_payload = updated_delta.json()
        self.assertFalse(updated_payload["full"])
        updated_item = next(tenant for tenant in updated_payload["tenants"] if tenant["id"] == tenant_id)
        self.assertEqual(updated_item["name"], "Tenant Delta Updated")
        self.assertFalse(updated_item["is_active"])

    def test_governance_template_delta_tracks_create_update_and_delete(self) -> None:
        headers, login = self._login()
        tenant_id = login["tenant"]["id"]
        approver_headers = self._create_system_approver(headers, tenant_id=tenant_id, email="governance-delta-approver@example.local")

        initial = self.client.get("/api/v1/admin/system/governance-templates/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        create_payload = {
            "kind": "group",
            "slug": "governance-delta",
            "name": "Governance Delta",
            "description": "Delta tracked system group",
            "permissions": [],
            "is_active": True,
            "assignments": [{"tenant_id": tenant_id, "mode": "available"}],
        }
        create_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="governance_templates",
            value=create_payload,
            target={"kind": "group", "slug": "governance-delta"},
        )
        created = self.client.post(
            "/api/v1/admin/system/governance-templates",
            headers=headers,
            json={**create_payload, "change_request_id": create_change_request_id},
        )
        self.assertEqual(created.status_code, 201, created.text)
        template_id = created.json()["id"]

        created_delta = self.client.get(
            "/api/v1/admin/system/governance-templates/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(created_delta.status_code, 200, created_delta.text)
        created_payload = created_delta.json()
        self.assertFalse(created_payload["full"])
        self.assertIn(template_id, {item["id"] for item in created_payload["templates"]})

        update_payload = {
            "name": "Governance Delta Updated",
            "description": "Delta tracked system group updated",
            "permissions": [],
            "is_active": False,
            "assignments": [{"tenant_id": tenant_id, "mode": "available"}],
        }
        update_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="governance_templates",
            value=update_payload,
            target={"kind": "group", "id": template_id},
        )
        updated = self.client.patch(
            f"/api/v1/admin/system/governance-templates/{template_id}",
            headers=headers,
            json={**update_payload, "change_request_id": update_change_request_id},
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        updated_delta = self.client.get(
            "/api/v1/admin/system/governance-templates/delta",
            headers=headers,
            params={"since": created_payload["watermark"]},
        )
        self.assertEqual(updated_delta.status_code, 200, updated_delta.text)
        updated_item = next(item for item in updated_delta.json()["templates"] if item["id"] == template_id)
        self.assertEqual(updated_item["name"], "Governance Delta Updated")
        self.assertFalse(updated_item["is_active"])

        delete_value = {"id": template_id, "kind": "group", "delete": True}
        delete_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="governance_templates",
            value=delete_value,
            target={"kind": "group", "id": template_id},
        )
        deleted = self.client.delete(
            f"/api/v1/admin/system/governance-templates/{template_id}",
            headers=headers,
            params={"change_request_id": delete_change_request_id},
        )
        self.assertEqual(deleted.status_code, 204, deleted.text)

        deleted_delta = self.client.get(
            "/api/v1/admin/system/governance-templates/delta",
            headers=headers,
            params={"since": updated_delta.json()["watermark"]},
        )
        self.assertEqual(deleted_delta.status_code, 200, deleted_delta.text)
        self.assertTrue(any(item["id"] == template_id and item["resource_type"] == "governance_template" for item in deleted_delta.json()["deleted"]))

    def test_module_installer_history_supports_cursor_windows(self) -> None:
        from govoplan_core.core.module_installer import default_installer_runtime_dir

        headers, _ = self._login()
        runtime_dir = default_installer_runtime_dir(os.environ["DATABASE_URL"])
        shutil.rmtree(runtime_dir, ignore_errors=True)
        for run_id in ("run-001", "run-002", "run-003"):
            run_dir = runtime_dir / "runs" / run_id
            run_dir.mkdir(parents=True, exist_ok=True)
            (run_dir / "record.json").write_text(
                json.dumps({"run_id": run_id, "status": "applied", "started_at": f"2030-01-0{run_id[-1]}T00:00:00+00:00", "commands": [], "plan": []}),
                encoding="utf-8",
            )
        requests_dir = runtime_dir / "requests"
        requests_dir.mkdir(parents=True, exist_ok=True)
        for request_id in ("request-001", "request-002", "request-003"):
            (requests_dir / f"{request_id}.json").write_text(
                json.dumps({"request_id": request_id, "status": "queued", "created_at": f"2030-01-0{request_id[-1]}T00:00:00+00:00", "options": {}}),
                encoding="utf-8",
            )

        first_runs = self.client.get(
            "/api/v1/admin/system/modules/install-runs",
            headers=headers,
            params={"page_size": 2},
        )
        self.assertEqual(first_runs.status_code, 200, first_runs.text)
        first_runs_payload = first_runs.json()
        self.assertEqual([item["run_id"] for item in first_runs_payload["runs"]], ["run-003", "run-002"])
        self.assertTrue(first_runs_payload["next_cursor"])

        next_runs = self.client.get(
            "/api/v1/admin/system/modules/install-runs",
            headers=headers,
            params={"page_size": 2, "cursor": first_runs_payload["next_cursor"]},
        )
        self.assertEqual(next_runs.status_code, 200, next_runs.text)
        self.assertEqual([item["run_id"] for item in next_runs.json()["runs"]], ["run-001"])

        shutil.rmtree(runtime_dir / "runs" / "run-002", ignore_errors=True)
        stale_runs = self.client.get(
            "/api/v1/admin/system/modules/install-runs",
            headers=headers,
            params={"page_size": 2, "cursor": first_runs_payload["next_cursor"]},
        )
        self.assertEqual(stale_runs.status_code, 200, stale_runs.text)
        self.assertTrue(stale_runs.json()["full"])
        self.assertEqual([item["run_id"] for item in stale_runs.json()["runs"]], ["run-003", "run-001"])

        first_requests = self.client.get(
            "/api/v1/admin/system/modules/install-requests",
            headers=headers,
            params={"page_size": 2},
        )
        self.assertEqual(first_requests.status_code, 200, first_requests.text)
        first_requests_payload = first_requests.json()
        self.assertEqual([item["request_id"] for item in first_requests_payload["requests"]], ["request-003", "request-002"])
        self.assertTrue(first_requests_payload["next_cursor"])

        next_requests = self.client.get(
            "/api/v1/admin/system/modules/install-requests",
            headers=headers,
            params={"page_size": 2, "cursor": first_requests_payload["next_cursor"]},
        )
        self.assertEqual(next_requests.status_code, 200, next_requests.text)
        self.assertEqual([item["request_id"] for item in next_requests.json()["requests"]], ["request-001"])

    def test_system_accounts_delta_tolerates_repeated_no_change_reload(self) -> None:
        headers, _ = self._login()
        initial = self.client.get("/api/v1/admin/system/accounts/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        first_reload = self.client.get(
            "/api/v1/admin/system/accounts/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(first_reload.status_code, 200, first_reload.text)
        first_payload = first_reload.json()
        self.assertFalse(first_payload["full"])

        second_reload = self.client.get(
            "/api/v1/admin/system/accounts/delta",
            headers=headers,
            params={"since": first_payload["watermark"]},
        )
        self.assertEqual(second_reload.status_code, 200, second_reload.text)
        second_payload = second_reload.json()
        self.assertFalse(second_payload["full"])

    def test_configuration_changes_delta_tracks_requests(self) -> None:
        headers, _ = self._login()

        initial = self.client.get("/api/v1/admin/configuration-changes/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        created = self.client.post(
            "/api/v1/admin/configuration-change-requests",
            headers=headers,
            json={
                "key": "maintenance_mode",
                "value": {"enabled": False, "message": "delta request"},
                "dry_run": True,
                "target": {"scope": "system"},
                "reason": "delta smoke test",
            },
        )
        self.assertEqual(created.status_code, 201, created.text)
        request_id = created.json()["request"]["id"]

        delta = self.client.get(
            "/api/v1/admin/configuration-changes/delta",
            headers=headers,
            params={"since": initial_payload["watermark"]},
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertIn(request_id, {item["id"] for item in delta_payload["requests"]})

    def test_admin_audit_delta_tracks_new_events(self) -> None:
        headers, _ = self._login()

        initial = self.client.get("/api/v1/admin/audit/delta", headers=headers, params={"scope": "tenant", "page_size": 25})
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])
        self.assertTrue(initial_payload["watermark"])

        created = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": "audit-delta-user@example.local",
                "display_name": "Audit Delta User",
                "password": "audit-delta-password",
                "password_reset_required": False,
                "is_active": True,
                "group_ids": [],
                "role_ids": [],
            },
        )
        self.assertEqual(created.status_code, 201, created.text)

        delta = self.client.get(
            "/api/v1/admin/audit/delta",
            headers=headers,
            params={"scope": "tenant", "page_size": 25, "since": initial_payload["watermark"]},
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertIn("user.created", {item["action"] for item in delta_payload["items"]})

    def test_access_admin_users_delta_paginates_enforces_permissions_and_tenant_scope(self) -> None:
        headers, _ = self._login()

        initial = self.client.get("/api/v1/admin/users/delta", headers=headers)
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_watermark = initial.json()["watermark"]

        for index in range(2):
            created = self.client.post(
                "/api/v1/admin/users",
                headers=headers,
                json={
                    "email": f"delta-page-{index}@example.local",
                    "display_name": f"Delta Page {index}",
                    "password": "delta-page-password",
                    "password_reset_required": False,
                    "is_active": True,
                    "group_ids": [],
                    "role_ids": [],
                },
            )
            self.assertEqual(created.status_code, 201, created.text)

        page_one = self.client.get(
            "/api/v1/admin/users/delta",
            headers=headers,
            params={"since": initial_watermark, "limit": 1},
        )
        self.assertEqual(page_one.status_code, 200, page_one.text)
        page_one_payload = page_one.json()
        self.assertFalse(page_one_payload["full"])
        self.assertTrue(page_one_payload["has_more"])
        self.assertEqual(len(page_one_payload["users"]), 1)

        page_two = self.client.get(
            "/api/v1/admin/users/delta",
            headers=headers,
            params={"since": page_one_payload["watermark"], "limit": 1},
        )
        self.assertEqual(page_two.status_code, 200, page_two.text)
        self.assertFalse(page_two.json()["full"])
        self.assertEqual(len(page_two.json()["users"]), 1)

        reader_role = self._create_role(headers, slug="delta-file-reader", permissions=["files:read"])
        self._create_user(
            headers,
            email="delta-reader@example.local",
            password="delta-reader-password",
            role_ids=[str(reader_role["id"])],
        )
        reader_headers, _ = self._login_as(email="delta-reader@example.local", password="delta-reader-password")
        denied = self.client.get("/api/v1/admin/users/delta", headers=reader_headers)
        self.assertEqual(denied.status_code, 403, denied.text)

        scoped_initial = self.client.get("/api/v1/admin/users/delta", headers=headers)
        self.assertEqual(scoped_initial.status_code, 200, scoped_initial.text)
        created_tenant = self.client.post(
            "/api/v1/admin/tenants",
            headers=headers,
            json={"slug": "delta-scope", "name": "Delta scope", "settings": {}},
        )
        self.assertEqual(created_tenant.status_code, 201, created_tenant.text)
        switched = self.client.post(
            "/api/v1/auth/switch-tenant",
            headers=headers,
            json={"tenant_id": created_tenant.json()["id"]},
        )
        self.assertEqual(switched.status_code, 200, switched.text)
        cross_tenant_user = self.client.post(
            "/api/v1/admin/users",
            headers=headers,
            json={
                "email": "other-tenant-delta@example.local",
                "display_name": "Other Tenant Delta",
                "password": "other-tenant-delta-password",
                "password_reset_required": False,
                "is_active": True,
                "group_ids": [],
                "role_ids": [],
            },
        )
        self.assertEqual(cross_tenant_user.status_code, 201, cross_tenant_user.text)

        default_headers, _ = self._login()
        scoped_delta = self.client.get(
            "/api/v1/admin/users/delta",
            headers=default_headers,
            params={"since": scoped_initial.json()["watermark"]},
        )
        self.assertEqual(scoped_delta.status_code, 200, scoped_delta.text)
        self.assertNotIn("other-tenant-delta@example.local", {user["email"] for user in scoped_delta.json()["users"]})

    def test_admin_audit_delta_supports_filtered_sorted_first_page(self) -> None:
        headers, _ = self._login()

        initial = self.client.get(
            "/api/v1/admin/audit/delta",
            headers=headers,
            params={
                "scope": "system",
                "page_size": 25,
                "sort_by": "action",
                "sort_direction": "asc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(initial.status_code, 200, initial.text)
        initial_payload = initial.json()
        self.assertTrue(initial_payload["full"])

        settings_response = self.client.get("/api/v1/admin/system/settings", headers=headers)
        self.assertEqual(settings_response.status_code, 200, settings_response.text)
        settings_payload = settings_response.json()
        updated = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": settings_payload["default_locale"],
                "allow_tenant_custom_groups": settings_payload["allow_tenant_custom_groups"],
                "allow_tenant_custom_roles": settings_payload["allow_tenant_custom_roles"],
                "allow_tenant_api_keys": settings_payload["allow_tenant_api_keys"],
                "available_languages": settings_payload["available_languages"],
                "enabled_language_codes": settings_payload["enabled_language_codes"],
            },
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        delta = self.client.get(
            "/api/v1/admin/audit/delta",
            headers=headers,
            params={
                "scope": "system",
                "page_size": 25,
                "sort_by": "action",
                "sort_direction": "asc",
                "filter_action": "system_settings",
                "since": initial_payload["watermark"],
            },
        )
        self.assertEqual(delta.status_code, 200, delta.text)
        delta_payload = delta.json()
        self.assertFalse(delta_payload["full"])
        self.assertTrue(all("system_settings" in item["action"] for item in delta_payload["items"]))
        self.assertIn("system_settings.updated", {item["action"] for item in delta_payload["items"]})

    def test_system_governance_defaults_templates_and_central_accounts(self) -> None:
        headers, login = self._login()
        tenant_id = login["tenant"]["id"]

        settings_response = self.client.get("/api/v1/admin/system/settings", headers=headers)
        self.assertEqual(settings_response.status_code, 200, settings_response.text)
        self.assertTrue(settings_response.json()["allow_tenant_custom_groups"])
        self.assertEqual(settings_response.json()["privacy_retention_policy"]["audit_detail_level"], "full")
        approver_headers = self._create_system_approver(headers, tenant_id=tenant_id)

        privacy_update = {
            **settings_response.json()["privacy_retention_policy"],
            "audit_detail_level": "redacted",
            "mock_mailbox_retention_days": 0,
        }
        settings_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="privacy_retention_policy",
            value=privacy_update,
            target={"scope": "system"},
        )

        deny_local_groups = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": "en",
                "allow_tenant_custom_groups": False,
                "allow_tenant_custom_roles": True,
                "allow_tenant_api_keys": True,
                "privacy_retention_policy": privacy_update,
                "change_request_id": settings_change_request_id,
            },
        )
        self.assertEqual(deny_local_groups.status_code, 200, deny_local_groups.text)
        self.assertEqual(deny_local_groups.json()["privacy_retention_policy"]["audit_detail_level"], "redacted")
        self.assertEqual(deny_local_groups.json()["privacy_retention_policy"]["mock_mailbox_retention_days"], 0)

        widened_tenant = self.client.patch(
            f"/api/v1/admin/tenants/{tenant_id}",
            headers=headers,
            json={"allow_custom_groups": True},
        )
        self.assertEqual(widened_tenant.status_code, 422, widened_tenant.text)

        retention_patch = {"allow_lower_level_limits": {"raw_campaign_json_retention_days": False}}
        retention_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="privacy_retention_policy",
            value=retention_patch,
            target={"scope_type": "system", "scope_id": None},
        )
        locked_retention = self.client.put(
            "/api/v1/admin/privacy-retention/policies/system",
            headers=headers,
            json={"policy": retention_patch, "change_request_id": retention_change_request_id},
        )
        self.assertEqual(locked_retention.status_code, 200, locked_retention.text)
        self.assertFalse(locked_retention.json()["effective_policy"]["allow_lower_level_limits"]["raw_campaign_json_retention_days"])
        retention_explain = self.client.get(
            "/api/v1/admin/privacy-retention/policies/tenant/explain",
            headers=headers,
        )
        self.assertEqual(retention_explain.status_code, 200, retention_explain.text)
        retention_explain_payload = retention_explain.json()
        self.assertFalse(retention_explain_payload["decision"]["allowed"])
        self.assertIn("raw_campaign_json_retention_days", retention_explain_payload["blocked_fields"])
        self.assertEqual(retention_explain_payload["parent_policy_sources"][0]["path"], "system")
        self.assertEqual(retention_explain_payload["decision"]["source_path"][0]["path"], "system")
        tenant_retention_widen = self.client.put(
            "/api/v1/admin/privacy-retention/policies/tenant",
            headers=headers,
            json={"policy": {"raw_campaign_json_retention_days": 1}},
        )
        self.assertEqual(tenant_retention_widen.status_code, 422, tenant_retention_widen.text)
        tenant_retention_unlock = self.client.put(
            "/api/v1/admin/privacy-retention/policies/tenant",
            headers=headers,
            json={"policy": {"allow_lower_level_limits": {"raw_campaign_json_retention_days": True}}},
        )
        self.assertEqual(tenant_retention_unlock.status_code, 422, tenant_retention_unlock.text)

        retention_dry_run = self.client.post(
            "/api/v1/admin/system/retention/run",
            headers=headers,
            json={"dry_run": True},
        )
        self.assertEqual(retention_dry_run.status_code, 200, retention_dry_run.text)
        self.assertTrue(retention_dry_run.json()["result"]["dry_run"])
        self.assertIn("raw_campaign_json", retention_dry_run.json()["result"]["counts"])

        blocked_group = self.client.post(
            "/api/v1/admin/groups",
            headers=headers,
            json={"slug": "blocked-local", "name": "Blocked local group", "member_ids": [], "role_ids": []},
        )
        self.assertEqual(blocked_group.status_code, 409, blocked_group.text)

        central_group_payload = {
            "kind": "group",
            "slug": "case-workers",
            "name": "Case workers",
            "description": "System-controlled group identity",
            "permissions": [],
            "is_active": True,
            "assignments": [{"tenant_id": tenant_id, "mode": "required"}],
        }
        governance_change_request_id = self._approved_configuration_change(
            headers,
            approver_headers,
            key="governance_templates",
            value=central_group_payload,
            target={"kind": "group", "slug": "case-workers"},
        )
        central_group = self.client.post(
            "/api/v1/admin/system/governance-templates",
            headers=headers,
            json={**central_group_payload, "change_request_id": governance_change_request_id},
        )
        self.assertEqual(central_group.status_code, 201, central_group.text)
        template_id = central_group.json()["id"]

        tenant_groups = self.client.get("/api/v1/admin/groups", headers=headers)
        self.assertEqual(tenant_groups.status_code, 200, tenant_groups.text)
        materialized = next(group for group in tenant_groups.json()["groups"] if group["system_template_id"] == template_id)
        self.assertTrue(materialized["system_required"])
        self.assertTrue(materialized["is_active"])

        reject_required_deactivation = self.client.patch(
            f"/api/v1/admin/groups/{materialized['id']}",
            headers=headers,
            json={"is_active": False},
        )
        self.assertEqual(reject_required_deactivation.status_code, 409, reject_required_deactivation.text)

        created_tenant = self.client.post(
            "/api/v1/admin/tenants",
            headers=headers,
            json={"slug": "governed", "name": "Governed", "settings": {}},
        )
        self.assertEqual(created_tenant.status_code, 201, created_tenant.text)
        governed_tenant_id = created_tenant.json()["id"]

        central_account = self.client.post(
            "/api/v1/admin/system/accounts",
            headers=headers,
            json={
                "email": "central-user@example.local",
                "display_name": "Central User",
                "password": "central-user-password",
                "password_reset_required": False,
                "is_active": True,
                "role_ids": [],
                "memberships": [{
                    "tenant_id": governed_tenant_id,
                    "is_active": True,
                    "role_ids": [],
                    "group_ids": [],
                }],
            },
        )
        self.assertEqual(central_account.status_code, 201, central_account.text)
        memberships = central_account.json()["account"]["memberships"]
        self.assertEqual([item["tenant_id"] for item in memberships], [governed_tenant_id])

        audit = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={"all_tenants": True, "limit": 500},
        )
        self.assertEqual(audit.status_code, 200, audit.text)
        actions = {item["action"] for item in audit.json()["items"]}
        self.assertIn("system_settings.updated", actions)
        self.assertIn("governance_template.created", actions)
        self.assertIn("system_account.created", actions)
        changes = self.client.get("/api/v1/admin/configuration-changes", headers=headers)
        self.assertEqual(changes.status_code, 200, changes.text)
        history_keys = {item["key"] for item in changes.json()["history"]}
        self.assertIn("privacy_retention_policy", history_keys)
        self.assertIn("governance_templates", history_keys)


    def test_refined_permissions_are_narrow_and_enforce_delegation_ceiling(self) -> None:
        owner_headers, _ = self._login()
        designer_role = self._create_role(
            owner_headers,
            slug="role-designer",
            permissions=["admin:users:read", "admin:roles:read", "admin:roles:write", "campaign:queue"],
        )
        designer = self._create_user(
            owner_headers,
            email="designer@example.local",
            password="designer-password",
            role_ids=[str(designer_role["id"])],
        )
        designer_headers, designer_login = self._login_as(email="designer@example.local", password="designer-password")

        self.assertIn("campaign:queue", designer_login["scopes"])
        self.assertNotIn("campaign:retry", designer_login["scopes"])
        self.assertNotIn("campaign:send", designer_login["scopes"])

        excessive_role = self.client.post(
            "/api/v1/admin/roles",
            headers=designer_headers,
            json={
                "slug": "sender-escalation",
                "name": "Sender escalation",
                "description": "Must be rejected",
                "permissions": ["campaign:send"],
            },
        )
        self.assertEqual(excessive_role.status_code, 422, excessive_role.text)

        assign_without_assignment_permission = self.client.patch(
            f"/api/v1/admin/users/{designer['id']}",
            headers=designer_headers,
            json={"role_ids": []},
        )
        self.assertEqual(assign_without_assignment_permission.status_code, 403, assign_without_assignment_permission.text)

    def test_campaign_acl_separates_capability_from_object_access(self) -> None:
        owner_headers, _ = self._login()
        mail_profile_id = self._create_test_mail_profile(owner_headers, name="ACL campaign delivery")
        access_role = self._create_role(
            owner_headers,
            slug="campaign-collaborator",
            permissions=["campaign:read", "campaign:update", "recipients:read"],
        )
        reader = self._create_user(
            owner_headers,
            email="campaign-reader@example.local",
            password="reader-password",
            role_ids=[str(access_role["id"])],
        )
        self._create_user(
            owner_headers,
            email="campaign-other@example.local",
            password="other-password",
            role_ids=[str(access_role["id"])],
        )
        reader_headers, _ = self._login_as(email="campaign-reader@example.local", password="reader-password")
        other_headers, _ = self._login_as(email="campaign-other@example.local", password="other-password")

        config = {
            "version": "1.0",
            "campaign": {"id": "acl-campaign", "name": "ACL campaign", "mode": "test"},
            "fields": [],
            "global_values": {},
            "server": {"mail_profile_id": mail_profile_id},
            "recipients": {"from": {"email": "sender@example.org", "type": "to"}},
            "template": {"subject": "ACL", "text": "ACL"},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {"inline": [{"id": "one", "to": [{"email": "one@example.org", "type": "to"}], "fields": {}}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"rate_limit": {"messages_per_minute": 60}, "imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        created = self.client.post("/api/v1/campaigns", headers=owner_headers, json={"config": config})
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        self.assertEqual(self.client.get(f"/api/v1/campaigns/{campaign_id}", headers=reader_headers).status_code, 403)
        self.assertEqual(self.client.get(f"/api/v1/campaigns/{campaign_id}", headers=other_headers).status_code, 403)

        read_share = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/shares",
            headers=owner_headers,
            json={"target_type": "user", "target_id": reader["id"], "permission": "read"},
        )
        self.assertEqual(read_share.status_code, 201, read_share.text)
        self.assertEqual(self.client.get(f"/api/v1/campaigns/{campaign_id}", headers=reader_headers).status_code, 200)
        version_detail = self.client.get(f"/api/v1/campaigns/{campaign_id}/versions/{version_id}", headers=reader_headers)
        self.assertEqual(version_detail.status_code, 200, version_detail.text)
        denied_write = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/set-step",
            headers=reader_headers,
            json={"current_step": "template"},
        )
        self.assertEqual(denied_write.status_code, 403, denied_write.text)

        write_share = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/shares",
            headers=owner_headers,
            json={"target_type": "user", "target_id": reader["id"], "permission": "write"},
        )
        self.assertEqual(write_share.status_code, 201, write_share.text)
        allowed_write = self.client.post(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}/set-step",
            headers=reader_headers,
            json={"current_step": "template"},
        )
        self.assertEqual(allowed_write.status_code, 200, allowed_write.text)
        changed_config = version_detail.json()["raw_json"]
        changed_config["entries"]["inline"][0]["to"][0]["email"] = "changed@example.org"
        mutation_headers, base_revision = self._campaign_version_precondition(
            reader_headers,
            campaign_id,
            version_id,
        )
        denied_recipient_edit = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=mutation_headers,
            json={
                "campaign_json": changed_config,
                "base_revision": base_revision,
            },
        )
        self.assertEqual(denied_recipient_edit.status_code, 403, denied_recipient_edit.text)
        self.assertEqual(self.client.get(f"/api/v1/campaigns/{campaign_id}", headers=other_headers).status_code, 403)

    def test_policy_responses_include_source_provenance(self) -> None:
        headers, login = self._login()
        tenant_id = login["tenant"]["id"]

        retention = self.client.get("/api/v1/admin/privacy-retention/policies/tenant", headers=headers)
        self.assertEqual(retention.status_code, 200, retention.text)
        retention_payload = retention.json()
        self.assertEqual([item["label"] for item in retention_payload["effective_policy_sources"]], ["System", "Tenant"])
        self.assertEqual([item["path"] for item in retention_payload["effective_policy_sources"]], ["system", f"tenant:{tenant_id}"])
        self.assertEqual([item["label"] for item in retention_payload["parent_policy_sources"]], ["System"])
        self.assertIn("defaults", retention_payload["effective_policy_sources"][0]["applied_fields"])

        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "policy-source-campaign", "name": "Policy source campaign"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]

        updated = self.client.put(
            f"/api/v1/mail/policies/campaign?scope_id={campaign_id}",
            headers=headers,
            json={"policy": {"allow_campaign_profiles": False}},
        )
        self.assertEqual(updated.status_code, 200, updated.text)
        payload = updated.json()
        self.assertEqual([item["label"] for item in payload["effective_policy_sources"]], ["System", "Tenant", "Owner user", "Campaign"])
        campaign_source = payload["effective_policy_sources"][-1]
        self.assertEqual(campaign_source["scope_type"], "campaign")
        self.assertEqual(campaign_source["scope_id"], campaign_id)
        self.assertEqual(campaign_source["path"], f"campaign:{campaign_id}")
        self.assertIn("allow_campaign_profiles", campaign_source["applied_fields"])

    def test_campaign_scoped_mail_profile_policy_is_enforced(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "profile-policy-campaign", "name": "Profile policy campaign"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]

        profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Campaign SMTP",
                "scope_type": "campaign",
                "scope_id": campaign_id,
                "smtp": {"host": "allowed.smtp.local", "port": 2525, "username": "sender@example.org", "password": "secret", "security": "starttls"},
            },
        )
        self.assertEqual(profile.status_code, 201, profile.text)
        profile_id = profile.json()["id"]
        self.assertEqual(profile.json()["scope_type"], "campaign")

        effective = self.client.get(f"/api/v1/mail/profiles?campaign_id={campaign_id}", headers=headers)
        self.assertEqual(effective.status_code, 200, effective.text)
        self.assertIn(profile_id, {item["id"] for item in effective.json()["profiles"]})

        policy = self.client.put(
            f"/api/v1/mail/policies/campaign?scope_id={campaign_id}",
            headers=headers,
            json={"policy": {"blacklist": {"smtp_hosts": ["blocked.smtp.local"]}}},
        )
        self.assertEqual(policy.status_code, 200, policy.text)

        blocked = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Blocked SMTP",
                "scope_type": "campaign",
                "scope_id": campaign_id,
                "smtp": {"host": "blocked.smtp.local", "port": 2525, "username": "sender@example.org", "password": "secret", "security": "starttls"},
            },
        )
        self.assertEqual(blocked.status_code, 422, blocked.text)
        self.assertIn("blacklist", blocked.json()["detail"])

    def test_campaign_local_mail_policy_blocks_inline_but_allows_reusable_profiles(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "campaign-local-mail-policy", "name": "Campaign local mail policy"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        tenant_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Tenant reusable SMTP",
                "smtp": {"host": "tenant.smtp.local", "port": 2525, "username": "sender@example.org", "password": "secret", "security": "starttls"},
            },
        )
        self.assertEqual(tenant_profile.status_code, 201, tenant_profile.text)
        tenant_profile_id = tenant_profile.json()["id"]

        policy = self.client.put(
            f"/api/v1/mail/policies/campaign?scope_id={campaign_id}",
            headers=headers,
            json={"policy": {"allow_campaign_profiles": False}},
        )
        self.assertEqual(policy.status_code, 200, policy.text)
        self.assertFalse(policy.json()["effective_policy"]["allow_campaign_profiles"])

        blocked_profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Blocked campaign SMTP",
                "scope_type": "campaign",
                "scope_id": campaign_id,
                "smtp": {"host": "campaign.smtp.local", "port": 2525, "username": "sender@example.org", "password": "secret", "security": "starttls"},
            },
        )
        self.assertEqual(blocked_profile.status_code, 422, blocked_profile.text)
        self.assertIn("disabled by policy", blocked_profile.json()["detail"])

        detail = self.client.get(f"/api/v1/campaigns/{campaign_id}/versions/{version_id}", headers=headers)
        self.assertEqual(detail.status_code, 200, detail.text)
        inline_json = detail.json()["raw_json"]
        inline_json["server"] = {
            "smtp": {
                "host": "campaign.smtp.local",
                "port": 2525,
                "username": "sender@example.org",
                "password": "secret",
                "security": "starttls",
            }
        }
        blocked_inline = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers={**headers, "If-Match": str(detail.headers["etag"])},
            json={
                "campaign_json": inline_json,
                "base_revision": detail.json()["edit_revision"],
            },
        )
        self.assertEqual(blocked_inline.status_code, 422, blocked_inline.text)
        self.assertIn("may only reference", blocked_inline.json()["detail"])

        reusable_json = detail.json()["raw_json"]
        reusable_json["server"] = {"mail_profile_id": tenant_profile_id}
        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            version_id,
        )
        allowed_reusable = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=mutation_headers,
            json={
                "campaign_json": reusable_json,
                "base_revision": base_revision,
            },
        )
        self.assertEqual(allowed_reusable.status_code, 200, allowed_reusable.text)

    def test_allowed_mail_profile_ids_gate_campaign_selection(self) -> None:
        headers, _ = self._login()
        created = self.client.post(
            "/api/v1/campaigns/new",
            headers=headers,
            json={"external_id": "profile-selection-policy", "name": "Profile selection policy"},
        )
        self.assertEqual(created.status_code, 200, created.text)
        campaign_id = created.json()["campaign"]["id"]
        version_id = created.json()["version"]["id"]

        profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Tenant SMTP",
                "smtp": {"host": "allowed.smtp.local", "port": 2525, "username": "sender@example.org", "password": "secret", "security": "starttls"},
            },
        )
        self.assertEqual(profile.status_code, 201, profile.text)
        profile_id = profile.json()["id"]

        denied_policy = self.client.put(
            f"/api/v1/mail/policies/campaign?scope_id={campaign_id}",
            headers=headers,
            json={"policy": {"allowed_profile_ids": ["not-the-profile"]}},
        )
        self.assertEqual(denied_policy.status_code, 200, denied_policy.text)

        detail = self.client.get(f"/api/v1/campaigns/{campaign_id}/versions/{version_id}", headers=headers)
        self.assertEqual(detail.status_code, 200, detail.text)
        raw_json = detail.json()["raw_json"]
        raw_json["server"] = {"mail_profile_id": profile_id}
        denied_update = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers={**headers, "If-Match": str(detail.headers["etag"])},
            json={
                "campaign_json": raw_json,
                "base_revision": detail.json()["edit_revision"],
            },
        )
        self.assertEqual(denied_update.status_code, 422, denied_update.text)

        allowed_policy = self.client.put(
            f"/api/v1/mail/policies/campaign?scope_id={campaign_id}",
            headers=headers,
            json={"policy": {"allowed_profile_ids": [profile_id]}},
        )
        self.assertEqual(allowed_policy.status_code, 200, allowed_policy.text)
        mutation_headers, base_revision = self._campaign_version_precondition(
            headers,
            campaign_id,
            version_id,
        )
        allowed_update = self.client.put(
            f"/api/v1/campaigns/{campaign_id}/versions/{version_id}",
            headers=mutation_headers,
            json={
                "campaign_json": raw_json,
                "base_revision": base_revision,
            },
        )
        self.assertEqual(allowed_update.status_code, 200, allowed_update.text)

    def test_locked_mail_profile_credential_policy_blocks_campaign_override(self) -> None:
        headers, _ = self._login()
        profile = self.client.post(
            "/api/v1/mail/profiles",
            headers=headers,
            json={
                "name": "Locked credential SMTP",
                "smtp": {
                    "host": "mock.smtp",
                    "port": 2525,
                    "security": "starttls",
                },
                "credentials": {
                    "smtp": {"username": "profile-smtp@example.org", "password": "profile-smtp-secret"},
                },
            },
        )
        self.assertEqual(profile.status_code, 201, profile.text)
        profile_id = profile.json()["id"]

        policy = self.client.put(
            "/api/v1/mail/policies/tenant",
            headers=headers,
            json={"policy": {
                "smtp_credentials": {"inherit": True},
                "allow_lower_level_limits": {"smtp_credentials.inherit": False},
            }},
        )
        self.assertEqual(policy.status_code, 200, policy.text)
        self.assertEqual(policy.json()["effective_policy"]["smtp_credentials"]["inherit"], True)
        self.assertEqual(policy.json()["effective_policy"]["allow_lower_level_limits"]["smtp_credentials.inherit"], False)

        campaign_json = {
            "version": "1.0",
            "campaign": {"id": "locked-profile-creds", "name": "Locked profile creds", "mode": "test"},
            "fields": [{"name": "first_name", "type": "string", "required": True}],
            "global_values": {},
            "server": {
                "mail_profile_id": profile_id,
                "inherit_smtp_credentials": False,
                "credentials": {
                    "smtp": {"username": "campaign-smtp@example.org", "password": "campaign-smtp-secret"},
                },
            },
            "recipients": {
                "from": {"email": "sender@example.org", "name": "Sender", "type": "to"},
                "to": [{"email": "recipient@example.org", "type": "to"}],
            },
            "template": {"subject": "Hello ${local::first_name}", "text": "Hello ${local::first_name}."},
            "attachments": {"base_path": ".", "global": [], "allow_individual": False},
            "entries": {"inline": [{"id": "recipient-1", "fields": {"first_name": "Recipient"}}]},
            "validation_policy": {"missing_email": "block", "template_error": "block"},
            "delivery": {"rate_limit": {"messages_per_minute": 60}, "retry": {"max_attempts": 3, "backoff_seconds": [1]}, "imap_append_sent": {"enabled": False}},
            "status_tracking": {"enabled": True},
        }
        blocked = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(blocked.status_code, 422, blocked.text)
        self.assertIn("may only reference", blocked.json()["detail"])

        campaign_json["server"] = {"mail_profile_id": profile_id}
        allowed = self.client.post("/api/v1/campaigns", headers=headers, json={"config": campaign_json})
        self.assertEqual(allowed.status_code, 200, allowed.text)

    def test_file_and_raw_mail_permissions_are_independently_enforced(self) -> None:
        owner_headers, _ = self._login()
        role = self._create_role(
            owner_headers,
            slug="uploader-and-mail-tester",
            permissions=["files:read", "files:upload", "mail_servers:test"],
        )
        self._create_user(
            owner_headers,
            email="uploader@example.local",
            password="uploader-password",
            role_ids=[str(role["id"])],
        )
        headers, login = self._login_as(email="uploader@example.local", password="uploader-password")
        user_id = login["user"]["id"]
        uploaded = self.client.post(
            "/api/v1/files/upload",
            headers=headers,
            data={"owner_type": "user", "owner_id": user_id, "path": ""},
            files=[("files", ("allowed.txt", b"allowed", "text/plain"))],
        )
        self.assertEqual(uploaded.status_code, 200, uploaded.text)
        file_id = uploaded.json()["files"][0]["id"]
        self.assertEqual(self.client.get(f"/api/v1/files/{file_id}/download", headers=headers).status_code, 403)
        self.assertEqual(self.client.delete(f"/api/v1/files/{file_id}", headers=headers).status_code, 403)

        raw_test = self.client.post(
            "/api/v1/mail/test-smtp",
            headers=headers,
            json={"host": "mock.smtp", "port": 2525, "username": "u", "password": "p", "security": "starttls"},
        )
        self.assertEqual(raw_test.status_code, 403, raw_test.text)
        self.assertIn("manage_credentials", raw_test.json()["detail"])

    def test_profile_refresh_and_system_role_protection_model(self) -> None:
        headers, _ = self._login()
        profile = self.client.patch(
            "/api/v1/auth/profile",
            headers=headers,
            json={
                "display_name": "Global Account Name",
                "tenant_display_name": "Tenant Alias",
                "ui_preferences": {
                    "compact_tables": True,
                    "show_inline_help_hints": False,
                    "reduce_motion": True,
                    "sticky_section_sidebars": False,
                    "theme": "dark",
                },
            },
        )
        self.assertEqual(profile.status_code, 200, profile.text)
        self.assertEqual(profile.json()["user"]["display_name"], "Global Account Name")
        self.assertEqual(profile.json()["user"]["tenant_display_name"], "Tenant Alias")
        self.assertEqual(
            profile.json()["user"]["ui_preferences"],
            {
                "compact_tables": True,
                "show_inline_help_hints": False,
                "reduce_motion": True,
                "sticky_section_sidebars": False,
                "theme": "dark",
            },
        )
        refreshed = self.client.get("/api/v1/auth/me", headers=headers)
        self.assertEqual(refreshed.status_code, 200, refreshed.text)
        self.assertEqual(refreshed.json()["user"]["display_name"], "Global Account Name")
        self.assertEqual(refreshed.json()["user"]["tenant_display_name"], "Tenant Alias")
        self.assertEqual(refreshed.json()["user"]["ui_preferences"]["theme"], "dark")
        self.assertFalse(refreshed.json()["user"]["ui_preferences"]["show_inline_help_hints"])

        roles = self.client.get("/api/v1/admin/system/roles", headers=headers)
        self.assertEqual(roles.status_code, 200, roles.text)
        owner = next(item for item in roles.json()["roles"] if item["slug"] == "system_owner")
        administrator = next(item for item in roles.json()["roles"] if item["slug"] == "system_admin")
        auditor = next(item for item in roles.json()["roles"] if item["slug"] == "system_auditor")
        self.assertTrue(owner["is_builtin"])
        self.assertEqual(owner["user_assignments"], 1)
        expected_system_permission_count = sum(
            1
            for permission in access_permission_catalog(include_legacy=False)
            if permission.level == "system"
        )
        self.assertEqual(owner["effective_permission_count"], expected_system_permission_count)
        self.assertEqual(owner["permissions"], ["system:*"])
        self.assertFalse(administrator["is_builtin"])
        self.assertFalse(auditor["is_builtin"])

        blocked_owner_edit = self.client.patch(
            f"/api/v1/admin/system/roles/{owner['id']}",
            headers=headers,
            json={"name": "Changed owner"},
        )
        self.assertEqual(blocked_owner_edit.status_code, 409, blocked_owner_edit.text)
        edited_admin = self.client.patch(
            f"/api/v1/admin/system/roles/{administrator['id']}",
            headers=headers,
            json={"name": "Platform administrator"},
        )
        self.assertEqual(edited_admin.status_code, 200, edited_admin.text)
        self.assertEqual(edited_admin.json()["name"], "Platform administrator")



    def test_admin_audit_supports_lazy_pagination_sorting_and_filters(self) -> None:
        headers, _ = self._login()
        for index in range(5):
            updated = self.client.patch(
                "/api/v1/admin/system/settings",
                headers=headers,
                json={
                    "default_locale": f"en-{index}",
                    "allow_tenant_custom_groups": True,
                    "allow_tenant_custom_roles": True,
                    "allow_tenant_api_keys": True,
                },
            )
            self.assertEqual(updated.status_code, 200, updated.text)

        first_page = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={
                "scope": "system",
                "page": 1,
                "page_size": 2,
                "sort_by": "action",
                "sort_direction": "asc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(first_page.status_code, 200, first_page.text)
        payload = first_page.json()
        self.assertEqual(payload["page"], 1)
        self.assertEqual(payload["page_size"], 2)
        self.assertGreaterEqual(payload["total"], 5)
        self.assertEqual(len(payload["items"]), 2)
        self.assertTrue(all("system_settings" in item["action"] for item in payload["items"]))

        second_page = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={
                "scope": "system",
                "page": 2,
                "page_size": 2,
                "filter_actor": "admin@example.local",
            },
        )
        self.assertEqual(second_page.status_code, 200, second_page.text)
        self.assertEqual(second_page.json()["page"], 2)
        self.assertEqual(len(second_page.json()["items"]), 2)


    def test_admin_audit_cursor_pagination_is_stable_after_newer_insert(self) -> None:
        headers, _ = self._login()
        for index in range(6):
            updated = self.client.patch(
                "/api/v1/admin/system/settings",
                headers=headers,
                json={
                    "default_locale": f"en-{index}",
                    "allow_tenant_custom_groups": True,
                    "allow_tenant_custom_roles": True,
                    "allow_tenant_api_keys": True,
                },
            )
            self.assertEqual(updated.status_code, 200, updated.text)

        first_page = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={
                "scope": "system",
                "page": 1,
                "page_size": 2,
                "sort_by": "time",
                "sort_direction": "desc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(first_page.status_code, 200, first_page.text)
        first_payload = first_page.json()
        self.assertTrue(first_payload["next_cursor"])

        second_page = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={
                "scope": "system",
                "page": 2,
                "page_size": 2,
                "cursor": first_payload["next_cursor"],
                "sort_by": "time",
                "sort_direction": "desc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(second_page.status_code, 200, second_page.text)
        second_payload = second_page.json()
        second_page_ids = [item["id"] for item in second_payload["items"]]
        self.assertEqual(second_payload["cursor"], first_payload["next_cursor"])
        self.assertEqual(len(second_page_ids), 2)

        second_page_full_delta = self.client.get(
            "/api/v1/admin/audit/delta",
            headers=headers,
            params={
                "scope": "system",
                "page_size": 2,
                "cursor": first_payload["next_cursor"],
                "sort_by": "time",
                "sort_direction": "desc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(second_page_full_delta.status_code, 200, second_page_full_delta.text)
        full_delta_payload = second_page_full_delta.json()
        self.assertTrue(full_delta_payload["full"])
        self.assertEqual([item["id"] for item in full_delta_payload["items"]], second_page_ids)

        time.sleep(0.01)
        updated = self.client.patch(
            "/api/v1/admin/system/settings",
            headers=headers,
            json={
                "default_locale": "en-cursor",
                "allow_tenant_custom_groups": True,
                "allow_tenant_custom_roles": True,
                "allow_tenant_api_keys": True,
            },
        )
        self.assertEqual(updated.status_code, 200, updated.text)

        second_page_after_insert = self.client.get(
            "/api/v1/admin/audit",
            headers=headers,
            params={
                "scope": "system",
                "page": 2,
                "page_size": 2,
                "cursor": first_payload["next_cursor"],
                "sort_by": "time",
                "sort_direction": "desc",
                "filter_action": "system_settings",
            },
        )
        self.assertEqual(second_page_after_insert.status_code, 200, second_page_after_insert.text)
        self.assertEqual([item["id"] for item in second_page_after_insert.json()["items"]], second_page_ids)

        cursor_delta = self.client.get(
            "/api/v1/admin/audit/delta",
            headers=headers,
            params={
                "scope": "system",
                "page_size": 2,
                "cursor": first_payload["next_cursor"],
                "sort_by": "time",
                "sort_direction": "desc",
                "filter_action": "system_settings",
                "since": full_delta_payload["watermark"],
            },
        )
        self.assertEqual(cursor_delta.status_code, 200, cursor_delta.text)
        cursor_delta_payload = cursor_delta.json()
        self.assertFalse(cursor_delta_payload["full"])
        self.assertEqual(cursor_delta_payload["items"], [])


    def test_tenant_owner_selection_audit_scope_and_last_owner_protection(self) -> None:
        headers, _ = self._login()
        created_account = self.client.post(
            "/api/v1/admin/system/accounts",
            headers=headers,
            json={
                "email": "selected-owner@example.local",
                "display_name": "Selected Owner",
                "password": "selected-owner-password",
                "password_reset_required": False,
                "is_active": True,
                "role_ids": [],
                "memberships": [],
            },
        )
        self.assertEqual(created_account.status_code, 201, created_account.text)
        account_id = created_account.json()["account"]["account_id"]

        candidates = self.client.get("/api/v1/admin/tenants/owner-candidates", headers=headers)
        self.assertEqual(candidates.status_code, 200, candidates.text)
        self.assertIn(account_id, {item["account_id"] for item in candidates.json()["accounts"]})

        created_tenant = self.client.post(
            "/api/v1/admin/tenants",
            headers=headers,
            json={
                "slug": "selected-owner",
                "name": "Selected owner tenant",
                "owner_account_id": account_id,
                "settings": {},
            },
        )
        self.assertEqual(created_tenant.status_code, 201, created_tenant.text)
        tenant_id = created_tenant.json()["id"]

        system_audit = self.client.get(
            "/api/v1/admin/audit?scope=system&limit=500",
            headers=headers,
        )
        self.assertEqual(system_audit.status_code, 200, system_audit.text)
        tenant_created_rows = [
            item for item in system_audit.json()["items"]
            if item["action"] == "tenant.created" and item["object_id"] == tenant_id
        ]
        self.assertEqual(len(tenant_created_rows), 1)
        self.assertEqual(tenant_created_rows[0]["scope"], "system")

        owner_headers, _ = self._login_as(
            email="selected-owner@example.local",
            password="selected-owner-password",
            tenant_slug="selected-owner",
        )
        users = self.client.get("/api/v1/admin/users", headers=owner_headers)
        self.assertEqual(users.status_code, 200, users.text)
        owner = next(item for item in users.json()["users"] if item["account_id"] == account_id)
        self.assertTrue(owner["is_owner"])
        self.assertTrue(owner["is_last_active_owner"])

        tenant_audit = self.client.get(
            "/api/v1/admin/audit?scope=tenant&limit=500",
            headers=owner_headers,
        )
        self.assertEqual(tenant_audit.status_code, 200, tenant_audit.text)
        self.assertFalse(any(item["action"] == "tenant.created" for item in tenant_audit.json()["items"]))
        self.assertTrue(all(item["scope"] == "tenant" for item in tenant_audit.json()["items"]))

        blocked = self.client.patch(
            f"/api/v1/admin/users/{owner['id']}",
            headers=owner_headers,
            json={"is_active": False},
        )
        self.assertEqual(blocked.status_code, 409, blocked.text)

        accounts = self.client.get("/api/v1/admin/system/accounts", headers=headers)
        self.assertEqual(accounts.status_code, 200, accounts.text)
        selected = next(item for item in accounts.json()["accounts"] if item["account_id"] == account_id)
        selected_membership = next(item for item in selected["memberships"] if item["tenant_id"] == tenant_id)
        self.assertTrue(selected_membership["is_owner"])
        self.assertTrue(selected_membership["is_last_active_owner"])



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