"""Dashboard user accounts: registration, login, roles, API keys, usage.

Unit tests for AccountStore plus HTTP integration on the tracker:
register/login/logout, per-account balance and usage, API-key lifecycle
(revoked keys rejected by the OpenAI proxy), and the admin listing.
"""

import http.cookies
import json
import urllib.error
import urllib.request

import pytest

from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.auth import sign_hive_request
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer

HIVE_SECRET = "test-hive-secret"


# ---------------------------------------------------------------- unit tests


def test_first_account_is_admin_then_users():
    "First account is admin then users\n\nTags: accounts, auth, http"

    store = AccountStore()
    first = store.register(email="admin@example.com", password="secret-123")
    second = store.register(email="user@example.com", password="secret-123")
    assert first["role"] == "admin"
    assert second["role"] == "user"


def test_register_requires_email_or_wallet_and_password_length():
    "Register requires email or wallet and password length\n\nTags: accounts, auth, http, security, wallet"

    store = AccountStore()
    with pytest.raises(ValueError, match="email or a wallet"):
        store.register(password="secret-123")
    with pytest.raises(ValueError, match="invalid email"):
        store.register(email="not-an-email", password="secret-123")
    with pytest.raises(ValueError, match="at least 8"):
        store.register(email="a@b.co", password="short")


def test_register_rejects_duplicate_identifiers():
    "Register rejects duplicate identifiers\n\nTags: accounts, auth, http"

    store = AccountStore()
    store.register(email="dup@example.com", password="secret-123")
    with pytest.raises(ValueError, match="already exists"):
        store.register(email="DUP@example.com", password="other-secret")


def test_register_and_update_nickname():
    "Register and update nickname\n\nTags: accounts, auth, http"

    store = AccountStore()
    account = store.register(
        email="nick@example.com",
        password="secret-123",
        nickname="  Alpha  ",
    )
    assert account["nickname"] == "Alpha"
    updated = store.update_profile(account["account_id"], nickname="Beta")
    assert updated["nickname"] == "Beta"
    cleared = store.update_profile(account["account_id"], nickname=None)
    assert cleared["nickname"] is None


def test_nickname_replicates_across_stores():
    "Nickname replicates across stores\n\nTags: accounts, auth, http"

    leader = AccountStore()
    follower = AccountStore()
    account = leader.register(
        email="nick@example.com",
        password="secret-123",
        nickname="HiveNick",
    )
    leader.update_profile(account["account_id"], nickname="Renamed")
    events, _ = leader.events_since(0)
    follower.apply_events(events)
    view = follower.get_account(account["account_id"])
    assert view is not None
    assert view["nickname"] == "Renamed"


def test_login_by_email_or_wallet():
    "Login by email or wallet\n\nTags: accounts, auth, http, security, wallet"

    store = AccountStore()
    account = store.register(
        email="both@example.com", wallet="WalletXYZ", password="secret-123"
    )
    assert store.verify_login("both@example.com", "secret-123")["account_id"] == account["account_id"]
    assert store.verify_login("WalletXYZ", "secret-123")["account_id"] == account["account_id"]
    assert store.verify_login("both@example.com", "wrong-password") is None
    assert store.verify_login("nobody@example.com", "secret-123") is None


def test_sessions_resolve_and_destroy():
    "Sessions resolve and destroy\n\nTags: accounts, auth, http"

    store = AccountStore()
    account = store.register(email="s@example.com", password="secret-123")
    token = store.create_session(account["account_id"])
    assert store.session_account(token)["account_id"] == account["account_id"]
    store.destroy_session(token)
    assert store.session_account(token) is None
    assert store.session_account("bogus") is None


def test_sessions_persist_across_restart(tmp_path):
    "Sessions persist across restart\n\nTags: accounts, auth, http, persistence"

    db = str(tmp_path / "accounts.db")
    store = AccountStore(db_path=db)
    account = store.register(email="cookie@example.com", password="secret-123")
    token = store.create_session(account["account_id"])
    store.save_to_db()

    reloaded = AccountStore(db_path=db)
    assert reloaded.session_account(token)["account_id"] == account["account_id"]


