defmodule Mv.Membership.MemberGroupPoliciesTest do
  @moduledoc """
  Tests for MemberGroup resource authorization policies.

  Verifies own_data can only read linked member's associations;
  read_only can read all, cannot create/destroy;
  normal_user and admin can read, create, destroy.
  """
  use Mv.DataCase, async: false

  alias Mv.Membership

  require Ash.Query

  setup do
    system_actor = Mv.Helpers.SystemActor.get_system_actor()
    %{actor: system_actor}
  end

  defp create_member_fixture do
    Mv.Fixtures.member_fixture()
  end

  defp create_group_fixture do
    Mv.Fixtures.group_fixture()
  end

  defp create_member_group_fixture(member_id, group_id) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

    {:ok, member_group} =
      Membership.create_member_group(%{member_id: member_id, group_id: group_id}, actor: admin)

    member_group
  end

  describe "own_data permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("own_data")
      member = create_member_fixture()
      group = create_group_fixture()
      # Link user to member so actor.member_id is set
      admin = Mv.Fixtures.user_with_role_fixture("admin")

      user =
        user
        |> Ash.Changeset.for_update(:update, %{})
        |> Ash.Changeset.force_change_attribute(:member_id, member.id)
        |> Ash.update(actor: admin)

      {:ok, user} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
      mg_linked = create_member_group_fixture(member.id, group.id)
      # MemberGroup for another member (not linked to user)
      other_member = create_member_fixture()
      other_group = create_group_fixture()
      mg_other = create_member_group_fixture(other_member.id, other_group.id)
      %{user: user, member: member, group: group, mg_linked: mg_linked, mg_other: mg_other}
    end

    test "can read member_groups for linked member only", %{user: user, mg_linked: mg_linked} do
      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: user, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg_linked.id in ids
      refute Enum.empty?(list)
    end

    test "list returns only member_groups where member_id == actor.member_id", %{
      user: user,
      mg_linked: mg_linked,
      mg_other: mg_other
    } do
      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: user, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg_linked.id in ids
      refute mg_other.id in ids
    end

    test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
      # Use fresh member/group so we assert on Forbidden, not on duplicate validation
      other_member = create_member_fixture()
      other_group = create_group_fixture()

      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.create_member_group(
                 %{member_id: other_member.id, group_id: other_group.id},
                 actor: user
               )
    end

    test "cannot destroy member_group (returns forbidden)", %{user: user, mg_linked: mg_linked} do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.destroy_member_group(mg_linked, actor: user)
    end
  end

  describe "read_only permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("read_only")
      member = create_member_fixture()
      group = create_group_fixture()
      mg = create_member_group_fixture(member.id, group.id)
      %{actor: actor, user: user, member: member, group: group, mg: mg}
    end

    test "can read all member_groups", %{user: user, mg: mg} do
      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: user, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg.id in ids
    end

    test "cannot create member_group (returns forbidden)", %{user: user, actor: _actor} do
      member = create_member_fixture()
      group = create_group_fixture()

      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.create_member_group(%{member_id: member.id, group_id: group.id},
                 actor: user
               )
    end

    test "cannot destroy member_group (returns forbidden)", %{user: user, mg: mg} do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.destroy_member_group(mg, actor: user)
    end
  end

  describe "normal_user permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("normal_user")
      member = create_member_fixture()
      group = create_group_fixture()
      mg = create_member_group_fixture(member.id, group.id)
      %{actor: actor, user: user, member: member, group: group, mg: mg}
    end

    test "can read all member_groups", %{user: user, mg: mg} do
      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: user, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg.id in ids
    end

    test "can create member_group", %{user: user, actor: _actor} do
      member = create_member_fixture()
      group = create_group_fixture()

      assert {:ok, _mg} =
               Membership.create_member_group(%{member_id: member.id, group_id: group.id},
                 actor: user
               )
    end

    test "can destroy member_group", %{user: user, mg: mg} do
      assert :ok = Membership.destroy_member_group(mg, actor: user)
    end
  end

  describe "admin permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("admin")
      member = create_member_fixture()
      group = create_group_fixture()
      mg = create_member_group_fixture(member.id, group.id)
      %{actor: actor, user: user, member: member, group: group, mg: mg}
    end

    test "can read all member_groups", %{user: user, mg: mg} do
      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: user, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg.id in ids
    end

    test "admin with member_id set (linked to member) still reads all member_groups", %{
      actor: actor
    } do
      # Admin linked to a member (e.g. viewing as member context) must still get :all scope,
      # not restricted to linked member's groups (bypass is only for own_data).
      admin = Mv.Fixtures.user_with_role_fixture("admin")
      linked_member = create_member_fixture()
      other_member = create_member_fixture()
      group_a = create_group_fixture()
      group_b = create_group_fixture()

      admin =
        admin
        |> Ash.Changeset.for_update(:update, %{})
        |> Ash.Changeset.force_change_attribute(:member_id, linked_member.id)
        |> Ash.update(actor: actor)

      {:ok, admin} = Ash.load(admin, :role, domain: Mv.Accounts, actor: actor)

      mg_linked = create_member_group_fixture(linked_member.id, group_a.id)
      mg_other = create_member_group_fixture(other_member.id, group_b.id)

      {:ok, list} =
        Mv.Membership.MemberGroup
        |> Ash.read(actor: admin, domain: Mv.Membership)

      ids = Enum.map(list, & &1.id)
      assert mg_linked.id in ids, "Admin with member_id must see linked member's MemberGroups"

      assert mg_other.id in ids,
             "Admin with member_id must see all MemberGroups (:all), not only linked"
    end

    test "can create member_group", %{user: user, actor: _actor} do
      member = create_member_fixture()
      group = create_group_fixture()

      assert {:ok, _mg} =
               Membership.create_member_group(%{member_id: member.id, group_id: group.id},
                 actor: user
               )
    end

    test "can destroy member_group", %{user: user, mg: mg} do
      assert :ok = Membership.destroy_member_group(mg, actor: user)
    end
  end
end
