"""
Tests for Einwilligungen Routes — 008_einwilligungen migration.

Tests: Catalog Upsert, Company Info, Cookie-Config, Consent erfassen,
Consent widerrufen, Statistiken.
"""

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


# ============================================================================
# Shared Fixtures
# ============================================================================

def make_uuid():
    return str(uuid.uuid4())


def make_catalog(tenant_id='test-tenant'):
    rec = MagicMock()
    rec.id = uuid.uuid4()
    rec.tenant_id = tenant_id
    rec.selected_data_point_ids = ['dp-001', 'dp-002']
    rec.custom_data_points = []
    rec.updated_at = datetime.now(timezone.utc)
    return rec


def make_company(tenant_id='test-tenant'):
    rec = MagicMock()
    rec.id = uuid.uuid4()
    rec.tenant_id = tenant_id
    rec.data = {'company_name': 'Test GmbH', 'email': 'datenschutz@test.de'}
    rec.updated_at = datetime.now(timezone.utc)
    return rec


def make_cookies(tenant_id='test-tenant'):
    rec = MagicMock()
    rec.id = uuid.uuid4()
    rec.tenant_id = tenant_id
    rec.categories = [
        {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True},
        {'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': False},
    ]
    rec.config = {'position': 'bottom', 'style': 'bar'}
    rec.updated_at = datetime.now(timezone.utc)
    return rec


def make_consent(tenant_id='test-tenant', user_id='user-001', data_point_id='dp-001', granted=True):
    rec = MagicMock()
    rec.id = uuid.uuid4()
    rec.tenant_id = tenant_id
    rec.user_id = user_id
    rec.data_point_id = data_point_id
    rec.granted = granted
    rec.granted_at = datetime.now(timezone.utc)
    rec.revoked_at = None
    rec.consent_version = '1.0'
    rec.source = 'website'
    rec.ip_address = None
    rec.user_agent = None
    rec.created_at = datetime.now(timezone.utc)
    return rec


# ============================================================================
# Pydantic Schema Tests
# ============================================================================

class TestCatalogUpsert:
    def test_catalog_upsert_defaults(self):
        from compliance.api.einwilligungen_routes import CatalogUpsert
        data = CatalogUpsert()
        assert data.selected_data_point_ids == []
        assert data.custom_data_points == []

    def test_catalog_upsert_with_data(self):
        from compliance.api.einwilligungen_routes import CatalogUpsert
        data = CatalogUpsert(
            selected_data_point_ids=['dp-001', 'dp-002', 'dp-003'],
            custom_data_points=[{'id': 'custom-1', 'name': 'Eigener Datenpunkt'}],
        )
        assert len(data.selected_data_point_ids) == 3
        assert len(data.custom_data_points) == 1


class TestCompanyUpsert:
    def test_company_upsert_empty(self):
        from compliance.api.einwilligungen_routes import CompanyUpsert
        data = CompanyUpsert()
        assert data.data == {}

    def test_company_upsert_with_data(self):
        from compliance.api.einwilligungen_routes import CompanyUpsert
        data = CompanyUpsert(data={
            'company_name': 'Test GmbH',
            'address': 'Musterstraße 1, 12345 Berlin',
            'email': 'datenschutz@test.de',
            'dpo_name': 'Max Mustermann',
        })
        assert data.data['company_name'] == 'Test GmbH'
        assert data.data['dpo_name'] == 'Max Mustermann'


class TestCookiesUpsert:
    def test_cookies_upsert_defaults(self):
        from compliance.api.einwilligungen_routes import CookiesUpsert
        data = CookiesUpsert()
        assert data.categories == []
        assert data.config == {}

    def test_cookies_upsert_with_categories(self):
        from compliance.api.einwilligungen_routes import CookiesUpsert
        data = CookiesUpsert(
            categories=[
                {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True},
                {'id': 'analytics', 'name': 'Analyse', 'isRequired': False},
            ],
            config={'position': 'bottom'},
        )
        assert len(data.categories) == 2
        assert data.config['position'] == 'bottom'


