"""End-to-end test for Phase 7: Rate-Limiting + DSGVO.

Wichtig: `licensing.login` pflegt zusätzlich eine DB-basierte Failed-Login-Liste
pro IP. Um Testpollution zwischen Szenarien zu vermeiden, nutzen wir je
Test eine eigene X-Forwarded-For IP."""
import os, sys, pathlib, shutil, json, tempfile

# Windows: Konsole ist default cp1252 -> Emoji/Pfeile im Print killen den Test.
try:
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except Exception:
    pass

APP_DIR = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(APP_DIR))

TEST_ROOT = pathlib.Path(tempfile.gettempdir()) / 'abrechnung_test_p7'
shutil.rmtree(str(TEST_ROOT), ignore_errors=True)
TEST_ROOT.mkdir(parents=True, exist_ok=True)
TEST_DB = str(TEST_ROOT / 'licensing.sqlite3')
os.environ['ABRECHNUNG_LICENSE_DB'] = TEST_DB

import session_manager
session_manager.TEMP_BASE = TEST_ROOT

import licensing
licensing.init_db()
licensing.create_admin('admin-p7', 'passwordp7')

import app as app_module
flask_app = app_module.app
flask_app.testing = True

def reset_rate_limiter():
    app_module._RATE_BUCKETS.clear()

def reset_db_rate_limit(ip):
    """Löscht DB-basierte Fehlversuche für eine IP."""
    import sqlite3
    c = sqlite3.connect(TEST_DB)
    c.execute("DELETE FROM failed_logins WHERE ip = ?", (ip,))
    c.commit(); c.close()

print("=== Phase 7 Tests ===\n")

# --- 1. In-Memory Login-Rate-Limit (20 per IP per 60s) ---
#     DB-basiertes check_rate_limit greift bereits nach 10 FAILED logins,
#     deshalb testen wir mit gültigen credentials — mit einem echten UID.
reset_rate_limiter()
IP1 = '203.0.113.1'
reset_db_rate_limit(IP1)
uid_for_login = licensing.create_account('LoginTest', None, None, 1000)
print("1. /api/v1/login Burst-Limit (20 per IP per 60s) — gültige Requests")
hit = None
with flask_app.test_client() as c:
    for i in range(25):
        rv = c.post('/api/v1/login', json={'uid': uid_for_login, 'fingerprint': 'validfp'*3},
                    headers={'X-Forwarded-For': IP1})
        if rv.status_code == 429:
            hit = i + 1
            assert rv.get_json()['reason'] == 'rate_limited'
            break
        # Andere Logouts, damit keine Session-Kollision
        c.post('/api/v1/logout')
print(f"   → Limit greift bei Versuch {hit}")
assert hit is not None and 20 <= hit <= 22, f"Burst-Limit nicht wie erwartet: {hit}"

# --- 2. Admin-Login-Burst-Limit (15 pro IP pro 5min) ---
reset_rate_limiter()
IP2 = '203.0.113.2'
reset_db_rate_limit(IP2)
print("\n2. /admin/api/login Burst-Limit (15 per IP per 5 min)")
hit = None
with flask_app.test_client() as c:
    for i in range(20):
        rv = c.post('/admin/api/login',
                    json={'username': 'admin-p7', 'password': 'passwordp7'},
                    headers={'X-Forwarded-For': IP2})
        if rv.status_code == 429:
            hit = i + 1
            break
        # logout damit die Session nicht persistiert (nicht nötig für Rate-Limit)
print(f"   → Limit greift bei Versuch {hit}")
assert hit is not None and 15 <= hit <= 17, f"Admin-Burst-Limit nicht wie erwartet: {hit}"

# --- 3. /api/v1/process Rate-Limit per UID (20/min) ---
reset_rate_limiter()
IP3 = '203.0.113.3'
reset_db_rate_limit(IP3)
uid = licensing.create_account('ProcessTest', None, None, 1000)
with flask_app.test_client() as c:
    rv = c.post('/api/v1/login', json={'uid': uid, 'fingerprint': 'validfp'*3},
                headers={'X-Forwarded-For': IP3})
    assert rv.status_code == 200, f'Login fail: {rv.status_code} {rv.get_json()}'

    print("\n3. /api/v1/process Rate-Limit per UID (20/min)")
    hit = None
    for i in range(25):
        rv = c.get('/api/v1/process?sid=x', headers={'X-Forwarded-For': IP3})
        if rv.status_code == 429:
            hit = i + 1
            break
    print(f"   → Limit greift bei Versuch {hit}")
    assert hit is not None and 20 <= hit <= 22, f"Process-Limit nicht wie erwartet: {hit}"

# --- 4. /api/v1/download Rate-Limit per UID (10/min) ---
reset_rate_limiter()
IP4 = '203.0.113.4'
reset_db_rate_limit(IP4)
uid2 = licensing.create_account('DownloadTest', None, None, 1000)
with flask_app.test_client() as c:
    rv = c.post('/api/v1/login', json={'uid': uid2, 'fingerprint': 'validfp'*3},
                headers={'X-Forwarded-For': IP4})
    assert rv.status_code == 200, f'Login fail: {rv.status_code}'
    print("\n4. /api/v1/download Rate-Limit per UID (10/min)")
    hit = None
    for i in range(15):
        rv = c.get('/api/v1/download?sid=x', headers={'X-Forwarded-For': IP4})
        if rv.status_code == 429:
            hit = i + 1
            break
    print(f"   → Limit greift bei Versuch {hit}")
    assert hit is not None and 10 <= hit <= 12, f"Download-Limit nicht wie erwartet: {hit}"

