defmodule Mv.MembershipFees.MembershipFeeCyclePoliciesTest do
  @moduledoc """
  Tests for MembershipFeeCycle resource authorization policies.

  Verifies own_data can only read :linked (linked member's cycles);
  read_only can only read (no create/update/destroy);
  normal_user and admin can read, create, update, destroy (including mark_as_paid).
  """
  use Mv.DataCase, async: false

  alias Mv.MembershipFees
  alias Mv.Membership

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

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

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

    member
  end

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

    {:ok, fee_type} =
      MembershipFees.create_membership_fee_type(
        %{
          name: "Test Fee #{System.unique_integer([:positive])}",
          amount: Decimal.new("10.00"),
          interval: :yearly,
          description: "Test"
        },
        actor: admin
      )

    fee_type
  end

  defp create_cycle_fixture do
    admin = Mv.Fixtures.user_with_role_fixture("admin")
    member = create_member_fixture()
    fee_type = create_fee_type_fixture()

    {:ok, cycle} =
      MembershipFees.create_membership_fee_cycle(
        %{
          member_id: member.id,
          membership_fee_type_id: fee_type.id,
          cycle_start: Date.utc_today(),
          amount: Decimal.new("10.00"),
          status: :unpaid
        },
        actor: admin
      )

    cycle
  end

  describe "own_data permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("own_data")
      linked_member = create_member_fixture()
      other_member = create_member_fixture()
      fee_type = create_fee_type_fixture()
      admin = Mv.Fixtures.user_with_role_fixture("admin")

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

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

      {:ok, cycle_linked} =
        MembershipFees.create_membership_fee_cycle(
          %{
            member_id: linked_member.id,
            membership_fee_type_id: fee_type.id,
            cycle_start: Date.utc_today(),
            amount: Decimal.new("10.00"),
            status: :unpaid
          },
          actor: admin
        )

      {:ok, cycle_other} =
        MembershipFees.create_membership_fee_cycle(
          %{
            member_id: other_member.id,
            membership_fee_type_id: fee_type.id,
            cycle_start: Date.add(Date.utc_today(), -365),
            amount: Decimal.new("10.00"),
            status: :unpaid
          },
          actor: admin
        )

      %{user: user, cycle_linked: cycle_linked, cycle_other: cycle_other}
    end

    test "can read only linked member's cycles", %{
      user: user,
      cycle_linked: cycle_linked,
      cycle_other: cycle_other
    } do
      {:ok, list} =
        Mv.MembershipFees.MembershipFeeCycle
        |> Ash.read(actor: user, domain: Mv.MembershipFees)

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

  describe "read_only permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("read_only")
      cycle = create_cycle_fixture()
      %{actor: actor, user: user, cycle: cycle}
    end

    test "can read membership_fee_cycles (list)", %{user: user} do
      {:ok, list} =
        Mv.MembershipFees.MembershipFeeCycle
        |> Ash.read(actor: user, domain: Mv.MembershipFees)

      assert is_list(list)
    end

    test "cannot update cycle (returns forbidden)", %{user: user, cycle: cycle} do
      assert {:error, %Ash.Error.Forbidden{}} =
               MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)
    end

    test "cannot mark_as_paid (returns forbidden)", %{user: user, cycle: cycle} do
      assert {:error, %Ash.Error.Forbidden{}} =
               cycle
               |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
               |> Ash.update(actor: user, domain: Mv.MembershipFees)
    end

    test "cannot create cycle (returns forbidden)", %{user: user, actor: _actor} do
      member = create_member_fixture()
      fee_type = create_fee_type_fixture()

      assert {:error, %Ash.Error.Forbidden{}} =
               MembershipFees.create_membership_fee_cycle(
                 %{
                   member_id: member.id,
                   membership_fee_type_id: fee_type.id,
                   cycle_start: Date.utc_today(),
                   amount: Decimal.new("10.00"),
                   status: :unpaid
                 },
                 actor: user
               )
    end

    test "cannot destroy cycle (returns forbidden)", %{user: user, cycle: cycle} do
      assert {:error, %Ash.Error.Forbidden{}} =
               MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
    end
  end

  describe "normal_user permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("normal_user")
      cycle = create_cycle_fixture()
      %{actor: actor, user: user, cycle: cycle}
    end

    test "can read membership_fee_cycles (list)", %{user: user} do
      {:ok, list} =
        Mv.MembershipFees.MembershipFeeCycle
        |> Ash.read(actor: user, domain: Mv.MembershipFees)

      assert is_list(list)
    end

    test "can update cycle status", %{user: user, cycle: cycle} do
      assert {:ok, updated} =
               MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)

      assert updated.status == :paid
    end

    test "can mark_as_paid", %{user: user, cycle: cycle} do
      assert {:ok, updated} =
               cycle
               |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
               |> Ash.update(actor: user, domain: Mv.MembershipFees)

      assert updated.status == :paid
    end

    test "can create cycle", %{user: user, actor: _actor} do
      member = create_member_fixture()
      fee_type = create_fee_type_fixture()

      assert {:ok, created} =
               MembershipFees.create_membership_fee_cycle(
                 %{
                   member_id: member.id,
                   membership_fee_type_id: fee_type.id,
                   cycle_start: Date.utc_today(),
                   amount: Decimal.new("10.00"),
                   status: :unpaid
                 },
                 actor: user
               )

      assert created.member_id == member.id
    end

    test "can destroy cycle", %{user: user, cycle: cycle} do
      assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
    end
  end

  describe "admin permission set" do
    setup %{actor: actor} do
      user = Mv.Fixtures.user_with_role_fixture("admin")
      cycle = create_cycle_fixture()
      %{actor: actor, user: user, cycle: cycle}
    end

    test "can read membership_fee_cycles (list)", %{user: user} do
      {:ok, list} =
        Mv.MembershipFees.MembershipFeeCycle
        |> Ash.read(actor: user, domain: Mv.MembershipFees)

      assert is_list(list)
    end

    test "can update cycle", %{user: user, cycle: cycle} do
      assert {:ok, updated} =
               MembershipFees.update_membership_fee_cycle(cycle, %{status: :paid}, actor: user)

      assert updated.status == :paid
    end

    test "can mark_as_paid", %{user: user, cycle: cycle} do
      cycle_unpaid =
        cycle
        |> Ash.Changeset.for_update(:mark_as_unpaid, %{}, domain: Mv.MembershipFees)
        |> Ash.update!(actor: user, domain: Mv.MembershipFees)

      assert {:ok, updated} =
               cycle_unpaid
               |> Ash.Changeset.for_update(:mark_as_paid, %{}, domain: Mv.MembershipFees)
               |> Ash.update(actor: user, domain: Mv.MembershipFees)

      assert updated.status == :paid
    end

    test "can create cycle", %{user: user, actor: _actor} do
      member = create_member_fixture()
      fee_type = create_fee_type_fixture()

      assert {:ok, created} =
               MembershipFees.create_membership_fee_cycle(
                 %{
                   member_id: member.id,
                   membership_fee_type_id: fee_type.id,
                   cycle_start: Date.utc_today(),
                   amount: Decimal.new("10.00"),
                   status: :unpaid
                 },
                 actor: user
               )

      assert created.member_id == member.id
    end

    test "can destroy cycle", %{user: user, cycle: cycle} do
      assert :ok = MembershipFees.destroy_membership_fee_cycle(cycle, actor: user)
    end
  end
end
