"""The full callback flow against a stubbed token exchange:
T-AZ-040 (callback half — state/PKCE checks, forged tokens rejected
with no session) and the auto-provisioning contract (first login
creates a zero-capability users row; re-login reuses it column-scoped).

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

import urllib.parse
import uuid

import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models import User
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"] == "withtest"
    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))
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    return resp


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


def test_login_callback_establishes_session(client, monkeypatch, db_engine):
    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()
    # M1 contract (real identity, top-level) — M3 adds capabilities/
    # effective/view_as alongside (tests/routers cover those).
    assert body["username"] == "adi"
    assert body["email"] == "adi@example.com"
    assert body["is_super_admin"] is False
    # a fresh auto-provisioned user has zero capabilities
    assert not any(body["capabilities"].values())
    assert body["view_as"]["active"] is False


def test_next_destination_survives_the_round_trip(client, monkeypatch, db_engine):
    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, db_engine):
    for evil in ("https://evil.example", "//evil.example", "\\\\evil"):
        fresh = client
        fresh.cookies.delete(auth_session.SESSION_COOKIE)
        state = start_login(fresh, next_path=evil)
        stub_exchange(monkeypatch, fresh, make_id_token())
        resp = fresh.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, db_engine):
    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


# ------------------------------------------------- T-AZ-040 (callback half)


def test_t_az_040_state_mismatch_rejected(client, db_engine):
    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_t_az_040_callback_without_flow_cookies_rejected(client, db_engine):
    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_t_az_040_forged_token_rejected_no_session(client, monkeypatch, db_engine):
    # Signed with ANOTHER app's secret — signature check must fail.
    state = start_login(client)
    stub_exchange(monkeypatch, client, make_id_token(secret="another-apps-secret"))
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert resp.status_code == 401
    assert resp.json()["error_code"] == "SIGN_IN_FAILED"
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_t_az_040_wrong_audience_token_rejected(client, monkeypatch, db_engine):
    state = start_login(client)
    stub_exchange(monkeypatch, client, make_id_token(aud="someotherapp"))
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert resp.status_code == 401
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_t_az_040_expired_token_rejected(client, monkeypatch, db_engine):
    import time as _time

    now = int(_time.time())
    state = start_login(client)
    stub_exchange(monkeypatch, client, make_id_token(iat=now - 600, exp=now - 300))
    resp = client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert resp.status_code == 401
    assert not client.cookies.get(auth_session.SESSION_COOKIE)


def test_error_param_is_terminal_not_a_loop(client, db_engine):
    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, db_engine):
    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_zero_capability_row(client, monkeypatch, db_engine):
    complete_login(client, monkeypatch)
    with Session(db_engine) as s:
        users = s.scalars(select(User)).all()
        assert len(users) == 1
        user = users[0]
        assert user.idauth_username == "adi"
        assert user.email == "adi@example.com"
        assert user.is_super_admin is False   # zero capabilities...
        assert user.person_party_id is None   # ...and no party link
        assert user.preferences == {}
        assert isinstance(user.id, uuid.UUID)


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

    # Between logins: an owner grants capabilities + the user sets a pref.
    with Session(db_engine) as s:
        user = s.scalar(select(User).where(User.idauth_username == "adi"))
        user.is_super_admin = True
        user.preferences = {"theme": "dark"}
        s.commit()

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

    with Session(db_engine) as s:
        users = s.scalars(select(User)).all()
        assert len(users) == 1                          # reused, not duplicated
        user = users[0]
        assert user.email == "adi@newdomain.example"    # BW-derived: refreshed
        assert user.is_super_admin is True              # app-owned: untouched
        assert user.preferences == {"theme": "dark"}    # app-owned: untouched

    # The refreshed session reflects the granted capability.
    assert client.get("/api/me").json()["is_super_admin"] is True


def test_provisioning_failure_fails_closed_no_session(client, monkeypatch):
    # No db_engine fixture: get_engine raises (WITH_DB_URL is empty in the
    # unit env). The callback must fail closed — no session cookie minted.
    state = start_login(client)
    stub_exchange(monkeypatch, client, make_id_token())
    with pytest.raises(RuntimeError, match="WITH_DB_URL"):
        client.get(f"/auth/callback?code=test-code&state={state}", follow_redirects=False)
    assert not client.cookies.get(auth_session.SESSION_COOKIE)
