"""Tests for VVT Library + Template routes (vvt_library_routes.py, vvt_library_models.py)."""

import pytest
import uuid
from unittest.mock import MagicMock, patch
from datetime import datetime

from compliance.db.vvt_library_models import (
    VVTLibDataSubjectDB,
    VVTLibDataCategoryDB,
    VVTLibRecipientDB,
    VVTLibLegalBasisDB,
    VVTLibRetentionRuleDB,
    VVTLibTransferMechanismDB,
    VVTLibPurposeDB,
    VVTLibTomDB,
    VVTProcessTemplateDB,
)
from compliance.db.vvt_models import VVTActivityDB
from compliance.api.vvt_library_routes import (
    _row_to_dict,
    _resolve_ids,
    _template_to_dict,
)


# =============================================================================
# Model Tests
# =============================================================================

class TestLibraryModels:
    def test_data_subject_model(self):
        ds = VVTLibDataSubjectDB(
            id="EMPLOYEES",
            label_de="Beschaeftigte",
            description_de="Aktuelle Mitarbeiter",
            art9_relevant=False,
            typical_for=["hr"],
            sort_order=1,
        )
        assert ds.id == "EMPLOYEES"
        assert ds.label_de == "Beschaeftigte"
        assert ds.art9_relevant is False

    def test_data_category_hierarchical(self):
        parent = VVTLibDataCategoryDB(
            id="IDENTIFICATION",
            label_de="Identifikationsdaten",
            is_art9=False,
            is_art10=False,
            risk_weight=2,
        )
        child = VVTLibDataCategoryDB(
            id="NAME",
            parent_id="IDENTIFICATION",
            label_de="Name",
            is_art9=False,
            is_art10=False,
            risk_weight=1,
        )
        assert parent.parent_id is None
        assert child.parent_id == "IDENTIFICATION"

    def test_data_category_art9(self):
        cat = VVTLibDataCategoryDB(
            id="HEALTH_DATA",
            parent_id="ART9_SPECIAL",
            label_de="Gesundheitsdaten",
            is_art9=True,
            risk_weight=5,
        )
        assert cat.is_art9 is True
        assert cat.risk_weight == 5

    def test_recipient_model(self):
        rec = VVTLibRecipientDB(
            id="PROCESSOR_PAYROLL",
            type="PROCESSOR",
            label_de="Lohnabrechnungsdienstleister",
            is_third_country=False,
            country="DE",
        )
        assert rec.type == "PROCESSOR"
        assert rec.is_third_country is False

    def test_legal_basis_model(self):
        lb = VVTLibLegalBasisDB(
            id="ART6_1A",
            article="Art. 6 Abs. 1 lit. a",
            type="CONSENT",
            label_de="Einwilligung",
            is_art9=False,
        )
        assert lb.type == "CONSENT"
        assert lb.article == "Art. 6 Abs. 1 lit. a"

    def test_retention_rule_model(self):
        rr = VVTLibRetentionRuleDB(
            id="HGB_257_10Y",
            label_de="10 Jahre (HGB)",
            duration=10,
            duration_unit="YEARS",
            start_event="Ende des Kalenderjahres",
        )
        assert rr.duration == 10
        assert rr.duration_unit == "YEARS"

    def test_transfer_mechanism_model(self):
        tm = VVTLibTransferMechanismDB(
            id="SCC_PROCESSOR",
            label_de="Standardvertragsklauseln",
            article="Art. 46",
            requires_tia=True,
        )
        assert tm.requires_tia is True

    def test_purpose_model(self):
        p = VVTLibPurposeDB(
            id="PAYROLL",
            label_de="Gehaltsabrechnung",
            typical_legal_basis="BDSG_26",
            typical_for=["hr"],
        )
        assert p.typical_legal_basis == "BDSG_26"

    def test_tom_model(self):
        t = VVTLibTomDB(
            id="AC_RBAC",
            category="accessControl",
            label_de="Rollenbasierte Zugriffskontrolle",
            art32_reference="Art. 32 Abs. 1 lit. b",
        )
        assert t.category == "accessControl"

    def test_process_template_model(self):
        tpl = VVTProcessTemplateDB(
            id="hr-mitarbeiterverwaltung",
            name="Mitarbeiterverwaltung",
            business_function="hr",
            purpose_refs=["EMPLOYMENT_ADMIN", "PAYROLL"],
            data_subject_refs=["EMPLOYEES"],
            retention_rule_ref="HGB_257_10Y",
            is_system=True,
        )
        assert tpl.is_system is True
        assert len(tpl.purpose_refs) == 2


