"""Connexion, MFA, sessions, CSRF, verrouillage, rôles (tests d'intégration et de sécurité)."""
import pyotp

from edcf.core import db as dbmod
from edcf.models import AuditLog
from tests.conftest import Api, make_user


def actions():
    with dbmod.session_scope() as db:
        return [a.action for a in db.query(AuditLog).order_by(AuditLog.id)]


def test_route_sans_authentification(api: Api):
    assert api.get("/api/bilans/2026/40").status_code == 401
    assert api.get("/api/admin/users").status_code == 401
    assert api.get("/api/auth/me").status_code == 401


def test_connexion_mfa_et_journalisation(api: Api):
    u = make_user("chef")
    r = api.post("/api/auth/login", {"username": "chef", "password": u["password"]})
    assert r.status_code == 200 and r.json()["state"]["mfa_pending"] is True
    # Session partielle : aucune donnée accessible avant le second facteur.
    assert api.get("/api/bilans/2026/40").status_code == 401
    assert api.post("/api/auth/mfa", {"code": "000000"}).status_code == 401
    r = api.post("/api/auth/mfa", {"code": pyotp.TOTP(u["secret"]).now()})
    assert r.status_code == 200 and r.json()["state"]["mfa_pending"] is False
    assert api.get("/api/bilans/2026/40").status_code == 200
    assert "auth.mfa_failed" in actions() and "auth.login" in actions()


def test_code_totp_non_rejouable(app):
    u = make_user("chef2")
    code = pyotp.TOTP(u["secret"]).now()
    a, b = Api(app), Api(app)
    a.post("/api/auth/login", {"username": "chef2", "password": u["password"]})
    assert a.post("/api/auth/mfa", {"code": code}).status_code == 200
    b.post("/api/auth/login", {"username": "chef2", "password": u["password"]})
    assert b.post("/api/auth/mfa", {"code": code}).status_code == 401


def test_message_unique_anti_enumeration(api: Api):
    make_user("existant")
    r1 = api.post("/api/auth/login", {"username": "existant", "password": "mauvais-mot-de-passe"})
    r2 = api.post("/api/auth/login", {"username": "inexistant", "password": "mauvais-mot-de-passe"})
    assert r1.status_code == r2.status_code == 401
    assert r1.json()["message"] == r2.json()["message"]


def test_verrouillage_apres_tentatives_repetees(api: Api):
    u = make_user("cible", "reader")
    for _ in range(5):
        assert api.post("/api/auth/login", {"username": "cible", "password": "faux-faux-faux"}).status_code == 401
    r = api.post("/api/auth/login", {"username": "cible", "password": u["password"]})
    assert r.status_code == 429
    assert "auth.login_blocked" in actions()


def test_csrf_et_origine(admin: Api):
    rows = [{"indicator_id": 1, "value": 2, "observation": ""}]
    r = admin.c.put("/api/bilans/2026/40/draft", json={"revision": 0, "rows": rows})
    assert r.status_code == 403 and r.json()["code"] == "csrf"
    r = admin.c.put("/api/bilans/2026/40/draft", json={"revision": 0, "rows": rows},
                    headers={"X-CSRF-Token": admin.csrf, "Origin": "https://malveillant.example"})
    assert r.status_code == 403 and r.json()["code"] == "bad_origin"
    assert admin.put("/api/bilans/2026/40/draft", {"revision": 0, "rows": rows}).status_code == 200


def test_lecture_seule_ne_peut_rien_modifier(reader: Api):
    assert reader.get("/api/bilans/2026/40").status_code == 200
    rows = [{"indicator_id": 1, "value": 2, "observation": ""}]
    assert reader.put("/api/bilans/2026/40/draft", {"revision": 0, "rows": rows}).status_code == 403
    assert reader.post("/api/bilans/2026/40/validate", {"revision": 1, "confirm": True}).status_code == 403
    assert reader.post("/api/bilans/2026/40/reopen", {"reason": "essai"}).status_code == 403
    assert reader.get("/api/admin/users").status_code == 403
    assert reader.get("/api/admin/settings").status_code == 403
    assert reader.get("/api/admin/audit").status_code == 403
    assert reader.patch("/api/admin/indicators/1", {"label": "Piraté"}).status_code == 403
    assert reader.get("/api/bilans/2026/40/document/export.pdf").status_code == 403