class TestConsentCreate:
    def test_consent_create_valid(self):
        from compliance.api.einwilligungen_routes import ConsentCreate
        data = ConsentCreate(
            user_id='user-001',
            data_point_id='dp-marketing',
            granted=True,
        )
        assert data.user_id == 'user-001'
        assert data.granted is True
        assert data.consent_version == '1.0'
        assert data.source is None

    def test_consent_create_revoke(self):
        from compliance.api.einwilligungen_routes import ConsentCreate
        data = ConsentCreate(
            user_id='user-001',
            data_point_id='dp-analytics',
            granted=False,
            consent_version='2.0',
            source='cookie-banner',
        )
        assert data.granted is False
        assert data.consent_version == '2.0'


# ============================================================================
# Catalog Upsert Tests
# ============================================================================

class TestCatalogDB:
    def test_catalog_returns_empty_when_not_found(self):
        """GET catalog should return empty defaults when no record exists."""
        from compliance.db.einwilligungen_models import EinwilligungenCatalogDB

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        result = mock_db.query(EinwilligungenCatalogDB).filter().first()
        assert result is None

    def test_catalog_upsert_creates_new(self):
        """PUT catalog should create a new record if none exists."""
        from compliance.db.einwilligungen_models import EinwilligungenCatalogDB

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        new_record = EinwilligungenCatalogDB(
            tenant_id='test-tenant',
            selected_data_point_ids=['dp-001'],
            custom_data_points=[],
        )
        assert new_record.tenant_id == 'test-tenant'
        assert new_record.selected_data_point_ids == ['dp-001']

    def test_catalog_upsert_updates_existing(self):
        """PUT catalog should update existing record."""
        existing = make_catalog()

        existing.selected_data_point_ids = ['dp-001', 'dp-002', 'dp-003']
        existing.custom_data_points = [{'id': 'custom-1'}]

        assert len(existing.selected_data_point_ids) == 3
        assert len(existing.custom_data_points) == 1


# ============================================================================
# Cookie Config Tests
# ============================================================================

class TestCookieConfig:
    def test_cookie_config_returns_empty_when_not_found(self):
        """GET cookies should return empty defaults for new tenant."""
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        result = mock_db.query().filter().first()
        assert result is None

    def test_cookie_config_upsert_with_categories(self):
        """PUT cookies should store categories and config."""
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB

        categories = [
            {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True},
        ]
        config = {'position': 'bottom', 'primaryColor': '#6366f1'}

        rec = EinwilligungenCookiesDB(
            tenant_id='test-tenant',
            categories=categories,
            config=config,
        )
        assert rec.categories[0]['id'] == 'necessary'
        assert rec.config['position'] == 'bottom'

    def test_essential_cookies_cannot_be_disabled(self):
        """Category with isRequired=True should not allow enabled=False."""
        categories = [
            {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True},
            {'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': True},
        ]

        # Simulate the toggle logic
        category_id = 'necessary'
        enabled = False

        updated = []
        for cat in categories:
            if cat['id'] == category_id:
                if cat.get('isRequired') and not enabled:
                    updated.append(cat)  # Not changed
                else:
                    updated.append({**cat, 'defaultEnabled': enabled})
            else:
                updated.append(cat)

        necessary_cat = next(c for c in updated if c['id'] == 'necessary')
        assert necessary_cat['defaultEnabled'] is True  # Not changed


# ============================================================================
# Consent Tests
# ============================================================================