# =============================================================================
# Helper Function Tests
# =============================================================================

class TestRowToDict:
    def test_basic_fields(self):
        row = MagicMock()
        row.id = "TEST"
        row.label_de = "Test Label"
        row.description_de = "Test Description"
        row.sort_order = 1

        result = _row_to_dict(row)
        assert result["id"] == "TEST"
        assert result["label_de"] == "Test Label"
        assert result["description_de"] == "Test Description"

    def test_extra_fields(self):
        row = MagicMock()
        row.id = "ART6_1A"
        row.label_de = "Einwilligung"
        row.description_de = None
        row.sort_order = 1
        row.article = "Art. 6"
        row.type = "CONSENT"

        result = _row_to_dict(row, ["article", "type"])
        assert result["article"] == "Art. 6"
        assert result["type"] == "CONSENT"
        assert "description_de" not in result  # None is excluded

    def test_none_extra_fields_excluded(self):
        row = MagicMock()
        row.id = "X"
        row.label_de = "X"
        row.description_de = None
        row.sort_order = 0
        row.country = None

        result = _row_to_dict(row, ["country"])
        assert "country" not in result


class TestTemplateToDict:
    def test_template_to_dict(self):
        tpl = MagicMock()
        tpl.id = "hr-test"
        tpl.name = "HR Test"
        tpl.description = "Test desc"
        tpl.business_function = "hr"
        tpl.purpose_refs = ["PAYROLL"]
        tpl.legal_basis_refs = ["BDSG_26"]
        tpl.data_subject_refs = ["EMPLOYEES"]
        tpl.data_category_refs = ["NAME"]
        tpl.recipient_refs = ["INTERNAL_HR"]
        tpl.tom_refs = ["AC_RBAC"]
        tpl.transfer_mechanism_refs = []
        tpl.retention_rule_ref = "HGB_257_10Y"
        tpl.typical_systems = ["SAP"]
        tpl.protection_level = "HIGH"
        tpl.dpia_required = False
        tpl.risk_score = 3
        tpl.tags = ["hr"]
        tpl.is_system = True
        tpl.sort_order = 1

        result = _template_to_dict(tpl)
        assert result["id"] == "hr-test"
        assert result["name"] == "HR Test"
        assert result["purpose_refs"] == ["PAYROLL"]
        assert result["retention_rule_ref"] == "HGB_257_10Y"

    def test_template_empty_refs(self):
        tpl = MagicMock()
        tpl.id = "empty"
        tpl.name = "Empty"
        tpl.description = None
        tpl.business_function = "other"
        tpl.purpose_refs = None
        tpl.legal_basis_refs = None
        tpl.data_subject_refs = None
        tpl.data_category_refs = None
        tpl.recipient_refs = None
        tpl.tom_refs = None
        tpl.transfer_mechanism_refs = None
        tpl.retention_rule_ref = None
        tpl.typical_systems = None
        tpl.protection_level = None
        tpl.dpia_required = None
        tpl.risk_score = None
        tpl.tags = None
        tpl.is_system = True
        tpl.sort_order = 0

        result = _template_to_dict(tpl)
        assert result["purpose_refs"] == []
        assert result["retention_rule_ref"] is None
        assert result["protection_level"] == "MEDIUM"


class TestResolveIds:
    def test_resolve_ids_basic(self):
        mock_db = MagicMock()
        row1 = MagicMock()
        row1.id = "PAYROLL"
        row1.label_de = "Gehaltsabrechnung"
        row2 = MagicMock()
        row2.id = "CRM"
        row2.label_de = "Kundenbeziehungsmanagement"
        mock_db.query.return_value.filter.return_value.all.return_value = [row1, row2]

        result = _resolve_ids(mock_db, VVTLibPurposeDB, ["PAYROLL", "CRM"])
        assert result == ["Gehaltsabrechnung", "Kundenbeziehungsmanagement"]

    def test_resolve_ids_empty(self):
        mock_db = MagicMock()
        result = _resolve_ids(mock_db, VVTLibPurposeDB, [])
        assert result == []

    def test_resolve_ids_missing_fallback(self):
        mock_db = MagicMock()
        row1 = MagicMock()
        row1.id = "PAYROLL"
        row1.label_de = "Gehaltsabrechnung"
        mock_db.query.return_value.filter.return_value.all.return_value = [row1]

        result = _resolve_ids(mock_db, VVTLibPurposeDB, ["PAYROLL", "UNKNOWN"])
        assert result == ["Gehaltsabrechnung", "UNKNOWN"]


# =============================================================================
# FastAPI Endpoint Tests (via TestClient)
# =============================================================================

