"""Shared fixtures for the BW-Auth conformance suite.

STANDARD-OWNED: this file (and the test_*.py conformance files) are refreshed by
`new-bw-app.sh` on every run, like the vendored kit — do not edit them in an app.
App-specific fixtures/helpers go in `tests/app_conftest.py` (created by you, never
overwritten); it is imported at the bottom of this file when present.

Runs the app fully in-process against a throwaway SQLite accounts store in a temp
dir — no database container, no network. Every outbound BW call goes through the
one funnel `bw_auth._post_json`, which the autouse `bw_calls` fixture replaces with
a recorder, so (a) no test can touch the network and (b) a test can assert WHAT
would have been sent without anything being sent.

The contract the test files rely on (kept stable so the pack is portable):

  fixtures
    client            — a TestClient with the app's lifespan run (schema + seed).
    bw_calls          — list[(path, payload)] of every outbound BW call, recorded.
    kit               — a namespace over the accounts kit for arranging state as
                        the OWNER: kit.member(), kit.instance(), kit.grant(),
                        kit.set_level(), kit.set_all(), plus kit.bwa and kit.owner.

  helpers (import from conftest / tests package)
    as_user(client, username)      — sign `client` in as `username`.
    as_session(client, **session)  — sign in with an arbitrary session dict.

  constants
    OWNER, HAS_INSTANCES           — from bw_config, so a test can skip cleanly
                                     (e.g. instance tests when HAS_INSTANCES is False).
"""

import base64
import json
import os
import tempfile
from pathlib import Path
from types import SimpleNamespace

import pytest

# --- environment MUST be set before importing any app.* module ---------------
# app.config reads these at import; app.main wires the kit in its lifespan.
os.environ.setdefault("APP_DATA_DIR", tempfile.mkdtemp(prefix="bw-app-test-"))
os.environ.setdefault("APP_SESSION_SECRET", "test-session-secret")
os.environ.setdefault("APP_ENV", "test")
# BW Auth must LOOK configured so the invite/reset/report/search paths actually
# run — but no call may leave the process (the bw_calls stub below funnels them).
os.environ.setdefault("BW_CLIENT_ID", "t-client")
os.environ.setdefault("BW_CLIENT_SECRET", "t-secret")
os.environ.setdefault("BW_AUTH", "https://auth.bowden.works")
os.environ.setdefault("BW_APP_DOMAIN", "https://widgets.bowden.works")

from fastapi.testclient import TestClient  # noqa: E402
from itsdangerous import TimestampSigner  # noqa: E402

import app.main as main  # noqa: E402
from app import accounts  # noqa: E402
from app import bw_accounts as bwa  # noqa: E402
from app import bw_config  # noqa: E402
from app.config import get_settings  # noqa: E402

OWNER = bw_config.OWNER
HAS_INSTANCES = bw_config.HAS_INSTANCES


# --- fresh store per test ----------------------------------------------------

@pytest.fixture(autouse=True)
def _fresh_store():
    """Wipe the accounts store and re-seed levels before each test, so tests never
    leak state into one another. Runs before the `client` fixture's lifespan."""
    dbfile = Path(get_settings().accounts_db)
    if dbfile.exists():
        dbfile.unlink()
    accounts.init_accounts_kit()
    yield


# --- outbound BW calls: stubbed and recorded ---------------------------------

# The accounts that "exist centrally" in the stubbed BW directory. userinfo
# succeeds only for these (raising for anyone else, mirroring the real 404), so
# tests can exercise BOTH invite branches: an existing account (membership only,
# no credential email) vs a brand-new user (create + set-password invite email).
CENTRAL_USERS = {"alice", "aline", "bob", "carol", "dup", "existing"}