class TestConsentDB:
    def test_consent_record_creation(self):
        """Consent record should store all required fields."""
        from compliance.db.einwilligungen_models import EinwilligungenConsentDB

        consent = EinwilligungenConsentDB(
            tenant_id='test-tenant',
            user_id='user-001',
            data_point_id='dp-marketing',
            granted=True,
            granted_at=datetime.now(timezone.utc),
            consent_version='1.0',
            source='website',
        )
        assert consent.tenant_id == 'test-tenant'
        assert consent.granted is True
        assert consent.revoked_at is None

    def test_consent_revoke_sets_revoked_at(self):
        """Revoking a consent should set revoked_at timestamp."""
        consent = make_consent()
        assert consent.revoked_at is None

        consent.revoked_at = datetime.now(timezone.utc)
        assert consent.revoked_at is not None

    def test_cannot_revoke_already_revoked(self):
        """Should not be possible to revoke an already revoked consent."""
        consent = make_consent()
        consent.revoked_at = datetime.now(timezone.utc)

        # Simulate the guard logic from the route
        already_revoked = consent.revoked_at is not None
        assert already_revoked is True  # Route would raise HTTPException 400


# ============================================================================
# Statistics Tests
# ============================================================================

class TestConsentStats:
    def test_stats_empty_tenant(self):
        """Stats for tenant with no consents should return zeros."""
        consents = []
        total = len(consents)
        active = sum(1 for c in consents if c.granted and not c.revoked_at)
        revoked = sum(1 for c in consents if c.revoked_at)
        unique_users = len(set(c.user_id for c in consents))

        assert total == 0
        assert active == 0
        assert revoked == 0
        assert unique_users == 0

    def test_stats_with_mixed_consents(self):
        """Stats should correctly count active and revoked consents."""
        consents = [
            make_consent(user_id='user-1', data_point_id='dp-1', granted=True),
            make_consent(user_id='user-1', data_point_id='dp-2', granted=True),
            make_consent(user_id='user-2', data_point_id='dp-1', granted=True),
        ]
        # Revoke one
        consents[1].revoked_at = datetime.now(timezone.utc)

        total = len(consents)
        active = sum(1 for c in consents if c.granted and not c.revoked_at)
        revoked = sum(1 for c in consents if c.revoked_at)
        unique_users = len(set(c.user_id for c in consents))

        assert total == 3
        assert active == 2
        assert revoked == 1
        assert unique_users == 2

    def test_stats_conversion_rate(self):
        """Conversion rate = users with active consent / total unique users."""
        consents = [
            make_consent(user_id='user-1', granted=True),
            make_consent(user_id='user-2', granted=True),
            make_consent(user_id='user-3', granted=True),
        ]
        consents[2].revoked_at = datetime.now(timezone.utc)  # user-3 revoked

        unique_users = len(set(c.user_id for c in consents))
        users_with_active = len(set(c.user_id for c in consents if c.granted and not c.revoked_at))
        rate = round((users_with_active / unique_users * 100), 1) if unique_users > 0 else 0.0

        assert unique_users == 3
        assert users_with_active == 2
        assert rate == pytest.approx(66.7, 0.1)

    def test_stats_by_data_point(self):
        """Stats should group consents by data_point_id."""
        consents = [
            make_consent(data_point_id='dp-marketing', granted=True),
            make_consent(data_point_id='dp-marketing', granted=True),
            make_consent(data_point_id='dp-analytics', granted=True),
        ]

        by_dp: dict = {}
        for c in consents:
            dp = c.data_point_id
            if dp not in by_dp:
                by_dp[dp] = {'total': 0, 'active': 0}
            by_dp[dp]['total'] += 1
            if c.granted and not c.revoked_at:
                by_dp[dp]['active'] += 1

        assert by_dp['dp-marketing']['total'] == 2
        assert by_dp['dp-analytics']['total'] == 1


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

class TestModelReprs:
    def test_catalog_repr(self):
        from compliance.db.einwilligungen_models import EinwilligungenCatalogDB
        rec = EinwilligungenCatalogDB(tenant_id='my-tenant')
        assert 'my-tenant' in repr(rec)

    def test_cookies_repr(self):
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB
        rec = EinwilligungenCookiesDB(tenant_id='my-tenant')
        assert 'my-tenant' in repr(rec)

    def test_consent_repr(self):
        from compliance.db.einwilligungen_models import EinwilligungenConsentDB
        rec = EinwilligungenConsentDB(
            tenant_id='t1', user_id='u1', data_point_id='dp1', granted=True
        )
        assert 'u1' in repr(rec)
        assert 'dp1' in repr(rec)


