"""View As: server-side authorization on start, per-request fail-closed
re-verify, signed-cookie state, read-only default, and hub oversight
reporting — the drop-in mechanism + volleyboard's M0 policy
(admin rian or a lead may impersonate; never a peer/superior)."""

from app.services.auth import bw_auth
from app.services.auth import session as auth_session
from tests.conftest import make_lead, make_player, mint_client_for


def _roster(db_session):
    """rian (admin), erin + adi (leads), zeina (plain player)."""
    players = {
        "rian": make_player(db_session, "rian", display_name="Rian"),
        "erin": make_player(db_session, "erin", display_name="Erin"),
        "adi": make_player(db_session, "adi", display_name="Adi"),
        "zeina": make_player(db_session, "zeina", display_name="Zeina"),
    }
    make_lead(db_session, players["erin"], match_slug="m-erin")
    make_lead(db_session, players["adi"], match_slug="m-adi")
    return players


# ------------------------------------------------------------- happy path


def test_admin_starts_and_stops_view_as(authed_client, db_session, monkeypatch):
    _roster(db_session)
    reported = []
    monkeypatch.setattr(
        bw_auth,
        "report_impersonation",
        lambda real, target, mode="readonly", active=True: reported.append(
            (real, target, mode, active)
        ),
    )

    resp = authed_client.post("/view-as/start", json={"target": "zeina"})
    assert resp.status_code == 200
    body = resp.json()
    assert body["ok"] is True
    assert body["viewing_as"] == "zeina"
    assert body["mode"] == "readonly"
    assert authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)
    assert reported[-1] == ("rian", "zeina", "readonly", True)

    me = authed_client.get("/api/me").json()
    assert me["username"] == "rian"          # identity stays the real user
    assert me["impersonating"] is True
    assert me["viewing_as"] == "zeina"
    assert me["viewing_as_label"] == "Zeina"
    assert me["real_user"] == "rian"
    assert me["can_write"] is False          # read-only default
    # the /api/me heartbeat re-reported the active impersonation
    assert reported[-1] == ("rian", "zeina", "readonly", True)

    resp = authed_client.post("/view-as/stop")
    assert resp.status_code == 200
    assert reported[-1] == ("rian", "zeina", "readonly", False)
    assert not authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)
    assert authed_client.get("/api/me").json()["impersonating"] is False


def test_stop_get_redirects_for_banner_link(authed_client, db_session):
    _roster(db_session)
    authed_client.post("/view-as/start", json={"target": "zeina"})
    resp = authed_client.get("/view-as/stop", follow_redirects=False)
    assert resp.status_code == 302
    assert resp.headers["location"] == "/"
    assert not authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)


def test_lead_may_view_plain_player(client, db_session):
    _roster(db_session)
    mint_client_for(client, "erin")
    resp = client.post("/view-as/start", json={"target": "zeina"})
    assert resp.status_code == 200
    me = client.get("/api/me").json()
    assert me["viewing_as"] == "zeina"
    assert me["view_as_targets"]  # leads get the picker list


def test_me_offers_targets_only_to_admins_and_leads(client, db_session):
    _roster(db_session)
    mint_client_for(client, "rian")
    targets = {t["username"] for t in client.get("/api/me").json()["view_as_targets"]}
    assert targets == {"erin", "adi", "zeina"}  # everyone but self
    mint_client_for(client, "zeina")
    assert client.get("/api/me").json()["view_as_targets"] == []


# ------------------------------------------------------------- refusals


def test_plain_player_forbidden(client, db_session):
    _roster(db_session)
    mint_client_for(client, "zeina")
    resp = client.post("/view-as/start", json={"target": "erin"})
    assert resp.status_code == 403
    assert resp.json()["error_code"] == "FORBIDDEN"
    assert not client.cookies.get(auth_session.VIEW_AS_COOKIE)


def test_lead_cannot_view_peer_or_superior(client, db_session):
    _roster(db_session)
    mint_client_for(client, "erin")
    for peer_or_up in ("adi", "rian"):  # fellow lead / the admin
        resp = client.post("/view-as/start", json={"target": peer_or_up})
        assert resp.status_code == 403
        assert resp.json()["error_code"] == "ESCALATION"


def test_unknown_target_404(authed_client, db_session):
    _roster(db_session)
    resp = authed_client.post("/view-as/start", json={"target": "ghost"})
    assert resp.status_code == 404
    assert resp.json()["error_code"] == "TARGET_NOT_FOUND"


def test_self_target_400(authed_client, db_session):
    _roster(db_session)
    resp = authed_client.post("/view-as/start", json={"target": "rian"})
    assert resp.status_code == 400
    assert resp.json()["error_code"] == "BAD_TARGET"


def test_nesting_refused(authed_client, db_session):
    _roster(db_session)
    assert authed_client.post("/view-as/start", json={"target": "zeina"}).status_code == 200
    resp = authed_client.post("/view-as/start", json={"target": "erin"})
    assert resp.status_code == 409
    assert resp.json()["error_code"] == "NESTED"
    # the original impersonation is untouched
    assert authed_client.get("/api/me").json()["viewing_as"] == "zeina"


