"""The full callback flow against a stubbed token exchange (mirrors
with's test_auth_flow.py): state/PKCE checks, forged tokens rejected
with no session, and the column-scoped Player provisioning contract.

Only bw_auth.exchange_code (the network hop to id-auth) is stubbed —
state comparison, PKCE cookie handling, id_token verification, the DB
upsert, and the session mint all run for real.
"""

import urllib.parse

from sqlalchemy import select

from app.models import Player
from app.services.auth import bw_auth
from app.services.auth import session as auth_session
from tests.test_auth_tokens import make_id_token


def start_login(client, next_path: str | None = None):
    """GET /auth/login and return the state id-auth would echo back."""
    url = "/auth/login" + (f"?next={urllib.parse.quote(next_path)}" if next_path else "")
    resp = client.get(url, follow_redirects=False)
    assert resp.status_code == 302
    location = resp.headers["location"]
    assert location.startswith("https://bw-auth.invalid/app-authorize?")
    params = dict(urllib.parse.parse_qsl(urllib.parse.urlsplit(location).query))
    assert params["client_id"] == "voll-test"
    assert params["code_challenge_method"] == "S256"
    assert params["redirect_uri"] == "https://testserver/auth/callback"
    assert "prompt" not in params  # interactive, never a silent probe
    # The flow cookies landed in the jar.
    assert client.cookies.get(bw_auth.PKCE_COOKIE)
    assert client.cookies.get(bw_auth.STATE_COOKIE)
    return params["state"]


def stub_exchange(monkeypatch, client, id_token: str, expected_code: str = "test-code"):
    """Stub the server-to-server /app-token hop; assert the app presents
    the code and the SAME PKCE verifier it parked in the cookie."""
    expected_verifier = client.cookies.get(bw_auth.PKCE_COOKIE)

    def fake_exchange(code, code_verifier, redirect_uri=None):
        assert code == expected_code
        assert code_verifier == expected_verifier
        return {"identity": {"username": "adi"}, "id_token": id_token}

    monkeypatch.setattr(bw_auth, "exchange_code", fake_exchange)


def complete_login(client, monkeypatch, **token_overrides):
    state = start_login(client)
    stub_exchange(monkeypatch, client, make_id_token(**token_overrides))
    return client.get(
        f"/auth/callback?code=test-code&state={state}", follow_redirects=False
    )


# ------------------------------------------------------- happy path + session


def test_login_callback_establishes_session(client, monkeypatch):
    resp = complete_login(client, monkeypatch)
    assert resp.status_code == 302
    assert resp.headers["location"] == "/"
    assert client.cookies.get(auth_session.SESSION_COOKIE)

    me = client.get("/api/me")
    assert me.status_code == 200
    body = me.json()
    assert body["authenticated"] is True
    assert body["username"] == "adi"
    assert body["email"] == "adi@example.com"
    assert body["impersonating"] is False
    assert body["real_user"] == "adi"
    assert body["can_write"] is True
    # adi has no team yet and is not the app admin.
    assert body["role"] == "player"
    assert body["is_admin"] is False
    assert body["view_as_targets"] == []


def test_next_destination_survives_the_round_trip(client, monkeypatch):
    state = start_login(client, next_path="/some/page?week=27")
    stub_exchange(monkeypatch, client, make_id_token())
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert resp.status_code == 302
    assert resp.headers["location"] == "/some/page?week=27"


def test_next_never_redirects_off_origin(client, monkeypatch):
    for evil in ("https://evil.example", "//evil.example", "\\\\evil"):
        client.cookies.delete(auth_session.SESSION_COOKIE)
        state = start_login(client, next_path=evil)
        stub_exchange(monkeypatch, client, make_id_token())
        resp = client.get(
            f"/auth/callback?code=test-code&state={state}", follow_redirects=False
        )
        assert resp.status_code == 302
        assert resp.headers["location"] == "/"