def test_api_key_lifecycle():
    "Api key lifecycle\n\nTags: accounts, auth, http"

    store = AccountStore()
    account = store.register(email="k@example.com", password="secret-123")
    other = store.register(email="other@example.com", password="secret-123")
    key = store.create_api_key(account["account_id"])
    assert key.startswith("sk-mesh-")
    assert store.keys_for(account["account_id"]) == [key]
    # someone else's account cannot revoke it
    assert store.revoke_api_key(other["account_id"], key) is False
    assert store.revoke_api_key(account["account_id"], key) is True
    assert store.keys_for(account["account_id"]) == []
    assert store.is_key_revoked(key)


def test_accounts_persist_across_restart(tmp_path):
    "Accounts persist across restart\n\nTags: accounts, auth, http, persistence"

    db = str(tmp_path / "accounts.db")
    store = AccountStore(db_path=db)
    account = store.register(email="p@example.com", password="secret-123")
    key = store.create_api_key(account["account_id"])
    store.save_to_db()

    reloaded = AccountStore(db_path=db)
    assert reloaded.verify_login("p@example.com", "secret-123") is not None
    assert reloaded.keys_for(account["account_id"]) == [key]


def test_account_events_replicate_and_dedupe():
    "Account events replicate and dedupe\n\nTags: accounts, auth, http"

    leader = AccountStore()
    follower = AccountStore()
    account = leader.register(email="r@example.com", password="secret-123")
    key = leader.create_api_key(account["account_id"])
    leader.revoke_api_key(account["account_id"], key)

    events, cursor = leader.events_since(0)
    assert follower.apply_events(events) == len(events)
    assert follower.apply_events(events) == 0  # replay is a no-op
    assert follower.verify_login("r@example.com", "secret-123") is not None
    assert follower.is_key_revoked(key)
    more, _ = leader.events_since(cursor)
    assert more == []


# ---------------------------------------------------------- HTTP integration


def _call(url, method="GET", body=None, token=None):
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())


@pytest.fixture
def account_tracker():
    """Tracker with credit features pinned OFF (defaults are devnet-friendly 1.0)."""
    ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
    tracker = TrackerServer(
        billing=ledger,
        accounts=AccountStore(),
        hive_secret=HIVE_SECRET,
        starting_credit=0.0,
        devnet_topup_amount=0.0,
    )
    port = tracker.start()
    yield f"http://127.0.0.1:{port}", ledger
    tracker.stop()


def test_register_login_and_account_view(account_tracker):
    "Register login and account view\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    reg = _call(f"{url}/v1/auth/register", "POST",
                {"email": "admin@example.com", "password": "secret-123"})
    assert reg["account"]["role"] == "admin"
    assert reg["api_key"].startswith("sk-mesh-")
    assert reg["session_token"]

    login = _call(f"{url}/v1/auth/login", "POST",
                  {"identifier": "admin@example.com", "password": "secret-123"})
    me = _call(f"{url}/v1/account", token=login["session_token"])
    assert me["account"]["email"] == "admin@example.com"
    assert me["api_keys"] == [reg["api_key"]]
    assert me["total_balance"] == pytest.approx(0.0)
    assert me["usage"]["requests"] == 0
    assert "records" not in me["usage"]
    assert "recent" not in me["usage"]


def test_account_usage_endpoint_returns_records(account_tracker):
    "Account usage endpoint returns records\n\nTags: accounts, auth, http"

    url, ledger = account_tracker
    reg = _call(f"{url}/v1/auth/register", "POST",
                {"email": "usage@example.com", "password": "secret-123"})
    ledger.charge_request(reg["api_key"], "test-model", total_tokens=42, node_work=[("wallet-1", 1)])
    usage = _call(f"{url}/v1/account/usage", token=reg["session_token"])
    assert usage["requests"] == 1
    assert usage["total_tokens"] == 42
    assert len(usage["records"]) == 1
    assert usage["records"][0]["model"] == "test-model"


def test_account_nickname_register_and_profile_update(account_tracker):
    "Account nickname register and profile update\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    reg = _call(f"{url}/v1/auth/register", "POST", {
        "email": "nick@example.com",
        "password": "secret-123",
        "nickname": "Operator",
    })
    assert reg["account"]["nickname"] == "Operator"

    updated = _call(
        f"{url}/v1/account/profile",
        "POST",
        {"nickname": "Renamed"},
        token=reg["session_token"],
    )
    assert updated["account"]["nickname"] == "Renamed"

    me = _call(f"{url}/v1/account", token=reg["session_token"])
    assert me["account"]["nickname"] == "Renamed"