# ============================================================================
# Consent Response Field Tests (Regression: Phase 3 — ip_address + user_agent)
# ============================================================================

class TestConsentResponseFields:
    def test_consent_response_includes_ip_address(self):
        """GET /consents Serialisierung muss ip_address enthalten."""
        c = make_consent()
        c.ip_address = '10.0.0.1'

        row = {
            "id": str(c.id),
            "tenant_id": c.tenant_id,
            "user_id": c.user_id,
            "data_point_id": c.data_point_id,
            "granted": c.granted,
            "granted_at": c.granted_at,
            "revoked_at": c.revoked_at,
            "consent_version": c.consent_version,
            "source": c.source,
            "ip_address": c.ip_address,
            "user_agent": c.user_agent,
            "created_at": c.created_at,
        }

        assert "ip_address" in row
        assert row["ip_address"] == '10.0.0.1'

    def test_consent_response_includes_user_agent(self):
        """GET /consents Serialisierung muss user_agent enthalten."""
        c = make_consent()
        c.user_agent = 'Mozilla/5.0 (Test)'

        row = {
            "id": str(c.id),
            "tenant_id": c.tenant_id,
            "user_id": c.user_id,
            "data_point_id": c.data_point_id,
            "granted": c.granted,
            "granted_at": c.granted_at,
            "revoked_at": c.revoked_at,
            "consent_version": c.consent_version,
            "source": c.source,
            "ip_address": c.ip_address,
            "user_agent": c.user_agent,
            "created_at": c.created_at,
        }

        assert "user_agent" in row
        assert row["user_agent"] == 'Mozilla/5.0 (Test)'

    def test_consent_response_ip_and_ua_none_by_default(self):
        """ip_address und user_agent sind None wenn nicht gesetzt (make_consent Default)."""
        c = make_consent()
        row = {"ip_address": c.ip_address, "user_agent": c.user_agent}
        assert row["ip_address"] is None
        assert row["user_agent"] is None


# ============================================================================
# History-Tracking Tests (Migration 009)
# ============================================================================

class TestConsentHistoryTracking:
    def test_record_history_helper_builds_entry(self):
        """_record_history() erstellt korrekt befuelltes EinwilligungenConsentHistoryDB-Objekt."""
        from compliance.db.einwilligungen_models import EinwilligungenConsentHistoryDB

        consent = make_consent()
        consent.ip_address = '10.0.0.1'
        consent.user_agent = 'TestAgent/1.0'
        consent.source = 'test-source'

        mock_db = MagicMock()

        # Simulate _record_history inline (mirrors implementation)
        entry = EinwilligungenConsentHistoryDB(
            consent_id=consent.id,
            tenant_id=consent.tenant_id,
            action='granted',
            consent_version=consent.consent_version,
            ip_address=consent.ip_address,
            user_agent=consent.user_agent,
            source=consent.source,
        )
        mock_db.add(entry)

        assert entry.tenant_id == consent.tenant_id
        assert entry.consent_id == consent.id
        assert entry.ip_address == '10.0.0.1'
        assert entry.user_agent == 'TestAgent/1.0'
        assert entry.source == 'test-source'
        mock_db.add.assert_called_once_with(entry)

    def test_history_entry_has_correct_action_granted(self):
        """History-Eintrag bei Einwilligung hat action='granted'."""
        from compliance.db.einwilligungen_models import EinwilligungenConsentHistoryDB

        consent = make_consent()
        entry = EinwilligungenConsentHistoryDB(
            consent_id=consent.id,
            tenant_id=consent.tenant_id,
            action='granted',
            consent_version=consent.consent_version,
        )
        assert entry.action == 'granted'

    def test_history_entry_has_correct_action_revoked(self):
        """History-Eintrag bei Widerruf hat action='revoked'."""
        from compliance.db.einwilligungen_models import EinwilligungenConsentHistoryDB

        consent = make_consent()
        consent.revoked_at = datetime.now(timezone.utc)
        entry = EinwilligungenConsentHistoryDB(
            consent_id=consent.id,
            tenant_id=consent.tenant_id,
            action='revoked',
            consent_version=consent.consent_version,
        )
        assert entry.action == 'revoked'

    def test_history_serialization_format(self):
        """Response-Dict fuer einen History-Eintrag enthaelt alle 8 Pflichtfelder."""
        import uuid as _uuid

        entry_id = _uuid.uuid4()
        consent_id = _uuid.uuid4()
        now = datetime.now(timezone.utc)

        row = {
            "id": str(entry_id),
            "consent_id": str(consent_id),
            "action": "granted",
            "consent_version": "1.0",
            "ip_address": "192.168.1.1",
            "user_agent": "Mozilla/5.0",
            "source": "web_banner",
            "created_at": now,
        }

        assert len(row) == 8
        assert "id" in row
        assert "consent_id" in row
        assert "action" in row
        assert "consent_version" in row
        assert "ip_address" in row
        assert "user_agent" in row
        assert "source" in row
        assert "created_at" in row

    def test_history_empty_list_for_no_entries(self):
        """GET /consents/{id}/history gibt leere Liste zurueck wenn keine Eintraege vorhanden."""
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.order_by.return_value.all.return_value = []

        entries = mock_db.query().filter().order_by().all()
        result = [{"id": str(e.id), "action": e.action} for e in entries]

        assert result == []


