defmodule Mv.OidcRoleSyncTest do
  @moduledoc """
  Tests for OIDC group → Admin/Mitglied role sync (apply_admin_role_from_user_info/2).
  """
  use Mv.DataCase, async: false

  alias Mv.Accounts
  alias Mv.Accounts.User
  alias Mv.Authorization.Role
  alias Mv.OidcRoleSync
  require Ash.Query

  setup do
    ensure_roles_exist()
    restore_config = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "groups")
    on_exit(restore_config)
    :ok
  end

  describe "apply_admin_role_from_user_info/2" do
    test "when OIDC_ADMIN_GROUP_NAME not configured: does not change user (Mitglied stays)" do
      restore = put_oidc_config(admin_group_name: nil, groups_claim: "groups")
      on_exit(restore)

      email = "sync-no-config-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user} = create_user_with_mitglied(email)
      role_id_before = user.role_id
      user_info = %{"groups" => ["mila-admin"]}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info)

      {:ok, after_user} = get_user(user.id)
      assert after_user.role_id == role_id_before
    end

    test "when user_info contains configured admin group: user gets Admin role" do
      email = "sync-to-admin-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user} = create_user_with_mitglied(email)
      user_info = %{"groups" => ["mila-admin"]}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info)

      {:ok, after_user} = get_user(user.id)
      assert after_user.role_id == admin_role_id()
    end

    test "when user_info does not contain admin group: user gets Mitglied role" do
      email1 = "sync-to-mitglied-#{System.unique_integer([:positive])}@test.example.com"
      email2 = "other-admin-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user} = create_user_with_admin(email1)
      {:ok, _} = create_user_with_admin(email2)
      user_info = %{"groups" => ["other-group"]}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info)

      {:ok, after_user} = get_user(user.id)
      assert after_user.role_id == mitglied_role_id()
    end

    test "when OIDC_GROUPS_CLAIM is different: reads groups from that claim" do
      restore = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "ak_groups")
      on_exit(restore)

      email = "sync-claim-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user} = create_user_with_mitglied(email)
      user_info = %{"ak_groups" => ["mila-admin"]}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info)

      {:ok, after_user} = get_user(user.id)
      assert after_user.role_id == admin_role_id()
    end

    test "user already Admin and user_info without admin group: downgrade to Mitglied" do
      email1 = "sync-downgrade-#{System.unique_integer([:positive])}@test.example.com"
      email2 = "sync-other-admin-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user1} = create_user_with_admin(email1)
      {:ok, _user2} = create_user_with_admin(email2)
      user_info = %{"groups" => []}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user1, user_info)

      {:ok, after_user} = get_user(user1.id)
      assert after_user.role_id == mitglied_role_id()
    end

    test "when user_info has no groups, groups are read from access_token JWT (e.g. Rauthy)" do
      email = "sync-from-token-#{System.unique_integer([:positive])}@test.example.com"
      {:ok, user} = create_user_with_mitglied(email)
      user_info = %{"sub" => "oidc-123"}

      # Minimal JWT: header.payload.signature with "groups" in payload (Rauthy puts groups in access_token)
      payload = Jason.encode!(%{"groups" => ["mila-admin"], "sub" => "oidc-123"})
      payload_b64 = Base.url_encode64(payload, padding: false)
      header_b64 = Base.url_encode64("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", padding: false)
      sig_b64 = Base.url_encode64("sig", padding: false)
      access_token = "#{header_b64}.#{payload_b64}.#{sig_b64}"
      oauth_tokens = %{"access_token" => access_token}

      assert :ok = OidcRoleSync.apply_admin_role_from_user_info(user, user_info, oauth_tokens)

      {:ok, after_user} = get_user(user.id)
      assert after_user.role_id == admin_role_id()
    end
  end

  # B3: Role sync after registration is implemented via after_action in register_with_rauthy.
  # Full integration tests (create_register_with_rauthy + assert role) are skipped: when the
  # nested Ash.update! runs inside the create's after_action, authorization may evaluate in
  # the create context so set_role_from_oidc_sync bypass does not apply. Sync logic is covered
  # by the apply_admin_role_from_user_info tests above. B4 sign-in sync will also use that.

  defp ensure_roles_exist do
    for {name, perm} <- [{"Admin", "admin"}, {"Mitglied", "own_data"}] do
      case Role
           |> Ash.Query.filter(name == ^name)
           |> Ash.read_one(authorize?: false, domain: Mv.Authorization) do
        {:ok, nil} ->
          Role
          |> Ash.Changeset.for_create(:create_role_with_system_flag, %{
            name: name,
            description: name,
            permission_set_name: perm,
            is_system_role: name == "Mitglied"
          })
          |> Ash.create!(authorize?: false, domain: Mv.Authorization)

        _ ->
          :ok
      end
    end
  end

  defp put_oidc_config(opts) do
    current = Application.get_env(:mv, :oidc_role_sync, [])
    merged = Keyword.merge(current, opts)
    Application.put_env(:mv, :oidc_role_sync, merged)

    fn ->
      Application.put_env(:mv, :oidc_role_sync, current)
    end
  end

  defp admin_role_id do
    {:ok, role} = Role.get_admin_role()
    role.id
  end

  defp mitglied_role_id do
    {:ok, role} = Role.get_mitglied_role()
    role.id
  end

  defp get_user(id) do
    User
    |> Ash.Query.filter(id == ^id)
    |> Ash.read_one(authorize?: false, domain: Mv.Accounts)
  end

  defp create_user_with_mitglied(email) do
    {:ok, _} = Accounts.create_user(%{email: email}, authorize?: false)
    get_user_by_email(email)
  end

  defp create_user_with_admin(email) do
    {:ok, _} = Accounts.create_user(%{email: email}, authorize?: false)
    {:ok, u} = get_user_by_email(email)

    u
    |> Ash.Changeset.for_update(:update_user, %{role_id: admin_role_id()})
    |> Ash.update!(authorize?: false)

    get_user(u.id)
  end

  defp get_user_by_email(email) do
    User
    |> Ash.Query.filter(email == ^email)
    |> Ash.read_one(authorize?: false, domain: Mv.Accounts)
  end
end