def test_login_sets_cookie_and_cookie_auth_survives_tracker_restart(tmp_path):
    "Login sets cookie and cookie auth survives tracker restart\n\nTags: accounts, auth, http, security"

    accounts_db = str(tmp_path / "accounts.db")
    tracker = TrackerServer(
        billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
        accounts_db=accounts_db,
        starting_credit=0.0,
        devnet_topup_amount=0.0,
    )
    port = tracker.start()
    url = f"http://127.0.0.1:{port}"
    try:
        _call(f"{url}/v1/auth/register", "POST",
              {"email": "cookie-http@example.com", "password": "secret-123"})
        req = urllib.request.Request(
            f"{url}/v1/auth/login",
            data=json.dumps({
                "identifier": "cookie-http@example.com",
                "password": "secret-123",
            }).encode(),
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        with urllib.request.urlopen(req) as r:
            assert json.loads(r.read())["session_token"]
            cookie_header = r.headers["Set-Cookie"]
    finally:
        tracker.stop()

    cookie = http.cookies.SimpleCookie(cookie_header)
    session_cookie = cookie["meshnet_session"].OutputString()

    restarted = TrackerServer(
        billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
        accounts_db=accounts_db,
        starting_credit=0.0,
        devnet_topup_amount=0.0,
    )
    restarted_port = restarted.start()
    restarted_url = f"http://127.0.0.1:{restarted_port}"
    try:
        req = urllib.request.Request(
            f"{restarted_url}/v1/account",
            headers={"Cookie": session_cookie},
            method="GET",
        )
        with urllib.request.urlopen(req) as r:
            me = json.loads(r.read())
    finally:
        restarted.stop()

    assert me["account"]["email"] == "cookie-http@example.com"


def test_bad_credentials_and_missing_session_are_401(account_tracker):
    "Bad credentials and missing session are 401\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    _call(f"{url}/v1/auth/register", "POST",
          {"email": "a@example.com", "password": "secret-123"})
    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/auth/login", "POST",
              {"identifier": "a@example.com", "password": "wrong-pass"})
    assert exc_info.value.code == 401
    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/account")
    assert exc_info.value.code == 401


def test_key_create_revoke_and_revoked_key_rejected_by_proxy(account_tracker):
    "Key create revoke and revoked key rejected by proxy\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    reg = _call(f"{url}/v1/auth/register", "POST",
                {"email": "k@example.com", "password": "secret-123"})
    token = reg["session_token"]

    new_key = _call(f"{url}/v1/account/keys", "POST", {}, token=token)["api_key"]
    me = _call(f"{url}/v1/account", token=token)
    assert sorted(me["api_keys"]) == sorted([reg["api_key"], new_key])

    _call(f"{url}/v1/account/keys/revoke", "POST", {"api_key": new_key}, token=token)
    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/chat/completions", "POST",
              {"model": "any", "messages": []}, token=new_key)
    assert exc_info.value.code == 401
    assert "revoked" in exc_info.value.read().decode()


def test_admin_listing_requires_admin_role(account_tracker):
    "Admin listing requires admin role\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    admin = _call(f"{url}/v1/auth/register", "POST",
                  {"email": "admin@example.com", "password": "secret-123"})
    user = _call(f"{url}/v1/auth/register", "POST",
                 {"wallet": "WalletUser1", "password": "secret-123"})

    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/admin/accounts", token=user["session_token"])
    assert exc_info.value.code == 403

    listing = _call(f"{url}/v1/admin/accounts", token=admin["session_token"])
    accounts = listing["accounts"]
    assert len(accounts) == 2
    assert accounts[0]["role"] == "admin"
    assert accounts[1]["wallet"] == "WalletUser1"
    assert "balances" in accounts[0]


def test_accounts_gossip_endpoint_applies_events(account_tracker):
    "Accounts gossip endpoint applies events\n\nTags: accounts, auth, gossip, http, network"

    url, _ = account_tracker
    peer = AccountStore()
    peer.register(email="remote@example.com", password="secret-123")
    events, _ = peer.events_since(0)
    body = json.dumps({"events": events}).encode()
    req = urllib.request.Request(
        f"{url}/v1/accounts/gossip", data=body,
        headers={"Content-Type": "application/json", **sign_hive_request(HIVE_SECRET, body)},
        method="POST",
    )
    with urllib.request.urlopen(req) as r:
        result = json.loads(r.read())
    assert result["applied"] == len(events)
    login = _call(f"{url}/v1/auth/login", "POST",
                  {"identifier": "remote@example.com", "password": "secret-123"})
    assert login["account"]["email"] == "remote@example.com"


