defmodule Mv.Membership.CustomFieldValuePoliciesTest do
  @moduledoc """
  Tests for CustomFieldValue 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
  use Mv.DataCase, async: false

  alias Mv.Membership.{CustomField, CustomFieldValue}
  alias Mv.Accounts

  require Ash.Query

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

  defp create_linked_member_for_user(user, _actor) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

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

    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)

    member
  end

  defp create_unlinked_member(_actor) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

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

    member
  end

  defp create_custom_field do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

    {:ok, field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "test_field_#{System.unique_integer([:positive])}",
        value_type: :string
      })
      |> Ash.create(actor: admin, domain: Mv.Membership)

    field
  end

  defp create_custom_field_value(member_id, custom_field_id, value) do
    admin = Mv.Fixtures.user_with_role_fixture("admin")

    {:ok, cfv} =
      CustomFieldValue
      |> Ash.Changeset.for_create(:create, %{
        member_id: member_id,
        custom_field_id: custom_field_id,
        value: %{"_union_type" => "string", "_union_value" => value}
      })
      |> Ash.create(actor: admin, domain: Mv.Membership)

    cfv
  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)
      custom_field = create_custom_field()

      cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")

      cfv_unlinked =
        create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")

      {: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,
        custom_field: custom_field,
        cfv_linked: cfv_linked,
        cfv_unlinked: cfv_unlinked,
        actor: actor
      }
    end

    test "can read custom field values of linked member", %{user: user, cfv_linked: cfv_linked} do
      {:ok, cfv} =
        Ash.get(CustomFieldValue, cfv_linked.id, actor: user, domain: Mv.Membership)

      assert cfv.id == cfv_linked.id
    end

    test "can list custom field values returns only linked member's values", %{
      user: user,
      cfv_linked: cfv_linked
    } do
      {:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)

      assert length(values) == 1
      assert hd(values).id == cfv_linked.id
    end

    test "can update custom field value of linked member", %{user: user, cfv_linked: cfv_linked} do
      {:ok, updated} =
        cfv_linked
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "updated"}
        })
        |> Ash.update(actor: user, domain: Mv.Membership)

      assert %Ash.Union{value: "updated", type: :string} = updated.value
    end

    test "can create custom field value for linked member", %{
      user: user,
      linked_member: linked_member,
      actor: _actor
    } do
      # Create a second custom field via admin (own_data cannot create CustomField)
      custom_field2 = create_custom_field()

      {:ok, cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: linked_member.id,
          custom_field_id: custom_field2.id,
          value: %{"_union_type" => "string", "_union_value" => "new"}
        })
        |> Ash.create(actor: user, domain: Mv.Membership)

      assert cfv.member_id == linked_member.id
      assert cfv.custom_field_id == custom_field2.id
    end

    test "can destroy custom field value of linked member", %{
      user: user,
      cfv_linked: cfv_linked,
      actor: actor
    } do
      result = Ash.destroy(cfv_linked, actor: user, domain: Mv.Membership)
      assert :ok = result

      assert {:error, _} =
               Ash.get(CustomFieldValue, cfv_linked.id, domain: Mv.Membership, actor: actor)
    end

    test "cannot read custom field values of unlinked member", %{
      user: user,
      cfv_unlinked: cfv_unlinked
    } do
      assert_raise Ash.Error.Invalid, fn ->
        Ash.get!(CustomFieldValue, cfv_unlinked.id, actor: user, domain: Mv.Membership)
      end
    end

    test "cannot update custom field value of unlinked member", %{
      user: user,
      cfv_unlinked: cfv_unlinked
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        cfv_unlinked
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "hacked"}
        })
        |> Ash.update!(actor: user, domain: Mv.Membership)
      end
    end

    test "cannot create custom field value for unlinked member", %{
      user: user,
      unlinked_member: unlinked_member,
      custom_field: custom_field
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: unlinked_member.id,
          custom_field_id: custom_field.id,
          value: %{"_union_type" => "string", "_union_value" => "forbidden"}
        })
        |> Ash.create!(actor: user, domain: Mv.Membership)
      end
    end

    test "cannot destroy custom field value of unlinked member", %{
      user: user,
      cfv_unlinked: cfv_unlinked
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        Ash.destroy!(cfv_unlinked, actor: user, domain: Mv.Membership)
      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)
      custom_field = create_custom_field()

      cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")

      cfv_unlinked =
        create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")

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

      %{
        user: user,
        cfv_linked: cfv_linked,
        cfv_unlinked: cfv_unlinked,
        custom_field: custom_field,
        linked_member: linked_member,
        unlinked_member: unlinked_member
      }
    end

    test "can read all custom field values", %{
      user: user,
      cfv_linked: cfv_linked,
      cfv_unlinked: cfv_unlinked
    } do
      {:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)

      ids = Enum.map(values, & &1.id)
      assert cfv_linked.id in ids
      assert cfv_unlinked.id in ids
    end

    test "can read individual custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
      {:ok, cfv} =
        Ash.get(CustomFieldValue, cfv_unlinked.id, actor: user, domain: Mv.Membership)

      assert cfv.id == cfv_unlinked.id
    end

    test "cannot create custom field value (returns forbidden)", %{
      user: user,
      linked_member: linked_member,
      custom_field: custom_field
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: linked_member.id,
          custom_field_id: custom_field.id,
          value: %{"_union_type" => "string", "_union_value" => "forbidden"}
        })
        |> Ash.create!(actor: user, domain: Mv.Membership)
      end
    end

    test "cannot update custom field value (returns forbidden)", %{
      user: user,
      cfv_linked: cfv_linked
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        cfv_linked
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "hacked"}
        })
        |> Ash.update!(actor: user, domain: Mv.Membership)
      end
    end

    test "cannot destroy custom field value (returns forbidden)", %{
      user: user,
      cfv_linked: cfv_linked
    } do
      assert_raise Ash.Error.Forbidden, fn ->
        Ash.destroy!(cfv_linked, actor: user, domain: Mv.Membership)
      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)
      custom_field = create_custom_field()

      cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")

      cfv_unlinked =
        create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")

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

      %{
        user: user,
        cfv_linked: cfv_linked,
        cfv_unlinked: cfv_unlinked,
        custom_field: custom_field,
        linked_member: linked_member,
        unlinked_member: unlinked_member,
        actor: actor
      }
    end

    test "can read all custom field values", %{
      user: user,
      cfv_linked: cfv_linked,
      cfv_unlinked: cfv_unlinked
    } do
      {:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)

      ids = Enum.map(values, & &1.id)
      assert cfv_linked.id in ids
      assert cfv_unlinked.id in ids
    end

    test "can create custom field value", %{
      user: user,
      unlinked_member: unlinked_member,
      actor: _actor
    } do
      # normal_user cannot create CustomField; use actor (admin) to create it
      custom_field = create_custom_field()

      {:ok, cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: unlinked_member.id,
          custom_field_id: custom_field.id,
          value: %{"_union_type" => "string", "_union_value" => "new"}
        })
        |> Ash.create(actor: user, domain: Mv.Membership)

      assert cfv.member_id == unlinked_member.id
    end

    test "can update any custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
      {:ok, updated} =
        cfv_unlinked
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "updated"}
        })
        |> Ash.update(actor: user, domain: Mv.Membership)

      assert %Ash.Union{value: "updated", type: :string} = updated.value
    end

    test "can destroy any custom field value", %{
      user: user,
      cfv_unlinked: cfv_unlinked,
      actor: actor
    } do
      :ok = Ash.destroy(cfv_unlinked, actor: user, domain: Mv.Membership)

      assert {:error, _} =
               Ash.get(CustomFieldValue, cfv_unlinked.id, domain: Mv.Membership, actor: actor)
    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)
      custom_field = create_custom_field()

      cfv_linked = create_custom_field_value(linked_member.id, custom_field.id, "linked")

      cfv_unlinked =
        create_custom_field_value(unlinked_member.id, custom_field.id, "unlinked")

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

      %{
        user: user,
        cfv_linked: cfv_linked,
        cfv_unlinked: cfv_unlinked,
        custom_field: custom_field,
        linked_member: linked_member,
        unlinked_member: unlinked_member
      }
    end

    test "can read all custom field values", %{
      user: user,
      cfv_linked: cfv_linked,
      cfv_unlinked: cfv_unlinked
    } do
      {:ok, values} = Ash.read(CustomFieldValue, actor: user, domain: Mv.Membership)

      ids = Enum.map(values, & &1.id)
      assert cfv_linked.id in ids
      assert cfv_unlinked.id in ids
    end

    test "can create custom field value", %{user: user, unlinked_member: unlinked_member} do
      custom_field = create_custom_field()

      {:ok, cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: unlinked_member.id,
          custom_field_id: custom_field.id,
          value: %{"_union_type" => "string", "_union_value" => "new"}
        })
        |> Ash.create(actor: user, domain: Mv.Membership)

      assert cfv.member_id == unlinked_member.id
    end

    test "can update any custom field value", %{user: user, cfv_unlinked: cfv_unlinked} do
      {:ok, updated} =
        cfv_unlinked
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "updated"}
        })
        |> Ash.update(actor: user, domain: Mv.Membership)

      assert %Ash.Union{value: "updated", type: :string} = updated.value
    end

    test "can destroy any custom field value", %{
      user: user,
      cfv_unlinked: cfv_unlinked,
      actor: actor
    } do
      :ok = Ash.destroy(cfv_unlinked, actor: user, domain: Mv.Membership)

      assert {:error, _} =
               Ash.get(CustomFieldValue, cfv_unlinked.id, domain: Mv.Membership, actor: actor)
    end
  end
end