class TestLibraryEndpoints:
    """Tests using FastAPI app.dependency_overrides for DB mocking."""

    @pytest.fixture
    def client(self):
        from fastapi.testclient import TestClient
        from fastapi import FastAPI
        from classroom_engine.database import get_db
        from compliance.api.tenant_utils import get_tenant_id
        from compliance.api.vvt_library_routes import router

        app = FastAPI()
        app.include_router(router, prefix="/api/compliance")

        mock_db = MagicMock()
        app.dependency_overrides[get_db] = lambda: mock_db
        app.dependency_overrides[get_tenant_id] = lambda: "test-tenant"

        client = TestClient(app)
        client.mock_db = mock_db
        return client

    # Libraries overview
    def test_libraries_overview(self, client):
        client.mock_db.query.return_value.count.return_value = 5
        res = client.get("/api/compliance/vvt/libraries")
        assert res.status_code == 200
        data = res.json()
        assert "libraries" in data
        assert len(data["libraries"]) == 8

    # Data Subjects
    def test_list_data_subjects(self, client):
        row = MagicMock()
        row.id = "EMPLOYEES"
        row.label_de = "Beschaeftigte"
        row.description_de = "Aktuelle Mitarbeiter"
        row.art9_relevant = False
        row.typical_for = ["hr"]
        row.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/data-subjects")
        assert res.status_code == 200
        data = res.json()
        assert len(data) == 1
        assert data[0]["id"] == "EMPLOYEES"

    def test_list_data_subjects_filter(self, client):
        row1 = MagicMock()
        row1.id = "EMPLOYEES"
        row1.label_de = "Beschaeftigte"
        row1.description_de = None
        row1.art9_relevant = False
        row1.typical_for = ["hr"]
        row1.sort_order = 1

        row2 = MagicMock()
        row2.id = "CUSTOMERS"
        row2.label_de = "Kunden"
        row2.description_de = None
        row2.art9_relevant = False
        row2.typical_for = ["sales_crm"]
        row2.sort_order = 3

        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row1, row2]

        res = client.get("/api/compliance/vvt/libraries/data-subjects?typical_for=hr")
        assert res.status_code == 200
        data = res.json()
        assert len(data) == 1
        assert data[0]["id"] == "EMPLOYEES"

    # Data Categories
    def test_list_data_categories_flat(self, client):
        row = MagicMock()
        row.id = "NAME"
        row.label_de = "Name"
        row.description_de = None
        row.parent_id = "IDENTIFICATION"
        row.is_art9 = False
        row.is_art10 = False
        row.risk_weight = 1
        row.default_retention_rule = None
        row.default_legal_basis = None
        row.sort_order = 10
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/data-categories?flat=true")
        assert res.status_code == 200
        data = res.json()
        assert len(data) == 1
        assert data[0]["parent_id"] == "IDENTIFICATION"

    def test_list_data_categories_tree(self, client):
        parent = MagicMock()
        parent.id = "IDENTIFICATION"
        parent.label_de = "Identifikationsdaten"
        parent.description_de = None
        parent.parent_id = None
        parent.is_art9 = False
        parent.is_art10 = False
        parent.risk_weight = 2
        parent.default_retention_rule = None
        parent.default_legal_basis = None
        parent.sort_order = 1

        child = MagicMock()
        child.id = "NAME"
        child.label_de = "Name"
        child.description_de = None
        child.parent_id = "IDENTIFICATION"
        child.is_art9 = False
        child.is_art10 = False
        child.risk_weight = 1
        child.default_retention_rule = None
        child.default_legal_basis = None
        child.sort_order = 10

        client.mock_db.query.return_value.order_by.return_value.all.return_value = [parent, child]

        res = client.get("/api/compliance/vvt/libraries/data-categories")
        assert res.status_code == 200
        data = res.json()
        assert len(data) == 1  # only parent at top level
        assert data[0]["id"] == "IDENTIFICATION"
        assert len(data[0]["children"]) == 1
        assert data[0]["children"][0]["id"] == "NAME"

    def test_list_data_categories_filter_art9(self, client):
        row = MagicMock()
        row.id = "HEALTH_DATA"
        row.label_de = "Gesundheitsdaten"
        row.description_de = None
        row.parent_id = "ART9_SPECIAL"
        row.is_art9 = True
        row.is_art10 = False
        row.risk_weight = 5
        row.default_retention_rule = None
        row.default_legal_basis = None
        row.sort_order = 80
        client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row]
        # Adjusting for chained filter calls
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/data-categories?is_art9=true&flat=true")
        assert res.status_code == 200

    # Recipients
    def test_list_recipients(self, client):
        row = MagicMock()
        row.id = "INTERNAL_HR"
        row.label_de = "Personalabteilung"
        row.description_de = None
        row.type = "INTERNAL"
        row.is_third_country = False
        row.country = "DE"
        row.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/recipients")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["type"] == "INTERNAL"

    def test_list_recipients_filter_type(self, client):
        row = MagicMock()
        row.id = "PROCESSOR_PAYROLL"
        row.label_de = "Lohnabrechnung"
        row.description_de = None
        row.type = "PROCESSOR"
        row.is_third_country = False
        row.country = "DE"
        row.sort_order = 7
        client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/recipients?type=PROCESSOR")
        assert res.status_code == 200

    # Legal Bases
    def test_list_legal_bases(self, client):
        row = MagicMock()
        row.id = "ART6_1A"
        row.label_de = "Einwilligung"
        row.description_de = None
        row.article = "Art. 6"
        row.type = "CONSENT"
        row.is_art9 = False
        row.typical_national_law = None
        row.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/legal-bases")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["article"] == "Art. 6"

    # Retention Rules
    def test_list_retention_rules(self, client):
        row = MagicMock()
        row.id = "HGB_257_10Y"
        row.label_de = "10 Jahre (HGB)"
        row.description_de = None
        row.legal_basis = "HGB § 257"
        row.duration = 10
        row.duration_unit = "YEARS"
        row.start_event = "Ende des Kalenderjahres"
        row.deletion_procedure = "Vernichtung"
        row.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/retention-rules")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["duration"] == 10
        assert data[0]["duration_unit"] == "YEARS"

    # Transfer Mechanisms
    def test_list_transfer_mechanisms(self, client):
        row = MagicMock()
        row.id = "SCC_PROCESSOR"
        row.label_de = "SCC"
        row.description_de = None
        row.article = "Art. 46"
        row.requires_tia = True
        row.sort_order = 3
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/transfer-mechanisms")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["requires_tia"] is True

    # Purposes
    def test_list_purposes(self, client):
        row = MagicMock()
        row.id = "PAYROLL"
        row.label_de = "Gehaltsabrechnung"
        row.description_de = None
        row.typical_legal_basis = "BDSG_26"
        row.typical_for = ["hr"]
        row.sort_order = 2
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/purposes")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["typical_legal_basis"] == "BDSG_26"

    # TOMs
    def test_list_toms(self, client):
        row = MagicMock()
        row.id = "AC_RBAC"
        row.label_de = "RBAC"
        row.description_de = None
        row.category = "accessControl"
        row.art32_reference = "Art. 32"
        row.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/toms")
        assert res.status_code == 200
        data = res.json()
        assert data[0]["category"] == "accessControl"

    def test_list_toms_filter(self, client):
        row = MagicMock()
        row.id = "CONF_ENCRYPTION_REST"
        row.label_de = "Verschluesselung"
        row.description_de = None
        row.category = "confidentiality"
        row.art32_reference = "Art. 32"
        row.sort_order = 5
        client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = [row]

        res = client.get("/api/compliance/vvt/libraries/toms?category=confidentiality")
        assert res.status_code == 200

    # Templates
    def test_list_templates(self, client):
        tpl = MagicMock()
        tpl.id = "hr-test"
        tpl.name = "HR Test"
        tpl.description = "Test"
        tpl.business_function = "hr"
        tpl.purpose_refs = ["PAYROLL"]
        tpl.legal_basis_refs = ["BDSG_26"]
        tpl.data_subject_refs = ["EMPLOYEES"]
        tpl.data_category_refs = ["NAME"]
        tpl.recipient_refs = []
        tpl.tom_refs = ["AC_RBAC"]
        tpl.transfer_mechanism_refs = []
        tpl.retention_rule_ref = "HGB_257_10Y"
        tpl.typical_systems = ["SAP"]
        tpl.protection_level = "HIGH"
        tpl.dpia_required = False
        tpl.risk_score = 3
        tpl.tags = ["hr"]
        tpl.is_system = True
        tpl.sort_order = 1
        client.mock_db.query.return_value.order_by.return_value.all.return_value = [tpl]

        res = client.get("/api/compliance/vvt/templates")
        assert res.status_code == 200
        data = res.json()
        assert len(data) == 1
        assert data[0]["id"] == "hr-test"

    def test_list_templates_filter_bf(self, client):
        client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = []
        res = client.get("/api/compliance/vvt/templates?business_function=finance")
        assert res.status_code == 200

    def test_list_templates_search(self, client):
        client.mock_db.query.return_value.order_by.return_value.filter.return_value.all.return_value = []
        res = client.get("/api/compliance/vvt/templates?search=gehalts")
        assert res.status_code == 200

    def test_get_template(self, client):
        tpl = MagicMock()
        tpl.id = "hr-test"
        tpl.name = "HR Test"
        tpl.description = None
        tpl.business_function = "hr"
        tpl.purpose_refs = ["PAYROLL"]
        tpl.legal_basis_refs = []
        tpl.data_subject_refs = []
        tpl.data_category_refs = []
        tpl.recipient_refs = []
        tpl.tom_refs = []
        tpl.transfer_mechanism_refs = []
        tpl.retention_rule_ref = None
        tpl.typical_systems = []
        tpl.protection_level = "MEDIUM"
        tpl.dpia_required = False
        tpl.risk_score = None
        tpl.tags = []
        tpl.is_system = True
        tpl.sort_order = 0
        client.mock_db.query.return_value.filter.return_value.first.return_value = tpl
        # For label resolution queries
        client.mock_db.query.return_value.filter.return_value.all.return_value = []

        res = client.get("/api/compliance/vvt/templates/hr-test")
        assert res.status_code == 200
        data = res.json()
        assert data["id"] == "hr-test"

    def test_get_template_not_found(self, client):
        client.mock_db.query.return_value.filter.return_value.first.return_value = None
        res = client.get("/api/compliance/vvt/templates/nonexistent")
        assert res.status_code == 404

    # Template instantiation
    def test_instantiate_template(self, client):
        tpl = MagicMock()
        tpl.id = "hr-test"
        tpl.name = "HR Test"
        tpl.description = "Test"
        tpl.business_function = "hr"
        tpl.purpose_refs = ["PAYROLL"]
        tpl.legal_basis_refs = ["BDSG_26"]
        tpl.data_subject_refs = ["EMPLOYEES"]
        tpl.data_category_refs = ["NAME"]
        tpl.recipient_refs = ["INTERNAL_HR"]
        tpl.tom_refs = ["AC_RBAC"]
        tpl.transfer_mechanism_refs = []
        tpl.retention_rule_ref = "HGB_257_10Y"
        tpl.typical_systems = ["SAP"]
        tpl.protection_level = "HIGH"
        tpl.dpia_required = False
        tpl.risk_score = 3

        purpose_row = MagicMock()
        purpose_row.id = "PAYROLL"
        purpose_row.label_de = "Gehaltsabrechnung"

        legal_row = MagicMock()
        legal_row.id = "BDSG_26"
        legal_row.label_de = "Beschaeftigtenverhaeltnis"

        subject_row = MagicMock()
        subject_row.id = "EMPLOYEES"
        subject_row.label_de = "Beschaeftigte"

        cat_row = MagicMock()
        cat_row.id = "NAME"
        cat_row.label_de = "Name"

        rec_row = MagicMock()
        rec_row.id = "INTERNAL_HR"
        rec_row.label_de = "Personalabteilung"

        rr_row = MagicMock()
        rr_row.id = "HGB_257_10Y"
        rr_row.label_de = "10 Jahre (HGB)"
        rr_row.legal_basis = "HGB § 257"
        rr_row.deletion_procedure = "Vernichtung"
        rr_row.duration = 10
        rr_row.duration_unit = "YEARS"

        tom_row = MagicMock()
        tom_row.id = "AC_RBAC"
        tom_row.label_de = "RBAC"
        tom_row.category = "accessControl"

        def mock_query(model):
            mock_q = MagicMock()
            if model == VVTProcessTemplateDB:
                mock_q.filter.return_value.first.return_value = tpl
            elif model == VVTActivityDB:
                mock_q.filter.return_value.count.return_value = 5
            elif model == VVTLibPurposeDB:
                mock_q.filter.return_value.all.return_value = [purpose_row]
            elif model == VVTLibLegalBasisDB:
                mock_q.filter.return_value.all.return_value = [legal_row]
            elif model == VVTLibDataSubjectDB:
                mock_q.filter.return_value.all.return_value = [subject_row]
            elif model == VVTLibDataCategoryDB:
                mock_q.filter.return_value.all.return_value = [cat_row]
            elif model == VVTLibRecipientDB:
                mock_q.filter.return_value.all.return_value = [rec_row]
            elif model == VVTLibRetentionRuleDB:
                mock_q.filter.return_value.first.return_value = rr_row
            elif model == VVTLibTomDB:
                mock_q.filter.return_value.all.return_value = [tom_row]
            return mock_q

        client.mock_db.query.side_effect = mock_query

        created_id = uuid.uuid4()
        created_at = datetime.utcnow()

        def mock_add(obj):
            if hasattr(obj, 'vvt_id'):
                obj.id = created_id
                obj.created_at = created_at
                obj.updated_at = created_at
        client.mock_db.add.side_effect = mock_add
        client.mock_db.flush.return_value = None
        client.mock_db.commit.return_value = None
        client.mock_db.refresh.return_value = None

        res = client.post(
            "/api/compliance/vvt/templates/hr-test/instantiate",
            headers={"X-Tenant-ID": "test-tenant", "X-User-ID": "test-user"}
        )
        # The response contains the created activity — verify it was created
        assert res.status_code == 201
        data = res.json()
        assert data["name"] == "HR Test"
        assert data["purpose_refs"] == ["PAYROLL"]
        assert data["source_template_id"] == "hr-test"

    def test_instantiate_template_not_found(self, client):
        def mock_query(model):
            mock_q = MagicMock()
            mock_q.filter.return_value.first.return_value = None
            return mock_q
        client.mock_db.query.side_effect = mock_query
        res = client.post(
            "/api/compliance/vvt/templates/nonexistent/instantiate",
            headers={"X-Tenant-ID": "test-tenant"}
        )
        assert res.status_code == 404