# ------------------------------------------------- forgery + fail-closed


def test_forged_or_foreign_cookie_impersonates_nobody(authed_client, db_session):
    _roster(db_session)
    # Garbage cookie.
    authed_client.cookies.set(auth_session.VIEW_AS_COOKIE, "garbage|junk")
    assert authed_client.get("/api/me").json()["impersonating"] is False
    # A REAL token minted for a different real user (replay across users).
    foreign = auth_session.mint_view_as("erin", "zeina")
    authed_client.cookies.set(auth_session.VIEW_AS_COOKIE, foreign)
    assert authed_client.get("/api/me").json()["impersonating"] is False


def test_deleted_target_fail_closes_and_drops_cookie(authed_client, db_session):
    players = _roster(db_session)
    assert authed_client.post("/view-as/start", json={"target": "zeina"}).status_code == 200
    db_session.delete(players["zeina"])
    db_session.commit()

    me = authed_client.get("/api/me")
    assert me.json()["impersonating"] is False   # verify() auto-stopped
    assert not authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)  # cookie dropped


def test_revoked_privilege_fail_closes(client, db_session):
    """A lead whose lead-ship ends mid-impersonation drops back to
    themselves on the very next request (continuous re-auth)."""
    players = _roster(db_session)
    mint_client_for(client, "erin")
    assert client.post("/view-as/start", json={"target": "zeina"}).status_code == 200
    assert client.get("/api/me").json()["impersonating"] is True

    # Erin stops being a boundary player -> no longer a lead.
    from sqlalchemy import update

    from app.models import TeamNode

    db_session.execute(
        update(TeamNode)
        .where(TeamNode.boundary_player_id == players["erin"].id)
        .values(boundary_player_id=None)
    )
    db_session.commit()

    me = client.get("/api/me").json()
    assert me["impersonating"] is False
    assert me["username"] == "erin"
    assert not client.cookies.get(auth_session.VIEW_AS_COOKIE)


def test_logout_clears_view_as_cookie(authed_client, db_session):
    _roster(db_session)
    authed_client.post("/view-as/start", json={"target": "zeina"})
    assert authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)
    resp = authed_client.get("/auth/logout", follow_redirects=False)
    # The app-set view-as cookie is gone from the jar...
    assert not authed_client.cookies.get(auth_session.VIEW_AS_COOKIE)
    # ...and the response explicitly expires BOTH cookies (the session
    # cookie in the jar was fixture-set with a bare domain, so assert the
    # deletion header rather than httpx's domain-scoped jar state).
    deletions = [
        c for c in resp.headers.get_list("set-cookie") if "Max-Age=0" in c or "max-age=0" in c
    ]
    assert any(c.startswith(f"{auth_session.SESSION_COOKIE}=") for c in deletions)
    assert any(c.startswith(f"{auth_session.VIEW_AS_COOKIE}=") for c in deletions)


def test_view_as_default_is_read_only_for_writes(authed_client, db_session):
    """VIEW-AS.md: read-only default. While impersonating, mutating API
    calls 403 (VIEW_AS_READ_ONLY); stopping restores writes. The guard is
    middleware-level so no router needs to remember it."""
    from app import constants
    from app.models import Ball, Court, Match, Side, TeamMember, TeamNode
    from tests.conftest import make_player

    rian = make_player(db_session, "rian", display_name="Rian")
    zeina = make_player(db_session, "zeina", display_name="Zeina")
    match = Match(slug="vamatch", name="VA Match")
    db_session.add(match)
    db_session.flush()
    side = Side(match_id=match.id, name="Bowden Works", position=1)
    db_session.add(side)
    db_session.flush()
    node = TeamNode(side_id=side.id, name="Front line", boundary_player_id=rian.id)
    db_session.add(node)
    db_session.flush()
    db_session.add(TeamMember(match_id=match.id, node_id=node.id, player_id=rian.id))
    db_session.add(TeamMember(match_id=match.id, node_id=node.id, player_id=zeina.id))
    court = Court(
        match_id=match.id, config_key="issues", name="Issues", position=0,
        config=constants.COURT_CONFIGS["issues"],
    )
    db_session.add(court)
    db_session.flush()
    ball = Ball(court_id=court.id, title="Guard test", status="Open",
                category="Revision", author_id=rian.id)
    db_session.add(ball)
    db_session.commit()

    start = authed_client.post("/view-as/start", json={"target": "zeina"})
    assert start.status_code == 200, start.text

    blocked = authed_client.patch(f"/api/balls/{ball.id}", json={"status": "Addressing"})
    assert blocked.status_code == 403
    assert blocked.json()["error_code"] == "VIEW_AS_READ_ONLY"

    # Reads still work while impersonating.
    board = authed_client.get("/api/matches/vamatch/board")
    assert board.status_code == 200

    stop = authed_client.post("/view-as/stop")
    assert stop.status_code == 200

    allowed = authed_client.patch(f"/api/balls/{ball.id}", json={"status": "Addressing"})
    assert allowed.status_code == 200, allowed.text
