"""End-to-end test for Phase 5: Atomic token deduction on /api/v1/download."""
import os, sys, tempfile, shutil, json, io, zipfile, importlib, pathlib

# 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_p5'
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()

import app as app_module
flask_app = app_module.app
flask_app.testing = True
print('v1 rules:', sum(1 for r in flask_app.url_map.iter_rules() if '/api/v1/' in r.rule))


def fresh_account(name, tokens=1000):
    return licensing.create_account(name, None, None, tokens)


def do_login(client, uid):
    rv = client.post('/api/v1/login', json={
        'uid': uid,
        'fingerprint': 'fp-test-' + os.urandom(4).hex(),
    })
    if rv.status_code != 200:
        raise RuntimeError(f'Login failed: {rv.status_code} {rv.data!r}')
    return rv.get_json()


def seed_session_done(sid, periode='2024', modules=None):
    """Create a 'done' session matching what main.py would produce."""
    td = pathlib.Path(app_module.sm.temp_dir(sid))
    r = td / 'Rechnungen'
    # pdfs
    (r / periode).mkdir(parents=True, exist_ok=True)
    (r / periode / 'Rechnung_001.pdf').write_bytes(b'%PDF-1.4 dummy 1')
    (r / periode / 'Rechnung_002.pdf').write_bytes(b'%PDF-1.4 dummy 2')
    # kassabuch
    (r / 'Kassabuch.xlsx').write_bytes(b'dummy xlsx kassabuch')
    # sepa (liegt in Rechnungen/SEPA/ – so wie _build_zip es erwartet)
    (r / 'SEPA').mkdir(exist_ok=True)
    (r / 'SEPA' / 'SEPA_2024.xml').write_bytes(b'<?xml version="1.0"?><Document/>')
    # report
    (r / 'Report').mkdir(exist_ok=True)
    (r / 'Report' / 'Gemeinschaftsbericht.pdf').write_bytes(b'%PDF-1.4 report')
    sess = app_module.sm.get(sid)
    sess['status'] = 'done'
    sess['data'] = {'periode': periode}
    if modules is not None:
        sess['modules'] = list(modules)


def new_session(client):
    rv = client.post('/api/v1/session/new')
    if rv.status_code != 200:
        raise RuntimeError(f'session/new failed: {rv.status_code} {rv.data!r}')
    return rv.get_json()['sid']


def balance_of(uid):
    for a in licensing.list_accounts(include_disabled=True):
        if a['uid'] == uid:
            return a['remaining_tokens']
    return None


# --- Scenario 1: Happy path ---
print('\n=== Scenario 1: Happy path (1000 tokens, all modules) ===')
uid1 = fresh_account('Kunde-S1', tokens=1000)
with flask_app.test_client() as c:
    do_login(c, uid1)
    sid = new_session(c)
    seed_session_done(sid, modules=['pdfs','report','kassabuch','sepa'])
    rv = c.get(f'/api/v1/download?sid={sid}')
    print(f'  HTTP: {rv.status_code}')
    print(f'  Content-Type: {rv.headers.get("Content-Type")}')
    print(f'  Bytes: {len(rv.data)}')
    assert rv.status_code == 200, f'Expected 200, got {rv.status_code}: {rv.data[:300]!r}'
    assert rv.headers.get('Content-Type') == 'application/zip'
    zf = zipfile.ZipFile(io.BytesIO(rv.data))
    names = zf.namelist()
    print(f'  ZIP entries: {names}')
    assert len(names) == 5, f'Expected 5 files in ZIP, got {names}'
    # cookie cleared?
    set_cookies = rv.headers.getlist('Set-Cookie')
    cleared = any('license_session=' in sc and ('Expires=Thu, 01 Jan 1970' in sc or 'Max-Age=0' in sc) for sc in set_cookies)
    print(f'  Cookie cleared: {cleared}')
    assert cleared, f'License cookie NOT cleared: {set_cookies}'
bal1 = balance_of(uid1)
print(f'  Balance after: {bal1} (was 1000)')
assert bal1 < 1000

# --- Scenario 2: Insufficient tokens ---
print('\n=== Scenario 2: Insufficient tokens (1 token balance) ===')
uid2 = fresh_account('Kunde-S2', tokens=1)
with flask_app.test_client() as c:
    do_login(c, uid2)
    sid = new_session(c)
    seed_session_done(sid, modules=['pdfs','report','kassabuch','sepa'])
    rv = c.get(f'/api/v1/download?sid={sid}')
    print(f'  HTTP: {rv.status_code}')
    body = rv.get_json()
    print(f'  reason: {body.get("reason") if body else "--"}')
    assert rv.status_code == 402, f'Expected 402, got {rv.status_code}: {rv.data[:300]!r}'
    assert body.get('reason') == 'no_tokens'