# =============================================================================
# Completeness Calculation Tests
# =============================================================================

class TestCompletenessCalculation:
    def test_completeness_full(self):
        from compliance.api.vvt_routes import _calculate_completeness
        act = MagicMock()
        act.name = "Test Activity"
        act.purposes = ["Zweck 1"]
        act.purpose_refs = None
        act.legal_bases = [{"type": "ART6_1B"}]
        act.legal_basis_refs = None
        act.data_subject_categories = ["Mitarbeiter"]
        act.data_subject_refs = None
        act.personal_data_categories = ["Name"]
        act.data_category_refs = None
        act.recipient_categories = [{"type": "INTERNAL", "name": "HR"}]
        act.recipient_refs = None
        act.third_country_transfers = []
        act.transfer_mechanism_refs = None
        act.retention_period = {"description": "10 Jahre"}
        act.retention_rule_ref = None
        act.tom_description = "Verschluesselung"
        act.tom_refs = None
        act.structured_toms = {}
        act.responsible = "Max Mustermann"
        act.dpia_required = False
        act.dsfa_id = None

        result = _calculate_completeness(act)
        assert result["score"] == 100
        assert result["missing"] == []

    def test_completeness_empty(self):
        from compliance.api.vvt_routes import _calculate_completeness
        act = MagicMock()
        act.name = ""
        act.purposes = []
        act.purpose_refs = None
        act.legal_bases = []
        act.legal_basis_refs = None
        act.data_subject_categories = []
        act.data_subject_refs = None
        act.personal_data_categories = []
        act.data_category_refs = None
        act.recipient_categories = []
        act.recipient_refs = None
        act.third_country_transfers = []
        act.transfer_mechanism_refs = None
        act.retention_period = {}
        act.retention_rule_ref = None
        act.tom_description = ""
        act.tom_refs = None
        act.structured_toms = {}
        act.responsible = ""
        act.dpia_required = False
        act.dsfa_id = None

        result = _calculate_completeness(act)
        assert result["score"] < 50
        assert "name" in result["missing"]
        assert "purposes" in result["missing"]

    def test_completeness_with_refs_only(self):
        from compliance.api.vvt_routes import _calculate_completeness
        act = MagicMock()
        act.name = "Test"
        act.purposes = []
        act.purpose_refs = ["PAYROLL"]  # refs fill the gap
        act.legal_bases = []
        act.legal_basis_refs = ["BDSG_26"]
        act.data_subject_categories = []
        act.data_subject_refs = ["EMPLOYEES"]
        act.personal_data_categories = []
        act.data_category_refs = ["NAME"]
        act.recipient_categories = []
        act.recipient_refs = ["INTERNAL_HR"]
        act.third_country_transfers = []
        act.transfer_mechanism_refs = None
        act.retention_period = {}
        act.retention_rule_ref = "HGB_257_10Y"
        act.tom_description = ""
        act.tom_refs = ["AC_RBAC"]
        act.structured_toms = {}
        act.responsible = "Test Person"
        act.dpia_required = False
        act.dsfa_id = None

        result = _calculate_completeness(act)
        assert result["score"] == 100
        assert result["missing"] == []

    def test_completeness_dpia_warning(self):
        from compliance.api.vvt_routes import _calculate_completeness
        act = MagicMock()
        act.name = "Test"
        act.purposes = ["Zweck"]
        act.purpose_refs = None
        act.legal_bases = [{"type": "ART6_1B"}]
        act.legal_basis_refs = None
        act.data_subject_categories = ["Mitarbeiter"]
        act.data_subject_refs = None
        act.personal_data_categories = ["Name"]
        act.data_category_refs = None
        act.recipient_categories = [{"type": "INTERNAL", "name": "HR"}]
        act.recipient_refs = None
        act.third_country_transfers = [{"country": "US"}]
        act.transfer_mechanism_refs = None
        act.retention_period = {"description": "10 Jahre"}
        act.retention_rule_ref = None
        act.tom_description = "TOM"
        act.tom_refs = None
        act.structured_toms = {}
        act.responsible = "Max"
        act.dpia_required = True
        act.dsfa_id = None

        result = _calculate_completeness(act)
        assert "dpia_required_but_no_dsfa_linked" in result["warnings"]
        assert "third_country_transfer_without_mechanism" in result["warnings"]

    def test_completeness_partial(self):
        from compliance.api.vvt_routes import _calculate_completeness
        act = MagicMock()
        act.name = "Test"
        act.purposes = ["Zweck"]
        act.purpose_refs = None
        act.legal_bases = []
        act.legal_basis_refs = None
        act.data_subject_categories = ["Mitarbeiter"]
        act.data_subject_refs = None
        act.personal_data_categories = []
        act.data_category_refs = None
        act.recipient_categories = []
        act.recipient_refs = None
        act.third_country_transfers = []
        act.transfer_mechanism_refs = None
        act.retention_period = {}
        act.retention_rule_ref = None
        act.tom_description = ""
        act.tom_refs = None
        act.structured_toms = {}
        act.responsible = ""
        act.dpia_required = False
        act.dsfa_id = None

        result = _calculate_completeness(act)
        assert 30 <= result["score"] <= 50
        assert "legal_bases" in result["missing"]
        assert "data_categories" in result["missing"]
        assert "recipients" in result["missing"]


