"""Admin gate — per-member TOTP enrollment + revocation isolation."""

from __future__ import annotations

import json

import pyotp
import pytest

from psyc.cockpit import adminauth


@pytest.fixture
def fresh_state(tmp_path, monkeypatch):
    monkeypatch.setattr(adminauth, "_STATE_PATH", tmp_path / "admin_auth.json")
    yield tmp_path / "admin_auth.json"


def _code_for_secret(secret: str) -> str:
    return pyotp.TOTP(secret).now()


def _secret_of(state_path, member_id: str) -> str:
    data = json.loads(state_path.read_text())
    return next(m["secret"] for m in data["members"] if m["id"] == member_id)


def test_starts_unbootstrapped(fresh_state):
    assert adminauth.is_bootstrapped() is False
    assert adminauth.members() == []


def test_bootstrap_promotes_pending_to_owner(fresh_state):
    code = adminauth.current_code()  # pending secret
    assert adminauth.verify(code) == "owner"
    assert adminauth.is_bootstrapped() is True
    assert [m["label"] for m in adminauth.members()] == ["owner"]


def test_add_member_then_each_secret_authenticates(fresh_state):
    adminauth.verify(adminauth.current_code())  # bootstrap owner
    aid, _ = adminauth.add_member("alice")
    bid, _ = adminauth.add_member("bob")
    assert adminauth.verify(_code_for_secret(_secret_of(fresh_state, aid))) == "alice"
    assert adminauth.verify(_code_for_secret(_secret_of(fresh_state, bid))) == "bob"


def test_revoke_isolates_one_member(fresh_state):
    adminauth.verify(adminauth.current_code())
    aid, _ = adminauth.add_member("alice")
    bid, _ = adminauth.add_member("bob")
    a_secret = _secret_of(fresh_state, aid)
    b_secret = _secret_of(fresh_state, bid)

    assert adminauth.revoke_member(aid) is True

    # Alice's code no longer works…
    assert adminauth.verify(_code_for_secret(a_secret)) is None
    # …but Bob's still does.
    assert adminauth.verify(_code_for_secret(b_secret)) == "bob"
    # Alice is gone from the active roster, Bob remains.
    labels = [m["label"] for m in adminauth.members()]
    assert "alice" not in labels and "bob" in labels


def test_members_never_expose_secrets(fresh_state):
    adminauth.verify(adminauth.current_code())
    adminauth.add_member("alice")
    for m in adminauth.members():
        assert "secret" not in m


def test_revoke_unknown_id_is_false(fresh_state):
    adminauth.verify(adminauth.current_code())
    assert adminauth.revoke_member("deadbeef") is False


def test_migrates_old_single_secret_format(fresh_state):
    # Simulate the pre-stage-27 state file.
    old_secret = pyotp.random_base32()
    fresh_state.write_text(json.dumps({
        "totp_secret": old_secret, "session_secret": "s", "provisioned": True,
    }))
    # Old enrolled secret should authenticate as the migrated 'owner'.
    assert adminauth.verify(_code_for_secret(old_secret)) == "owner"
    assert [m["label"] for m in adminauth.members()] == ["owner"]
