"""View As — identity follows the effective user, writes are gated, revocation is
fail-closed.

The rule the whole app leans on: DATA and PERMISSIONS follow the EFFECTIVE
(impersonated) user, while IDENTITY, audit, and the write-guard follow the REAL
user. These tests pin: starting a view-as re-points identity but not the real user;
read-only mode structurally blocks any state change (the middleware, not each
route); every start/stop is reported to central oversight; act-mode is owner-only;
and an impersonation that is no longer authorized auto-stops on the very next
request (fail-closed), no explicit "stop" needed.
"""

import pytest

from tests.conftest import HAS_INSTANCES, as_user


def _code(resp):
    body = resp.json()
    d = body.get("detail")
    return d.get("error_code") if isinstance(d, dict) else body.get("error_code")


def _impersonation_calls(bw_calls):
    return [payload for path, payload in bw_calls
            if path == "/app/report-impersonation"]


def _a_mutation(client):
    """A representative write request that should be blocked in read-only view-as.
    Uses POST /api/instances when the app has instances, else POST /api/bw/levels."""
    if HAS_INSTANCES:
        return client.post("/api/instances", json={"id": "should-be-blocked"})
    return client.post("/api/bw/levels", json={"name": "should-be-blocked"})


def test_view_as_repoints_identity_not_real_user(client, kit):
    kit.member("bob", "member")
    as_user(client, "rian")

    r = client.post("/api/bw/view-as/start", json={"target": "bob"})
    assert r.status_code == 200

    who = client.get("/api/whoami").json()
    assert who["username"] == "bob"        # effective identity
    assert who["real_user"] == "rian"      # the real signed-in user is unchanged
    assert who["is_owner"] is False        # is_owner tracks the EFFECTIVE user


def test_me_reflects_impersonation_and_readonly(client, kit):
    kit.member("bob", "member")
    as_user(client, "rian")
    client.post("/api/bw/view-as/start", json={"target": "bob"})

    me = client.get("/api/bw/me").json()
    assert me["impersonating"] is True
    assert me["viewing_as"] == "bob"
    assert me["real_user"] == "rian"
    assert me["view_as_mode"] == "readonly"
    assert me["can_write"] is False         # read-only mode


def test_start_and_stop_are_reported_to_central(client, kit, bw_calls):
    kit.member("bob", "member")
    as_user(client, "rian")

    client.post("/api/bw/view-as/start", json={"target": "bob"})
    starts = _impersonation_calls(bw_calls)
    assert any(p["real"] == "rian" and p["target"] == "bob" and p["active"] is True
               for p in starts)

    client.post("/api/bw/view-as/stop")
    stops = _impersonation_calls(bw_calls)
    assert any(p["real"] == "rian" and p["target"] == "bob" and p["active"] is False
               for p in stops)


def test_readonly_view_as_blocks_a_mutation(client, kit):
    """The middleware refuses any state-changing method (except stop/logout) while
    impersonating read-only — structurally, before the route runs."""
    kit.member("bob", "member")
    as_user(client, "rian")
    client.post("/api/bw/view-as/start", json={"target": "bob"})

    r = _a_mutation(client)
    assert r.status_code == 403
    assert _code(r) == "VIEW_AS_READ_ONLY"


def test_stop_returns_identity_to_the_real_user(client, kit):
    kit.member("bob", "member")
    as_user(client, "rian")
    client.post("/api/bw/view-as/start", json={"target": "bob"})
    assert client.get("/api/whoami").json()["username"] == "bob"

    client.post("/api/bw/view-as/stop")
    who = client.get("/api/whoami").json()
    assert who["username"] == "rian"
    assert who["real_user"] == "rian"
    assert who["is_owner"] is True


def test_act_mode_is_owner_only(client, kit):
    """A non-owner who legitimately holds accounts.view_as (the seed admin does) may
    view-as read-only, but act-mode is refused — acting writes real rows under
    another user's name and is an owner-only testing tool."""
    kit.member("alice", "admin")           # admin carries accounts.view_as
    kit.member("bob", "member")
    as_user(client, "alice")

    r = client.post("/api/bw/view-as/start",
                    json={"target": "bob", "mode": "act"})
    assert r.status_code == 403
    assert _code(r) == "FORBIDDEN"


def test_non_owner_may_start_readonly_view_as(client, kit):
    """Companion to the act-mode test: the same admin CAN start a read-only view-as,
    so the refusal above is specifically about act-mode, not view-as itself."""
    kit.member("alice", "admin")
    kit.member("bob", "member")
    as_user(client, "alice")

    r = client.post("/api/bw/view-as/start", json={"target": "bob"})
    assert r.status_code == 200
    assert client.get("/api/whoami").json()["username"] == "bob"


def test_fail_closed_autostop_when_impersonator_ability_revoked(client, kit):
    """An admin starts a view-as; the owner then demotes the admin to a plain member
    (revoking accounts.view_as). On the admin's NEXT request the impersonation is
    re-checked, found no longer authorized, and auto-dropped — no explicit stop."""
    kit.member("alice", "admin")
    kit.member("bob", "member")
    as_user(client, "alice")
    client.post("/api/bw/view-as/start", json={"target": "bob"})
    assert client.get("/api/whoami").json()["username"] == "bob"

    # Owner revokes alice's ability out from under the live impersonation.
    kit.set_level("alice", "member")

    who = client.get("/api/whoami").json()
    assert who["username"] == "alice"       # dropped back to the real user
    assert client.get("/api/bw/me").json()["impersonating"] is False


def test_fail_closed_autostop_when_target_deleted(client, kit):
    """The other fail-closed path: the owner impersonates a member, the member is
    then removed → target_valid fails on the next request → auto-stop."""
    kit.member("bob", "member")
    as_user(client, "rian")
    client.post("/api/bw/view-as/start", json={"target": "bob"})
    assert client.get("/api/whoami").json()["username"] == "bob"

    kit.bwa.remove_member(kit.owner, "bob")  # target no longer a member

    who = client.get("/api/whoami").json()
    assert who["username"] == "rian"
    assert client.get("/api/bw/me").json()["impersonating"] is False