# --- 5. DSGVO-Export (Art. 15) ---
reset_rate_limiter()
IP5 = '203.0.113.5'
reset_db_rate_limit(IP5)
# Simuliere einen Usage-Log-Eintrag direkt in der DB
import sqlite3
conn = sqlite3.connect(TEST_DB)
conn.execute(
    "INSERT INTO usage_log (account_uid, timestamp, ip, user_agent, fingerprint_hash, fingerprint_match, "
    "session_id, periode, modules, tokens_consumed, remaining_after, event_type) "
    "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
    (uid, '2026-01-01 10:00:00', '192.0.2.1', 'test-ua', 'testfp'*3, 1,
     'dummy-sid-12345', '2026-01', '["pdfs","sepa"]', 16, 984, 'download'),
)
conn.commit(); conn.close()

print("\n5. DSGVO-Export (Art. 15)")
with flask_app.test_client() as c:
    rv = c.post('/admin/api/login', json={'username': 'admin-p7', 'password': 'passwordp7'},
                headers={'X-Forwarded-For': IP5})
    assert rv.status_code == 200, rv.get_json()
    rv = c.get(f'/admin/api/accounts/{uid}/export')
    print(f"   → {rv.status_code}, Größe: {len(rv.data)}, Content-Type: {rv.headers.get('Content-Type')}")
    assert rv.status_code == 200
    assert 'attachment' in rv.headers.get('Content-Disposition', '')
    data = json.loads(rv.data)
    assert data['uid'] == uid
    assert data['account']['customer_name'] == 'ProcessTest'
    # >=2: mindestens 1 Login + 1 fake Download
    assert len(data['usage_log']) >= 1
    assert any(e['ip'] == '192.0.2.1' for e in data['usage_log'])

    # --- 6. Export für unbekannte UID → 404 ---
    rv = c.get('/admin/api/accounts/EEGA-XXXX-XXXX-XXXX/export')
    print(f"\n6. Export für unbekannte UID → {rv.status_code}")
    assert rv.status_code == 404

    # --- 7. DSGVO-Löschung (Art. 17) ---
    print("\n7. DSGVO-Löschung (Art. 17)")
    rv = c.delete(f'/admin/api/accounts/{uid}')
    print(f"   → {rv.status_code}, Response: {rv.get_json()}")
    assert rv.status_code == 200
    assert rv.get_json()['usage_rows_deleted'] >= 1
    assert rv.get_json()['status'] == 'disabled'

    # --- 8. Nach Löschung: Daten anonymisiert ---
    rv = c.get(f'/admin/api/accounts/{uid}/export')
    data = json.loads(rv.data)
    print(f"8. Nach Löschung: customer_name={data['account']['customer_name']!r}, "
          f"status={data['account']['status']}, usage={len(data['usage_log'])}")
    assert data['account']['customer_name'] == '[gelöscht]'
    assert data['account']['status'] == 'disabled'
    assert len(data['usage_log']) == 0

    # --- 9. Audit-Log enthält delete_account ---
    rv = c.get('/admin/api/audit')
    actions = {e['action'] for e in rv.get_json()['entries']}
    print(f"9. Audit-Aktionen: {sorted(actions)}")
    assert 'delete_account' in actions

    # --- 10. Löschung für unbekannte UID → 404 ---
    rv = c.delete('/admin/api/accounts/EEGA-NOPE-NOPE-NOPE')
    print(f"10. Delete für unbekannte UID → {rv.status_code}")
    assert rv.status_code == 404

# --- 11. /datenschutz rendert ---
print("\n11. GET /datenschutz")
with flask_app.test_client() as c:
    rv = c.get('/datenschutz')
    print(f"    → {rv.status_code}, Länge: {len(rv.data)} bytes")
    assert rv.status_code == 200
    html = rv.data.decode('utf-8')
    assert 'Datenschutzerklärung' in html
    assert 'Art. 15' in html
    assert 'Art. 17' in html

# --- 12. DSGVO-Banner in index.html ---
print("\n12. DSGVO-Banner in index.html?")
with flask_app.test_client() as c:
    rv = c.get('/EEG-Assistent/abrechnung')
    assert rv.status_code == 200
    html = rv.data.decode('utf-8')
    assert 'dsgvo-banner' in html
    assert '/datenschutz' in html
    assert 'eeg_dsgvo_ack_v1' in html
    print("    → Banner + Link + LocalStorage-Key vorhanden")

# --- 13. X-Forwarded-For per-IP Rate-Limit-Trennung ---
print("\n13. X-Forwarded-For: verschiedene IPs haben getrennte Buckets")
reset_rate_limiter()
IP_A = '203.0.113.100'
IP_B = '203.0.113.200'
reset_db_rate_limit(IP_A); reset_db_rate_limit(IP_B)
uid_x = licensing.create_account('XFF-Test', None, None, 1000)
with flask_app.test_client() as c:
    # IP_A pummelt bis Limit
    hit = None
    for i in range(25):
        rv = c.post('/api/v1/login', json={'uid': uid_x, 'fingerprint': 'validfp'*3},
                    headers={'X-Forwarded-For': IP_A})
        if rv.status_code == 429:
            hit = i + 1
            break
        c.post('/api/v1/logout')
    assert hit is not None
    print(f"    → IP_A blockiert nach Versuch {hit}")
    # IP_B ist frei
    rv = c.post('/api/v1/login', json={'uid': uid_x, 'fingerprint': 'validfp'*3},
                headers={'X-Forwarded-For': IP_B})
    print(f"    → IP_B erster Request: {rv.status_code}")
    assert rv.status_code == 200

print("\n=== ALL PHASE 7 TESTS PASSED ===")