def test_session_revoquee_et_compte_desactive(admin: Api, app):
    r = Api(app)
    u = make_user("agent", "reader")
    r.login(u)
    assert r.get("/api/bilans/2026/40").status_code == 200
    assert admin.patch(f"/api/admin/users/{u['id']}", {"is_active": False}).status_code == 200
    assert r.get("/api/bilans/2026/40").status_code == 401
    assert "user.deactivated" in actions()


def test_deconnexion(admin: Api):
    assert admin.post("/api/auth/logout").status_code == 200
    assert admin.get("/api/auth/me").status_code == 401


def test_mfa_obligatoire_pour_gestionnaire_sans_mfa(api: Api):
    u = make_user("nouveau", "admin", mfa=False)
    r = api.post("/api/auth/login", {"username": "nouveau", "password": u["password"]})
    assert r.json()["state"]["mfa_setup_required"] is True
    assert api.get("/api/bilans/2026/40").json()["code"] == "mfa_setup_required"
    setup = api.post("/api/auth/mfa/setup").json()
    assert "<svg" in setup["qr_svg"]
    r = api.post("/api/auth/mfa/enable", {"code": pyotp.TOTP(setup["secret"]).now()})
    assert r.status_code == 200 and len(r.json()["recovery_codes"]) == 10
    assert api.get("/api/bilans/2026/40").status_code == 200


def test_code_de_recuperation_usage_unique(app):
    u = make_user("perdu", "admin", mfa=False)
    a = Api(app)
    a.post("/api/auth/login", {"username": "perdu", "password": u["password"]})
    setup = a.post("/api/auth/mfa/setup").json()
    codes = a.post("/api/auth/mfa/enable", {"code": pyotp.TOTP(setup["secret"]).now()}).json()["recovery_codes"]
    b = Api(app)
    b.post("/api/auth/login", {"username": "perdu", "password": u["password"]})
    assert b.post("/api/auth/mfa", {"code": codes[0]}).status_code == 200
    c = Api(app)
    c.post("/api/auth/login", {"username": "perdu", "password": u["password"]})
    assert c.post("/api/auth/mfa", {"code": codes[0]}).status_code == 401


def test_changement_de_mot_de_passe_obligatoire(api: Api):
    u = make_user("provisoire", "reader", must_change=True)
    api.login(u)
    assert api.get("/api/bilans/2026/40").json()["code"] == "password_change_required"
    weak = api.post("/api/auth/password", {"current_password": u["password"], "new_password": "azerty"})
    assert weak.status_code == 400
    ok = api.post("/api/auth/password", {"current_password": u["password"], "new_password": "Nouveau-Secret-2026!"})
    assert ok.status_code == 200
    assert api.get("/api/bilans/2026/40").status_code == 200


def test_aucun_secret_dans_le_journal(admin: Api):
    admin.post("/api/auth/password", {"current_password": "x", "new_password": "y"})
    with dbmod.session_scope() as db:
        for entry in db.query(AuditLog):
            dump = str(entry.details).lower()
            assert "correct-horse" not in dump and "password" not in dump


def test_chaine_audit_integre(admin: Api):
    r = admin.get("/api/admin/audit")
    assert r.status_code == 200 and r.json()["chain"]["ok"] is True


def test_creation_compte_lecteur_par_gestionnaire(admin: Api):
    r = admin.post("/api/admin/users", {"username": "lecteur.cdt", "display_name": "Lecteur Cdt"})
    assert r.status_code == 200
    body = r.json()
    assert body["user"]["role"] == "reader" and len(body["temporary_password"]) >= 16
    assert admin.post("/api/admin/users", {"username": "../etc", "display_name": "x y"}).status_code == 422