def test_logout_clears_session_and_links_idauth_logout(client, monkeypatch):
    complete_login(client, monkeypatch)
    resp = client.get("/auth/logout", follow_redirects=False)
    assert resp.status_code == 302
    assert resp.headers["location"] == (
        "https://bw-auth.invalid/logout?redirect=https%3A%2F%2Ftestserver"
    )
    assert client.get("/api/me").status_code == 401  # app session is gone


# ------------------------------------------------ callback rejection paths


def test_state_mismatch_rejected(client):
    start_login(client)
    resp = client.get("/auth/callback?code=test-code&state=WRONG", follow_redirects=False)
    assert resp.status_code == 400
    assert resp.json()["error_code"] == "AUTH_STATE_MISMATCH"
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_callback_without_flow_cookies_rejected(client):
    resp = client.get("/auth/callback?code=x&state=y", follow_redirects=False)
    assert resp.status_code == 400
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_callback_without_pkce_cookie_rejected(client):
    state = start_login(client)
    client.cookies.delete(bw_auth.PKCE_COOKIE)
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert resp.status_code == 400
    assert resp.json()["error_code"] == "AUTH_PKCE_MISSING"
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_forged_token_rejected_no_session(client, monkeypatch):
    # Signed with ANOTHER app's secret — signature check must fail.
    resp = complete_login(client, monkeypatch, secret="another-apps-secret")
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "SIGN_IN_FAILED"
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_wrong_audience_token_rejected(client, monkeypatch):
    resp = complete_login(client, monkeypatch, aud="someotherapp")
    assert resp.status_code == 401
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_expired_token_rejected(client, monkeypatch):
    import time as _time

    now = int(_time.time())
    resp = complete_login(client, monkeypatch, iat=now - 600, exp=now - 300)
    assert resp.status_code == 401
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_error_param_is_terminal_not_a_loop(client):
    start_login(client)
    resp = client.get("/auth/callback?error=access_denied", follow_redirects=False)
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "SIGN_IN_FAILED"


def test_expired_app_session_redirects_into_silent_reauth(client):
    expired = auth_session.mint_session(username="adi", now=0)  # exp long past
    client.cookies.set(auth_session.SESSION_COOKIE, expired)
    assert client.get("/api/me").status_code == 401
    resp = client.get("/", follow_redirects=False)
    assert resp.status_code == 302
    assert resp.headers["location"].startswith("/auth/login")


# ------------------------------------------------------------ auto-provision


def test_first_login_provisions_player_row(client, monkeypatch, db_session):
    complete_login(client, monkeypatch)
    players = db_session.scalars(select(Player)).all()
    assert len(players) == 1
    player = players[0]
    assert player.username == "adi"
    assert player.email == "adi@example.com"
    assert player.display_name == "Adi Example"  # seeded once from BW first/last
    assert player.avatar_color is None
    assert player.sound_settings == {}


def test_second_login_reuses_row_column_scoped(client, monkeypatch, db_session):
    complete_login(client, monkeypatch)

    # Between logins: the app/user customizes app-owned columns.
    player = db_session.scalar(select(Player).where(Player.username == "adi"))
    player.display_name = "Adi Custom"
    player.avatar_color = "#22c55e"
    player.sound_settings = {"muted": True}
    db_session.commit()

    client.cookies.delete(auth_session.SESSION_COOKIE)
    complete_login(client, monkeypatch, email="adi@newdomain.example")

    db_session.expire_all()
    players = db_session.scalars(select(Player)).all()
    assert len(players) == 1                                # reused, not duplicated
    player = players[0]
    assert player.email == "adi@newdomain.example"          # BW-derived: refreshed
    assert player.display_name == "Adi Custom"              # app-owned: untouched
    assert player.avatar_color == "#22c55e"                 # app-owned: untouched
    assert player.sound_settings == {"muted": True}         # app-owned: untouched