@pytest.fixture(autouse=True)
def bw_calls(monkeypatch):
    calls: list[tuple[str, dict]] = []

    import app.bw_auth as bw_auth_module

    def fake_post_json(path, payload):
        calls.append((path, dict(payload)))
        if path == "/app/userinfo":
            u = payload.get("username", "")
            if u not in CENTRAL_USERS:
                raise bw_auth_module.BWAuthError(f"unknown user '{u}' (stub 404)")
            return {"username": u, "email": f"{u}@example.com", "first": "T", "last": "User"}
        if path == "/app/reset-password":
            # The gateway replaces the password and returns it ONCE.
            return {"success": True, "user": payload.get("username", ""),
                    "password": "newp-2345-abcd-6789"}
        if path == "/app/notify-added":
            return {"success": True, "emailed_to": "t***@example.com"}
        if path == "/app/invite-user" and payload.get("delivery") == "password":
            # Manual handoff: the gateway creates the account and returns a
            # generated password ONCE; no email is sent.
            return {"success": True, "created": True, "delivery": "password",
                    "user": payload.get("username", ""), "class": "external",
                    "password": "wxyz-2345-abcd-6789"}
        if path == "/app/invite-user":
            return {"success": True, "created": True,
                    "user": {"username": payload.get("username", "")},
                    "emailed_to": payload.get("email", "")}
        if path == "/app/search-users":
            q = (payload.get("q") or "").lower()
            hits = [u for u in ("alice", "aline", "bob") if u.startswith(q)]
            return {"success": True,
                    "users": [{"username": u, "email": f"{u}@example.com",
                               "first": u.title(), "last": "", "class": "external"}
                              for u in hits]}
        if path == "/app/user-access":
            return {"success": True, "access": []}
        return {"success": True}

    monkeypatch.setattr(bw_auth_module, "_post_json", fake_post_json)
    return calls


# --- client / session --------------------------------------------------------

@pytest.fixture
def client():
    # base_url MUST be https: SessionMiddleware sets https_only=True (correct — the
    # app only ever runs behind Caddy/TLS), so the session cookie is Secure and
    # httpx will not resend it over http. A View As start/stop mutates the session;
    # without the https scheme the NEXT request silently falls back to the stale
    # pre-mutation cookie and the impersonation looks like it never happened.
    with TestClient(main.app, raise_server_exceptions=False,
                    base_url="https://testserver") as test_client:
        yield test_client


def session_cookie(**session) -> str:
    """Mint a cookie value byte-for-byte compatible with Starlette's
    SessionMiddleware, so tests can sign in without running the OAuth flow."""
    signer = TimestampSigner(str(get_settings().session_secret))
    payload = base64.b64encode(json.dumps(session).encode("utf-8"))
    return signer.sign(payload).decode("utf-8")


def as_user(client, username: str) -> None:
    """Sign `client` in as `username` for subsequent requests. Clears the jar first
    so a directly-set cookie can never coexist with a server-set one under the same
    name (which name the server then reads back would be ambiguous)."""
    client.cookies.jar.clear()
    client.cookies.set(get_settings().session_cookie, session_cookie(user=username))


def as_session(client, **session) -> None:
    """Sign `client` in with an arbitrary session dict (e.g. a pre-set view-as)."""
    client.cookies.jar.clear()
    client.cookies.set(get_settings().session_cookie, session_cookie(**session))


# --- arranging kit state as the owner ----------------------------------------

@pytest.fixture
def kit():
    """Arrange accounts/levels/instances directly as the OWNER (bypassing the API),
    so a test can set up the world it wants to exercise, then hit the API as a
    non-owner. Everything here goes through the same kit the API uses."""

    def member(username, level, all_instances=False):
        bwa.add_member(OWNER, username, level, all_instances=all_instances)

    def instance(iid, label=None):
        bwa.create_instance(OWNER, iid, label or iid)

    def grant(username, iid, level):
        bwa.grant_instance(OWNER, username, iid, level)

    def set_level(username, level):
        bwa.set_member_level(OWNER, username, level)

    def set_all(username, value):
        bwa.set_all_instances(OWNER, username, value)

    return SimpleNamespace(
        bwa=bwa, owner=OWNER,
        member=member, instance=instance, grant=grant,
        set_level=set_level, set_all=set_all,
    )


# --- app extension point ------------------------------------------------------
# App-specific fixtures live in tests/app_conftest.py (yours, never refreshed).
# Importing * here registers its fixtures suite-wide.
try:
    from tests.app_conftest import *  # noqa: F401,F403
except ImportError:
    pass
