from src.domain.project_auth.reconciliation import (
    build_reconciliation_plan,
    is_stale_plan,
    plan_as_dict,
)
from src.domain.project_auth.phase5b_apply import ApplyRefused, execute_bounded_plan
from src.domain.project_auth.staff_directory import WorkspaceAppIdentity


def identity(email: str, role: str, owner: bool = False) -> WorkspaceAppIdentity:
    return WorkspaceAppIdentity(
        email=email,
        name="redacted",
        workspace_role="owner" if owner else "admin",
        app_role=role,
        is_owner=owner,
    )


def test_plan_only_changes_proven_workspace_identity_and_is_deterministic():
    plan = build_reconciliation_plan(
        project_id="p1",
        workspace_id="w1",
        identities=[identity("owner@example.test", "super_admin", True)],
        cloud_users=[
            {"id": "cloud-owner", "email": "owner@example.test", "role": "member", "user_metadata": {"source": "workspace_owner"}},
            {"id": "customer", "email": "customer@example.test", "role": "member", "user_metadata": {}},
        ],
    )
    assert len(plan.operations) == 1
    assert plan.operations[0].field == "role"
    assert plan.operations[0].before == "member"
    assert plan.operations[0].after == "super_admin"
    assert plan.operations[0].endpoint == "POST /v1/projects/p1/auth/users"
    assert "/role" not in plan.operations[0].endpoint
    assert plan.blocked == ()
    assert plan == build_reconciliation_plan(
        project_id="p1",
        workspace_id="w1",
        identities=[identity("owner@example.test", "super_admin", True)],
        cloud_users=[
            {"id": "cloud-owner", "email": "owner@example.test", "role": "member", "user_metadata": {"source": "workspace_owner"}},
            {"id": "customer", "email": "customer@example.test", "role": "member", "user_metadata": {}},
        ],
    )


def test_missing_identity_is_an_upsert_and_customer_is_not_stale():
    plan = build_reconciliation_plan(
        project_id="p1",
        workspace_id="w1",
        identities=[identity("owner@example.test", "super_admin", True)],
        cloud_users=[],
    )
    assert plan.operations[0].field == "identity"
    assert plan.operations[0].before == "absent"
    assert plan_as_dict(plan)["operations"][0]["stable_identity"]
    assert not plan.findings or plan.findings[0].startswith("missing_inherited:")


def test_same_email_without_provenance_blocks_instead_of_escalating_customer():
    plan = build_reconciliation_plan(
        project_id="p1",
        workspace_id="w1",
        identities=[identity("owner@example.test", "super_admin", True)],
        cloud_users=[{"id": "customer", "email": "owner@example.test", "role": "member", "user_metadata": {}}],
    )
    assert plan.operations == ()
    assert plan.blocked


def test_stale_plan_detection_and_noop_after_convergence():
    plan = build_reconciliation_plan(
        project_id="p1",
        workspace_id="w1",
        identities=[identity("owner@example.test", "super_admin", True)],
        cloud_users=[{"id": "cloud-owner", "email": "owner@example.test", "role": "member", "user_metadata": {"source": "workspace_owner"}}],
    )
    assert is_stale_plan(plan, current_cloud_users=[{"id": "cloud-owner", "email": "owner@example.test", "role": "admin", "user_metadata": {"source": "workspace_owner"}}])
    converged = [{"id": "cloud-owner", "email": "owner@example.test", "role": "super_admin", "user_metadata": {"source": "workspace_owner"}}]
    second = build_reconciliation_plan(
        project_id="p1", workspace_id="w1", identities=[identity("owner@example.test", "super_admin", True)], cloud_users=converged
    )
    assert second.operations == ()
    assert second.findings == ()


def test_bounded_apply_converges_and_second_run_is_noop():
    import asyncio

    identities = [identity("owner@example.test", "super_admin", True)]
    users = [{"id": "cloud-owner", "email": "owner@example.test", "role": "member", "user_metadata": {"source": "workspace_owner"}}]
    plan = build_reconciliation_plan(project_id="p1", workspace_id="w1", identities=identities, cloud_users=users)

    async def run():
        async def mutate(operation):
            users[0]["role"] = operation.after
            users[0]["user_metadata"]["workspace_role"] = "owner"

        async def read():
            return users

        def replan(current):
            return build_reconciliation_plan(project_id="p1", workspace_id="w1", identities=identities, cloud_users=current)

        return await execute_bounded_plan(plan=plan, approved_project_ids={"p1"}, current_cloud_users=list(users), mutate=mutate, read_cloud_users=read, replan=replan)

    result = asyncio.run(run())
    assert result.status == "CONVERGED"
    assert result.second_run == "NO_OP"


def test_bounded_apply_rejects_unauthorized_project_and_stale_plan():
    import asyncio

    plan = build_reconciliation_plan(project_id="p1", workspace_id="w1", identities=[identity("owner@example.test", "super_admin", True)], cloud_users=[])

    async def run():
        async def never(_):
            raise AssertionError("mutation must not run")
        async def read():
            return []
        with_error = None
        try:
            await execute_bounded_plan(plan=plan, approved_project_ids={"other"}, current_cloud_users=[], mutate=never, read_cloud_users=read, replan=lambda current: plan)
        except ApplyRefused as exc:
            with_error = str(exc)
        return with_error

    assert asyncio.run(run()) == "project_not_allowlisted"


def test_bounded_apply_stops_on_readback_mismatch():
    import asyncio

    identities = [identity("owner@example.test", "super_admin", True)]
    users = [{"id": "cloud-owner", "email": "owner@example.test", "role": "member", "user_metadata": {"source": "workspace_owner"}}]
    plan = build_reconciliation_plan(project_id="p1", workspace_id="w1", identities=identities, cloud_users=users)

    async def run():
        async def mutate(_):
            pass
        async def read():
            return users
        return await execute_bounded_plan(plan=plan, approved_project_ids={"p1"}, current_cloud_users=list(users), mutate=mutate, read_cloud_users=read, replan=lambda current: plan)

    result = asyncio.run(run())
    assert result.status == "RECONCILIATION_FAILED"
    assert result.operations[0]["status"] == "readback_mismatch"