bal2 = balance_of(uid2)
print(f'  Balance after: {bal2} (should still be 1 - atomic rollback)')
assert bal2 == 1, f'Balance changed on failure: {bal2}'

# --- Scenario 3: Disabled account post-login ---
print('\n=== Scenario 3: Account disabled post-login (decorator rejects) ===')
uid3 = fresh_account('Kunde-S3', tokens=500)
with flask_app.test_client() as c:
    do_login(c, uid3)
    sid = new_session(c)
    seed_session_done(sid, modules=['pdfs','report','kassabuch','sepa'])
    licensing.set_status(uid3, 'disabled', admin_user='test')
    rv = c.get(f'/api/v1/download?sid={sid}')
    print(f'  HTTP: {rv.status_code}')
    body = rv.get_json()
    print(f'  need_login: {body.get("need_login") if body else "--"}')
    assert rv.status_code == 401, f'Expected 401, got {rv.status_code}'
    assert body.get('need_login') is True
bal3 = balance_of(uid3)
print(f'  Balance after: {bal3} (should still be 500)')
assert bal3 == 500

# --- Scenario 4: Module subset ---
print('\n=== Scenario 4: Module subset (pdfs+report only) ===')
uid4 = fresh_account('Kunde-S4', tokens=1000)
with flask_app.test_client() as c:
    do_login(c, uid4)
    sid = new_session(c)
    seed_session_done(sid, modules=['pdfs','report'])
    bal_before = balance_of(uid4)
    rv = c.get(f'/api/v1/download?sid={sid}')
    print(f'  HTTP: {rv.status_code}')
    assert rv.status_code == 200, f'Body: {rv.data[:300]!r}'
    zf = zipfile.ZipFile(io.BytesIO(rv.data))
    names = zf.namelist()
    print(f'  ZIP entries: {names}')
    has_sepa = any('SEPA' in n for n in names)
    has_kassa = any('Kassabuch' in n for n in names)
    has_pdf = any(n.startswith('Rechnungen/') and n.endswith('.pdf') for n in names)
    has_rep = any(n.startswith('Gemeinschaftsbericht/') for n in names)
    print(f'  pdfs={has_pdf} report={has_rep} kassabuch={has_kassa} sepa={has_sepa}')
    assert has_pdf and has_rep and not has_sepa and not has_kassa, f'Filter broken: {names}'
bal4 = balance_of(uid4)
print(f'  Deducted: {bal_before - bal4} tokens (subset, should be less than full)')

full_deducted = 1000 - bal1
subset_deducted = bal_before - bal4
print(f'  Full-set deducted: {full_deducted}, subset deducted: {subset_deducted}')
assert subset_deducted <= full_deducted, f'Subset cost more than full?! {subset_deducted} vs {full_deducted}'

# --- Scenario 5: ZIP failure - NO deduction ---
print('\n=== Scenario 5: _build_zip raises exception - NO deduction ===')
uid5 = fresh_account('Kunde-S5', tokens=1000)
orig_build_zip = app_module._build_zip
def broken_zip(td, periode, modules=None):
    raise RuntimeError('Simulierter ZIP-Fehler')
app_module._build_zip = broken_zip
try:
    with flask_app.test_client() as c:
        do_login(c, uid5)
        sid = new_session(c)
        seed_session_done(sid, modules=['pdfs','report','kassabuch','sepa'])
        rv = c.get(f'/api/v1/download?sid={sid}')
        print(f'  HTTP: {rv.status_code}')
        body = rv.get_json()
        print(f'  error: {body.get("error") if body else "--"}')
        assert rv.status_code == 500
    bal5 = balance_of(uid5)
    print(f'  Balance after: {bal5} (should still be 1000 - atomic)')
    assert bal5 == 1000, f'Tokens deducted despite ZIP failure: {bal5}'
finally:
    app_module._build_zip = orig_build_zip

# --- Scenario 6: Re-using an already-consumed session ---
print('\n=== Scenario 6: Session destroyed after successful download (no replay) ===')
uid6 = fresh_account('Kunde-S6', tokens=1000)
with flask_app.test_client() as c:
    do_login(c, uid6)
    sid = new_session(c)
    seed_session_done(sid, modules=['pdfs','report','kassabuch','sepa'])
    rv1 = c.get(f'/api/v1/download?sid={sid}')
    assert rv1.status_code == 200
    import time; time.sleep(0.3)
    rv2 = c.get(f'/api/v1/download?sid={sid}')
    print(f'  Second request HTTP: {rv2.status_code}')
    assert rv2.status_code in (401, 404), f'Replay should fail, got {rv2.status_code}'
bal6 = balance_of(uid6)
print(f'  Balance after: {bal6} (one deduction only)')

print('\n=== ALL PHASE 5 TESTS PASSED ===')
of(uid6)
print(f'  Balance after: {bal6} (one deduction only)')

print('\n=== ALL PHASE 5 TESTS PASSED ===')