# =============================================================================
# VVT Activity Schema Tests — new ref fields
# =============================================================================

class TestVVTActivityCreateWithRefs:
    def test_create_with_refs(self):
        from compliance.api.schemas import VVTActivityCreate
        req = VVTActivityCreate(
            vvt_id="VVT-010",
            name="Test mit Refs",
            purpose_refs=["PAYROLL", "CRM"],
            legal_basis_refs=["BDSG_26"],
            data_subject_refs=["EMPLOYEES"],
            data_category_refs=["NAME", "SALARY_DATA"],
            recipient_refs=["INTERNAL_HR"],
            retention_rule_ref="HGB_257_10Y",
            tom_refs=["AC_RBAC", "CONF_ENCRYPTION_REST"],
            source_template_id="hr-mitarbeiterverwaltung",
            risk_score=3,
        )
        assert req.purpose_refs == ["PAYROLL", "CRM"]
        assert req.retention_rule_ref == "HGB_257_10Y"
        assert req.source_template_id == "hr-mitarbeiterverwaltung"
        assert req.risk_score == 3

    def test_create_without_refs_backward_compat(self):
        from compliance.api.schemas import VVTActivityCreate
        req = VVTActivityCreate(
            vvt_id="VVT-011",
            name="Test ohne Refs",
            purposes=["Vertragserfuellung"],
            legal_bases=["Art. 6 Abs. 1b"],
        )
        assert req.purpose_refs is None
        assert req.retention_rule_ref is None
        assert req.source_template_id is None

    def test_serialization_with_refs(self):
        from compliance.api.schemas import VVTActivityCreate
        req = VVTActivityCreate(
            vvt_id="VVT-012",
            name="Test",
            purpose_refs=["PAYROLL"],
            linked_loeschfristen_ids=["uuid-1"],
        )
        data = req.model_dump()
        assert data["purpose_refs"] == ["PAYROLL"]
        assert data["linked_loeschfristen_ids"] == ["uuid-1"]