# =============================================================================
# Cookie Banner Embed-Code + Banner-Text Persistenz
# =============================================================================

class TestCookieBannerEmbedCode:
    """Tests fuer Cookie-Banner Embed-Code und Banner-Text Persistenz."""

    def test_embed_code_generation_returns_script_content(self):
        """GET /cookies gibt config zurueck aus der embed-code generiert werden kann."""
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB

        config = EinwilligungenCookiesDB(
            tenant_id='t1',
            categories=[{'id': 'necessary', 'name': 'Notwendig'}],
            config={'position': 'bottom', 'style': 'banner'},
        )
        assert config.tenant_id == 't1'
        assert len(config.categories) == 1
        assert config.config['position'] == 'bottom'

    def test_banner_texts_saved_in_config(self):
        """PUT /cookies mit banner_texts in config persistiert die Texte."""
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB

        banner_texts = {
            'title': 'Cookie Hinweis',
            'description': 'Wir nutzen Cookies',
            'accept_all': 'Alle akzeptieren',
        }
        config = EinwilligungenCookiesDB(
            tenant_id='t2',
            categories=[],
            config={'banner_texts': banner_texts},
        )
        assert config.config['banner_texts']['title'] == 'Cookie Hinweis'
        assert config.config['banner_texts']['accept_all'] == 'Alle akzeptieren'

    def test_config_roundtrip_preserves_all_fields(self):
        """Config-Felder (styling, texts, position) bleiben bei Upsert erhalten."""
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB

        full_config = {
            'position': 'bottom-right',
            'style': 'card',
            'primaryColor': '#2563eb',
            'banner_texts': {'title': 'Test', 'description': 'Desc'},
        }
        config = EinwilligungenCookiesDB(tenant_id='t3', categories=[], config=full_config)
        assert config.config['primaryColor'] == '#2563eb'
        assert config.config['banner_texts']['description'] == 'Desc'

    def test_required_categories_flag_preserved(self):
        """isRequired-Flag fuer notwendige Kategorien bleibt nach Upsert erhalten."""
        from compliance.db.einwilligungen_models import EinwilligungenCookiesDB

        categories = [
            {'id': 'necessary', 'name': 'Notwendig', 'isRequired': True, 'defaultEnabled': True},
            {'id': 'analytics', 'name': 'Analyse', 'isRequired': False, 'defaultEnabled': False},
        ]
        config = EinwilligungenCookiesDB(tenant_id='t4', categories=categories, config={})
        required = [c for c in config.categories if c.get('isRequired')]
        assert len(required) == 1
        assert required[0]['id'] == 'necessary'
