defmodule Mv.Membership.MemberPoliciesTest do
  @moduledoc """
  Tests for Member resource authorization policies.

  Tests all 4 permission sets (own_data, read_only, normal_user, admin)
  and verifies that policies correctly enforce access control based on
  user roles and permission sets.
  """
  # async: false because we need database commits to be visible across queries
  # in the same test (especially for unlinked members)
  use Mv.DataCase, async: false

  alias Mv.Membership
  alias Mv.Accounts

  require Ash.Query

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

  # Helper to create a member linked to a user
  defp create_linked_member_for_user(user, _actor) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

    # Create member
    # NOTE: We need to ensure the member is actually persisted to the database
    # before we try to link it. Ash may delay writes, so we explicitly return the struct.
    {:ok, member} =
      Membership.create_member(
        %{
          first_name: "Linked",
          last_name: "Member",
          email: "linked#{System.unique_integer([:positive])}@example.com"
        },
        actor: admin
      )

    # Link member to user (User.member_id = member.id)
    # We use force_change_attribute because the member already exists and we just
    # need to set the foreign key. This avoids the issue where manage_relationship
    # tries to query the member without the actor context.
    result =
      user
      |> Ash.Changeset.for_update(:update, %{})
      |> Ash.Changeset.force_change_attribute(:member_id, member.id)
      |> Ash.update(actor: admin, domain: Mv.Accounts, return_notifications?: false)

    {:ok, _user} = result

    # Return the member struct directly - no need to reload since we just created it
    # and we're in the same transaction/sandbox
    member
  end

  # Helper to create an unlinked member (no user relationship)
  defp create_unlinked_member(_actor) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

    {:ok, member} =
      Membership.create_member(
        %{
          first_name: "Unlinked",
          last_name: "Member",
          email: "unlinked#{System.unique_integer([:positive])}@example.com"
        },
        actor: admin
      )

    member
  end

  describe "own_data permission set (Mitglied)" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("own_data")
      linked_member = create_linked_member_for_user(user, actor)
      unlinked_member = create_unlinked_member(actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)

      %{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
    end

    test "can read linked member", %{user: user, linked_member: linked_member} do
      {:ok, member} =
        Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)

      assert member.id == linked_member.id
    end

    test "can update linked member", %{user: user, linked_member: linked_member} do
      # Update is allowed via HasPermission check with :linked scope (not via special case)
      # The special case policy only applies to :read actions
      {:ok, updated_member} =
        Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)

      assert updated_member.first_name == "Updated"
    end

    test "cannot read unlinked member (returns forbidden)", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      # Note: With auto_filter policies, when a user tries to read a member that doesn't
      # match the filter (id == actor.member_id), Ash returns NotFound, not Forbidden.
      # This is the expected behavior - the filter makes the record "invisible" to the user.
      assert_raise Ash.Error.Invalid, fn ->
        Ash.get!(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership)
      end
    end

    test "cannot update unlinked member (returns forbidden)", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)
    end

    test "list members returns only linked member", %{
      user: user,
      linked_member: linked_member
    } do
      {:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)

      # Should only return the linked member (scope :linked filters)
      assert length(members) == 1
      assert hd(members).id == linked_member.id
    end

    test "cannot create member (returns forbidden)", %{user: user} do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.create_member(
                 %{
                   first_name: "New",
                   last_name: "Member",
                   email: "new#{System.unique_integer([:positive])}@example.com"
                 },
                 actor: user
               )
    end

    test "cannot destroy member (returns forbidden)", %{
      user: user,
      linked_member: linked_member
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        Ash.destroy!(linked_member, actor: user)
      end
    end
  end

  describe "read_only permission set (Vorstand/Buchhaltung)" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("read_only")
      linked_member = create_linked_member_for_user(user, actor)
      unlinked_member = create_unlinked_member(actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      %{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
    end

    test "can read all members", %{
      user: user,
      linked_member: linked_member,
      unlinked_member: unlinked_member
    } do
      {:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)

      # Should return all members (scope :all)
      member_ids = Enum.map(members, & &1.id)
      assert linked_member.id in member_ids
      assert unlinked_member.id in member_ids
    end

    test "can read individual member", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      {:ok, member} =
        Ash.get(Membership.Member, unlinked_member.id, actor: user, domain: Mv.Membership)

      assert member.id == unlinked_member.id
    end

    test "cannot create member (returns forbidden)", %{user: user} do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.create_member(
                 %{
                   first_name: "New",
                   last_name: "Member",
                   email: "new#{System.unique_integer([:positive])}@example.com"
                 },
                 actor: user
               )
    end

    test "cannot update any member (returns forbidden)", %{
      user: user,
      linked_member: linked_member
    } do
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)
    end

    test "cannot destroy any member (returns forbidden)", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        Ash.destroy!(unlinked_member, actor: user)
      end
    end
  end

  describe "normal_user permission set (Kassenwart)" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("normal_user")
      linked_member = create_linked_member_for_user(user, actor)
      unlinked_member = create_unlinked_member(actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      %{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
    end

    test "can read all members", %{
      user: user,
      linked_member: linked_member,
      unlinked_member: unlinked_member
    } do
      {:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)

      # Should return all members (scope :all)
      member_ids = Enum.map(members, & &1.id)
      assert linked_member.id in member_ids
      assert unlinked_member.id in member_ids
    end

    test "can create member", %{user: user} do
      {:ok, member} =
        Membership.create_member(
          %{
            first_name: "New",
            last_name: "Member",
            email: "new#{System.unique_integer([:positive])}@example.com"
          },
          actor: user
        )

      assert member.first_name == "New"
    end

    test "can update any member", %{user: user, unlinked_member: unlinked_member} do
      {:ok, updated_member} =
        Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)

      assert updated_member.first_name == "Updated"
    end

    test "cannot destroy member (safety - not in permission set)", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        Ash.destroy!(unlinked_member, actor: user)
      end
    end
  end

  describe "admin permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("admin")
      linked_member = create_linked_member_for_user(user, actor)
      unlinked_member = create_unlinked_member(actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      %{user: user, linked_member: linked_member, unlinked_member: unlinked_member}
    end

    test "can read all members", %{
      user: user,
      linked_member: linked_member,
      unlinked_member: unlinked_member
    } do
      {:ok, members} = Ash.read(Membership.Member, actor: user, domain: Mv.Membership)

      # Should return all members (scope :all)
      member_ids = Enum.map(members, & &1.id)
      assert linked_member.id in member_ids
      assert unlinked_member.id in member_ids
    end

    test "can create member", %{user: user} do
      {:ok, member} =
        Membership.create_member(
          %{
            first_name: "New",
            last_name: "Member",
            email: "new#{System.unique_integer([:positive])}@example.com"
          },
          actor: user
        )

      assert member.first_name == "New"
    end

    test "can update any member", %{user: user, unlinked_member: unlinked_member} do
      {:ok, updated_member} =
        Membership.update_member(unlinked_member, %{first_name: "Updated"}, actor: user)

      assert updated_member.first_name == "Updated"
    end

    test "can destroy any member", %{
      user: user,
      unlinked_member: unlinked_member
    } do
      :ok = Ash.destroy(unlinked_member, actor: user)

      # Verify member is deleted
      assert {:error, _} = Ash.get(Membership.Member, unlinked_member.id, domain: Mv.Membership)
    end
  end

  describe "special case: user can always READ linked member" do
    setup %{actor: _actor} do
      # Note: The special case policy only applies to :read actions.
      # Updates are handled by HasPermission with :linked scope (if permission exists).
      :ok
    end

    test "read_only user can read linked member (via special case bypass)", %{actor: actor} do
      # read_only has Member.read scope :all, but the special case ensures
      # users can ALWAYS read their linked member, even if they had no read permission.
      # This test verifies the special case works independently of permission sets.
      user = Mv.Fixtures.user_with_role_fixture("read_only")
      linked_member = create_linked_member_for_user(user, actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)

      # Should succeed (special case bypass policy for :read takes precedence)
      {:ok, member} =
        Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)

      assert member.id == linked_member.id
    end

    test "own_data user can read linked member (via special case bypass)", %{actor: actor} do
      # own_data has Member.read scope :linked, but the special case ensures
      # users can ALWAYS read their linked member regardless of permission set.
      user = Mv.Fixtures.user_with_role_fixture("own_data")
      linked_member = create_linked_member_for_user(user, actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)

      # Should succeed (special case bypass policy for :read takes precedence)
      {:ok, member} =
        Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)

      assert member.id == linked_member.id
    end

    test "own_data user can update linked member (via HasPermission :linked scope)", %{
      actor: actor
    } do
      # Update is NOT handled by special case - it's handled by HasPermission
      # with :linked scope. own_data has Member.update scope :linked.
      user = Mv.Fixtures.user_with_role_fixture("own_data")
      linked_member = create_linked_member_for_user(user, actor)

      # Reload user to get updated member_id
      {:ok, user} =
        Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role], actor: actor)

      {:ok, user} = Ash.load(user, :member, domain: Mv.Accounts, actor: actor)

      # Should succeed via HasPermission check (not special case)
      {:ok, updated_member} =
        Membership.update_member(linked_member, %{first_name: "Updated"}, actor: user)

      assert updated_member.first_name == "Updated"
    end
  end

  describe "member user link - only admin may set or change user link" do
    setup %{actor: actor} do
      normal_user =
        Mv.Fixtures.user_with_role_fixture("normal_user")
        |> Mv.Authorization.Actor.ensure_loaded()

      admin =
        Mv.Fixtures.user_with_role_fixture("admin")
        |> Mv.Authorization.Actor.ensure_loaded()

      unlinked_member = create_unlinked_member(actor)

      %{normal_user: normal_user, admin: admin, unlinked_member: unlinked_member}
    end

    test "normal_user can create member without :user argument", %{normal_user: normal_user} do
      {:ok, member} =
        Membership.create_member(
          %{
            first_name: "NoLink",
            last_name: "Member",
            email: "nolink#{System.unique_integer([:positive])}@example.com"
          },
          actor: normal_user
        )

      assert member.first_name == "NoLink"
      # Member has_one :user (FK on User side); ensure no user is linked
      {:ok, member} =
        Ash.load(member, :user, domain: Mv.Membership, actor: normal_user)

      assert is_nil(member.user)
    end

    test "normal_user cannot create member with :user argument (forbidden)", %{
      normal_user: normal_user
    } do
      other_user =
        Mv.Fixtures.user_with_role_fixture("read_only")
        |> Mv.Authorization.Actor.ensure_loaded()

      attrs = %{
        first_name: "Linked",
        last_name: "Member",
        email: "linked#{System.unique_integer([:positive])}@example.com",
        user: %{id: other_user.id}
      }

      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.create_member(attrs, actor: normal_user)
    end

    test "normal_user can update member without :user argument", %{
      normal_user: normal_user,
      unlinked_member: unlinked_member
    } do
      {:ok, updated} =
        Membership.update_member(unlinked_member, %{first_name: "UpdatedByNormal"},
          actor: normal_user
        )

      assert updated.first_name == "UpdatedByNormal"
    end

    test "normal_user cannot update member with :user argument (forbidden)", %{
      normal_user: normal_user,
      unlinked_member: unlinked_member
    } do
      other_user =
        Mv.Fixtures.user_with_role_fixture("own_data")
        |> Mv.Authorization.Actor.ensure_loaded()

      params = %{first_name: unlinked_member.first_name, user: %{id: other_user.id}}

      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.update_member(unlinked_member, params, actor: normal_user)
    end

    test "normal_user cannot update member with user: nil (unlink forbidden)", %{
      normal_user: normal_user,
      unlinked_member: unlinked_member
    } do
      # Link member first (via admin), then normal_user tries to unlink via user: nil
      admin =
        Mv.Fixtures.user_with_role_fixture("admin") |> Mv.Authorization.Actor.ensure_loaded()

      link_target =
        Mv.Fixtures.user_with_role_fixture("own_data") |> Mv.Authorization.Actor.ensure_loaded()

      {:ok, linked_member} =
        Membership.update_member(
          unlinked_member,
          %{user: %{id: link_target.id}},
          actor: admin
        )

      # Passing user: nil explicitly tries to unlink; only admin may do that
      assert {:error, %Ash.Error.Forbidden{}} =
               Membership.update_member(linked_member, %{user: nil}, actor: normal_user)
    end

    test "normal_user update linked member without :user keeps link", %{
      normal_user: normal_user,
      admin: admin,
      unlinked_member: unlinked_member
    } do
      # Admin links member to a user
      link_target =
        Mv.Fixtures.user_with_role_fixture("own_data")
        |> Mv.Authorization.Actor.ensure_loaded()

      {:ok, linked_member} =
        Membership.update_member(
          unlinked_member,
          %{user: %{id: link_target.id}},
          actor: admin
        )

      # normal_user updates only first_name (no :user) – link must remain (on_missing: :ignore)
      {:ok, updated} =
        Membership.update_member(linked_member, %{first_name: "Updated"}, actor: normal_user)

      assert updated.first_name == "Updated"

      {:ok, user} =
        Ash.get(Mv.Accounts.User, link_target.id, domain: Mv.Accounts, actor: admin)

      assert user.member_id == updated.id
    end

    test "admin can create member with :user argument", %{admin: admin} do
      link_target =
        Mv.Fixtures.user_with_role_fixture("own_data")
        |> Mv.Authorization.Actor.ensure_loaded()

      attrs = %{
        first_name: "AdminLinked",
        last_name: "Member",
        email: "adminlinked#{System.unique_integer([:positive])}@example.com",
        user: %{id: link_target.id}
      }

      {:ok, member} = Membership.create_member(attrs, actor: admin)

      assert member.first_name == "AdminLinked"

      {:ok, link_target} =
        Ash.get(Mv.Accounts.User, link_target.id, domain: Mv.Accounts, actor: admin)

      assert link_target.member_id == member.id
    end

    test "admin can update member with :user argument (link)", %{
      admin: admin,
      unlinked_member: unlinked_member
    } do
      link_target =
        Mv.Fixtures.user_with_role_fixture("read_only")
        |> Mv.Authorization.Actor.ensure_loaded()

      {:ok, updated} =
        Membership.update_member(
          unlinked_member,
          %{user: %{id: link_target.id}},
          actor: admin
        )

      assert updated.id == unlinked_member.id

      {:ok, reloaded_user} =
        Ash.get(Mv.Accounts.User, link_target.id,
          domain: Mv.Accounts,
          load: [:member],
          actor: admin
        )

      assert reloaded_user.member_id == updated.id
    end
  end
end