class TestVVTActivityUpdateWithRefs:
    def test_partial_update_refs(self):
        from compliance.api.schemas import VVTActivityUpdate
        req = VVTActivityUpdate(
            purpose_refs=["RECRUITING"],
            retention_rule_ref="AGG_15_6M",
        )
        data = req.model_dump(exclude_none=True)
        assert data["purpose_refs"] == ["RECRUITING"]
        assert data["retention_rule_ref"] == "AGG_15_6M"
        assert "name" not in data

    def test_update_without_refs_backward_compat(self):
        from compliance.api.schemas import VVTActivityUpdate
        req = VVTActivityUpdate(status="APPROVED")
        data = req.model_dump(exclude_none=True)
        assert "purpose_refs" not in data
        assert "status" in data


class TestVVTActivityResponseWithRefs:
    def test_response_with_refs(self):
        from compliance.api.schemas import VVTActivityResponse
        resp = VVTActivityResponse(
            id="test-id",
            vvt_id="VVT-010",
            name="Test",
            purpose_refs=["PAYROLL"],
            legal_basis_refs=["BDSG_26"],
            source_template_id="hr-test",
            risk_score=3,
            art30_completeness={"score": 85, "missing": ["recipients"]},
            created_at=datetime.utcnow(),
        )
        assert resp.purpose_refs == ["PAYROLL"]
        assert resp.art30_completeness["score"] == 85

    def test_response_without_refs_backward_compat(self):
        from compliance.api.schemas import VVTActivityResponse
        resp = VVTActivityResponse(
            id="test-id",
            vvt_id="VVT-011",
            name="Test",
            created_at=datetime.utcnow(),
        )
        assert resp.purpose_refs is None
        assert resp.retention_rule_ref is None
        assert resp.art30_completeness is None