def test_accounts_endpoints_404_when_disabled():
    "Accounts endpoints 404 when disabled\n\nTags: accounts, auth, http"

    tracker = TrackerServer(enable_billing=False)  # no accounts, no billing
    port = tracker.start()
    try:
        with pytest.raises(urllib.error.HTTPError) as exc_info:
            _call(f"http://127.0.0.1:{port}/v1/auth/register", "POST",
                  {"email": "x@example.com", "password": "secret-123"})
        assert exc_info.value.code == 404
    finally:
        tracker.stop()


# ------------------------------------------- US-039/US-040: credit and top-up


@pytest.fixture
def funded_tracker():
    """Tracker with Caller Credit and the devnet top-up faucet enabled."""
    ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
    tracker = TrackerServer(
        billing=ledger,
        accounts=AccountStore(),
        hive_secret=HIVE_SECRET,
        starting_credit=1.0,
        devnet_topup_amount=10.0,
    )
    port = tracker.start()
    yield f"http://127.0.0.1:{port}", ledger
    tracker.stop()


def test_caller_credit_granted_once_per_account(funded_tracker):
    "Caller credit granted once per account\n\nTags: accounts, auth, http"

    url, ledger = funded_tracker
    reg = _call(f"{url}/v1/auth/register", "POST",
                {"email": "c@example.com", "password": "secret-123"})
    token = reg["session_token"]
    first_key = reg["api_key"]
    assert ledger.get_client_balance(first_key) == pytest.approx(1.0)

    # A second key never re-grants — not even after revoking the first.
    second = _call(f"{url}/v1/account/keys", "POST", {}, token=token)
    assert second["caller_credit_granted"] is False
    assert ledger.get_client_balance(second["api_key"]) == pytest.approx(0.0)
    _call(f"{url}/v1/account/keys/revoke", "POST", {"api_key": first_key}, token=token)
    third = _call(f"{url}/v1/account/keys", "POST", {}, token=token)
    assert third["caller_credit_granted"] is False
    assert ledger.get_client_balance(third["api_key"]) == pytest.approx(0.0)


def test_unknown_bearer_key_rejected_by_proxy(funded_tracker):
    "Unknown bearer key rejected by proxy\n\nTags: accounts, auth, http"

    url, ledger = funded_tracker
    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/chat/completions", "POST",
              {"model": "any", "messages": []}, token="sk-mesh-made-up-key")
    assert exc_info.value.code == 401
    assert "unknown API key" in exc_info.value.read().decode()
    # The invented key must not have become a billable client.
    assert ledger.get_client_balance("sk-mesh-made-up-key") == pytest.approx(0.0)


def test_devnet_topup_credits_own_key_only(funded_tracker):
    "Devnet topup credits own key only\n\nTags: accounts, auth, http"

    url, ledger = funded_tracker
    owner = _call(f"{url}/v1/auth/register", "POST",
                  {"email": "own@example.com", "password": "secret-123"})
    other = _call(f"{url}/v1/auth/register", "POST",
                  {"email": "oth@example.com", "password": "secret-123"})

    me = _call(f"{url}/v1/account", token=owner["session_token"])
    assert me["topup_amount"] == pytest.approx(10.0)

    result = _call(f"{url}/v1/account/topup", "POST",
                   {"api_key": owner["api_key"]}, token=owner["session_token"])
    assert result["credited"] == pytest.approx(10.0)
    assert result["balance"] == pytest.approx(11.0)  # 1.0 caller credit + 10.0

    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/account/topup", "POST",
              {"api_key": owner["api_key"]}, token=other["session_token"])
    assert exc_info.value.code == 403
    assert ledger.get_client_balance(owner["api_key"]) == pytest.approx(11.0)


def test_topup_404_when_disabled(account_tracker):
    "Topup 404 when disabled\n\nTags: accounts, auth, http"

    url, _ = account_tracker
    reg = _call(f"{url}/v1/auth/register", "POST",
                {"email": "t@example.com", "password": "secret-123"})
    me = _call(f"{url}/v1/account", token=reg["session_token"])
    assert me["topup_amount"] == pytest.approx(0.0)
    with pytest.raises(urllib.error.HTTPError) as exc_info:
        _call(f"{url}/v1/account/topup", "POST",
              {"api_key": reg["api_key"]}, token=reg["session_token"])
    assert exc_info.value.code == 404