# =============================================================================
# VVT Activity DB Model — new columns
# =============================================================================

class TestVVTActivityDBNewColumns:
    def test_new_columns_exist(self):
        act = VVTActivityDB(
            tenant_id="test",
            vvt_id="VVT-001",
            name="Test",
            purpose_refs=["PAYROLL"],
            legal_basis_refs=["BDSG_26"],
            data_subject_refs=["EMPLOYEES"],
            data_category_refs=["NAME"],
            recipient_refs=["INTERNAL_HR"],
            retention_rule_ref="HGB_257_10Y",
            transfer_mechanism_refs=None,
            tom_refs=["AC_RBAC"],
            linked_loeschfristen_ids=["uuid-1"],
            linked_tom_measure_ids=None,
            source_template_id="hr-test",
            risk_score=3,
            art30_completeness={"score": 80, "missing": ["recipients"]},
        )
        assert act.purpose_refs == ["PAYROLL"]
        assert act.retention_rule_ref == "HGB_257_10Y"
        assert act.source_template_id == "hr-test"

    def test_new_columns_null_defaults(self):
        act = VVTActivityDB(
            tenant_id="test",
            vvt_id="VVT-002",
            name="Test ohne Refs",
        )
        assert act.purpose_refs is None
        assert act.retention_rule_ref is None
        assert act.source_template_id is None
        assert act.art30_completeness is None


# =============================================================================
# _activity_to_response with refs
# =============================================================================

class TestActivityToResponseWithRefs:
    def test_includes_ref_fields(self):
        from compliance.api.vvt_routes import _activity_to_response
        act = MagicMock()
        act.id = uuid.uuid4()
        act.vvt_id = "VVT-001"
        act.name = "Test"
        act.description = "Desc"
        act.purposes = ["Zweck"]
        act.legal_bases = []
        act.data_subject_categories = []
        act.personal_data_categories = []
        act.recipient_categories = []
        act.third_country_transfers = []
        act.retention_period = {}
        act.tom_description = None
        act.business_function = "hr"
        act.systems = []
        act.deployment_model = "cloud"
        act.data_sources = []
        act.data_flows = []
        act.protection_level = "HIGH"
        act.dpia_required = False
        act.structured_toms = {}
        act.status = "DRAFT"
        act.responsible = "Max"
        act.owner = "Max"
        act.last_reviewed_at = None
        act.next_review_at = None
        act.created_by = "system"
        act.dsfa_id = None
        act.purpose_refs = ["PAYROLL"]
        act.legal_basis_refs = ["BDSG_26"]
        act.data_subject_refs = ["EMPLOYEES"]
        act.data_category_refs = ["NAME"]
        act.recipient_refs = ["INTERNAL_HR"]
        act.retention_rule_ref = "HGB_257_10Y"
        act.transfer_mechanism_refs = None
        act.tom_refs = ["AC_RBAC"]
        act.source_template_id = "hr-test"
        act.risk_score = 3
        act.linked_loeschfristen_ids = None
        act.linked_tom_measure_ids = None
        act.art30_completeness = {"score": 90, "missing": ["responsible"]}
        act.created_at = datetime.utcnow()
        act.updated_at = None

        resp = _activity_to_response(act)
        assert resp.purpose_refs == ["PAYROLL"]
        assert resp.retention_rule_ref == "HGB_257_10Y"
        assert resp.source_template_id == "hr-test"
        assert resp.art30_completeness["score"] == 90
