defmodule Mv.Membership.MemberTypeChangeIntegrationTest do
  @moduledoc """
  Integration tests for membership fee type changes and cycle regeneration.
  """
  use Mv.DataCase, async: true

  alias Mv.MembershipFees.MembershipFeeType
  alias Mv.MembershipFees.MembershipFeeCycle
  alias Mv.MembershipFees.CalendarCycles

  require Ash.Query

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

  # Helper to create a membership fee type
  defp create_fee_type(attrs, actor) do
    default_attrs = %{
      name: "Test Fee Type #{System.unique_integer([:positive])}",
      amount: Decimal.new("50.00"),
      interval: :yearly
    }

    attrs = Map.merge(default_attrs, attrs)

    MembershipFeeType
    |> Ash.Changeset.for_create(:create, attrs)
    |> Ash.create!(actor: actor)
  end

  # Helper to create a member
  defp create_member(attrs, actor) do
    default_attrs = %{
      first_name: "Test",
      last_name: "Member",
      email: "test.member.#{System.unique_integer([:positive])}@example.com",
      join_date: ~D[2023-01-15]
    }

    attrs = Map.merge(default_attrs, attrs)
    {:ok, member} = Mv.Membership.create_member(attrs, actor: actor)
    member
  end

  # Helper to create a cycle
  defp create_cycle(member, fee_type, attrs, actor) do
    default_attrs = %{
      cycle_start: ~D[2024-01-01],
      amount: Decimal.new("50.00"),
      member_id: member.id,
      membership_fee_type_id: fee_type.id,
      status: :unpaid
    }

    attrs = Map.merge(default_attrs, attrs)

    MembershipFeeCycle
    |> Ash.Changeset.for_create(:create, attrs)
    |> Ash.create!(actor: actor)
  end

  describe "type change cycle regeneration" do
    test "future unpaid cycles are regenerated with new amount", %{actor: actor} do
      today = Date.utc_today()
      yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
      yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)

      # Create member without fee type first to avoid auto-generation
      member = create_member(%{}, actor)

      # Manually assign fee type (this will trigger cycle generation)
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
              actor: actor
            )

          updated
        end)

      # Cycle generation runs synchronously in the same transaction
      # No need to wait for async completion

      # Create cycles: one in the past (paid), one current (unpaid)
      # Note: Future cycles are not automatically generated by CycleGenerator,
      # so we only test with current cycle
      past_cycle_start = CalendarCycles.calculate_cycle_start(~D[2023-01-01], :yearly)
      current_cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)

      # Past cycle (paid) - should remain unchanged
      # Check if it already exists (from auto-generation), if not create it
      case MembershipFeeCycle
           |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
           |> Ash.read_one(actor: actor) do
        {:ok, existing_cycle} when not is_nil(existing_cycle) ->
          # Update to paid
          existing_cycle
          |> Ash.Changeset.for_update(:update, %{status: :paid})
          |> Ash.update!(actor: actor)

        _ ->
          create_cycle(
            member,
            yearly_type1,
            %{
              cycle_start: past_cycle_start,
              status: :paid,
              amount: Decimal.new("100.00")
            },
            actor
          )
      end

      # Current cycle (unpaid) - should be regenerated
      # Delete if exists (from auto-generation), then create with old amount
      case MembershipFeeCycle
           |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
           |> Ash.read_one(actor: actor) do
        {:ok, existing_cycle} when not is_nil(existing_cycle) ->
          Ash.destroy!(existing_cycle, actor: actor)

        _ ->
          :ok
      end

      _current_cycle =
        create_cycle(
          member,
          yearly_type1,
          %{
            cycle_start: current_cycle_start,
            status: :unpaid,
            amount: Decimal.new("100.00")
          },
          actor
        )

      # Change membership fee type (same interval, different amount)
      assert {:ok, _updated_member} =
               Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
                 actor: actor
               )

      # Cycle regeneration runs synchronously in the same transaction
      # No need to wait for async completion

      # Verify past cycle is unchanged
      past_cycle_after =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
        |> Ash.read_one!(actor: actor)

      assert past_cycle_after.status == :paid
      assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00"))
      assert past_cycle_after.membership_fee_type_id == yearly_type1.id

      # Verify current cycle was deleted and regenerated
      # Check that cycle with new type exists (regenerated)
      new_current_cycle =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
        |> Ash.read_one!(actor: actor)

      # Verify it has the new type and amount
      assert new_current_cycle.membership_fee_type_id == yearly_type2.id
      assert Decimal.equal?(new_current_cycle.amount, Decimal.new("150.00"))
      assert new_current_cycle.status == :unpaid

      # Verify old cycle with old type doesn't exist anymore
      old_current_cycles =
        MembershipFeeCycle
        |> Ash.Query.filter(
          member_id == ^member.id and cycle_start == ^current_cycle_start and
            membership_fee_type_id == ^yearly_type1.id
        )
        |> Ash.read!(actor: actor)

      assert Enum.empty?(old_current_cycles)
    end

    test "paid cycles remain unchanged", %{actor: actor} do
      today = Date.utc_today()
      yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
      yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)

      # Create member without fee type first to avoid auto-generation
      member = create_member(%{}, actor)

      # Manually assign fee type (this will trigger cycle generation)
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
              actor: actor
            )

          updated
        end)

      # Cycle generation runs synchronously in the same transaction
      # No need to wait for async completion

      # Get the current cycle and mark it as paid
      current_cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)

      # Find current cycle and mark as paid
      paid_cycle =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
        |> Ash.read_one!(actor: actor)
        |> Ash.Changeset.for_update(:mark_as_paid)
        |> Ash.update!(actor: actor)

      # Change membership fee type
      assert {:ok, _updated_member} =
               Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
                 actor: actor
               )

      # Cycle regeneration runs synchronously in the same transaction
      # No need to wait for async completion

      # Verify paid cycle is unchanged (not deleted and regenerated)
      {:ok, cycle_after} = Ash.get(MembershipFeeCycle, paid_cycle.id, actor: actor)
      assert cycle_after.status == :paid
      assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00"))
      assert cycle_after.membership_fee_type_id == yearly_type1.id
    end

    test "suspended cycles remain unchanged", %{actor: actor} do
      today = Date.utc_today()
      yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
      yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)

      # Create member without fee type first to avoid auto-generation
      member = create_member(%{}, actor)

      # Manually assign fee type (this will trigger cycle generation)
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
              actor: actor
            )

          updated
        end)

      # Cycle generation runs synchronously in the same transaction
      # No need to wait for async completion

      # Get the current cycle and mark it as suspended
      current_cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)

      # Find current cycle and mark as suspended
      suspended_cycle =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
        |> Ash.read_one!(actor: actor)
        |> Ash.Changeset.for_update(:mark_as_suspended)
        |> Ash.update!(actor: actor)

      # Change membership fee type
      assert {:ok, _updated_member} =
               Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
                 actor: actor
               )

      # Cycle regeneration runs synchronously in the same transaction
      # No need to wait for async completion

      # Verify suspended cycle is unchanged (not deleted and regenerated)
      {:ok, cycle_after} = Ash.get(MembershipFeeCycle, suspended_cycle.id, actor: actor)
      assert cycle_after.status == :suspended
      assert Decimal.equal?(cycle_after.amount, Decimal.new("100.00"))
      assert cycle_after.membership_fee_type_id == yearly_type1.id
    end

    test "only cycles that haven't ended yet are deleted", %{actor: actor} do
      today = Date.utc_today()
      yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
      yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)

      # Create member without fee type first to avoid auto-generation
      member = create_member(%{}, actor)

      # Manually assign fee type (this will trigger cycle generation)
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
              actor: actor
            )

          updated
        end)

      # Cycle generation runs synchronously in the same transaction
      # No need to wait for async completion

      # Create cycles: one in the past (unpaid, ended), one current (unpaid, not ended)
      past_cycle_start =
        CalendarCycles.calculate_cycle_start(
          Date.add(today, -365),
          :yearly
        )

      current_cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)

      # Past cycle (unpaid) - should remain unchanged (cycle_start < today)
      # Delete existing cycle if it exists (from auto-generation)
      case MembershipFeeCycle
           |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^past_cycle_start)
           |> Ash.read_one(actor: actor) do
        {:ok, existing_cycle} when not is_nil(existing_cycle) ->
          Ash.destroy!(existing_cycle, actor: actor)

        _ ->
          :ok
      end

      past_cycle =
        create_cycle(
          member,
          yearly_type1,
          %{
            cycle_start: past_cycle_start,
            status: :unpaid,
            amount: Decimal.new("100.00")
          },
          actor
        )

      # Current cycle (unpaid) - should be regenerated (cycle_start >= today)
      # Delete existing cycle if it exists (from auto-generation)
      case MembershipFeeCycle
           |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
           |> Ash.read_one(actor: actor) do
        {:ok, existing_cycle} when not is_nil(existing_cycle) ->
          Ash.destroy!(existing_cycle, actor: actor)

        _ ->
          :ok
      end

      _current_cycle =
        create_cycle(
          member,
          yearly_type1,
          %{
            cycle_start: current_cycle_start,
            status: :unpaid,
            amount: Decimal.new("100.00")
          },
          actor
        )

      # Change membership fee type
      assert {:ok, _updated_member} =
               Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
                 actor: actor
               )

      # Cycle regeneration runs synchronously in the same transaction
      # No need to wait for async completion

      # Verify past cycle is unchanged
      {:ok, past_cycle_after} = Ash.get(MembershipFeeCycle, past_cycle.id, actor: actor)
      assert past_cycle_after.status == :unpaid
      assert Decimal.equal?(past_cycle_after.amount, Decimal.new("100.00"))
      assert past_cycle_after.membership_fee_type_id == yearly_type1.id

      # Verify current cycle was regenerated
      # Check that cycle with new type exists
      new_current_cycle =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
        |> Ash.read_one!(actor: actor)

      assert new_current_cycle.membership_fee_type_id == yearly_type2.id
      assert Decimal.equal?(new_current_cycle.amount, Decimal.new("150.00"))

      # Verify old cycle with old type doesn't exist anymore
      old_current_cycles =
        MembershipFeeCycle
        |> Ash.Query.filter(
          member_id == ^member.id and cycle_start == ^current_cycle_start and
            membership_fee_type_id == ^yearly_type1.id
        )
        |> Ash.read!(actor: actor)

      assert Enum.empty?(old_current_cycles)
    end

    test "member calculations update after type change", %{actor: actor} do
      today = Date.utc_today()
      yearly_type1 = create_fee_type(%{interval: :yearly, amount: Decimal.new("100.00")}, actor)
      yearly_type2 = create_fee_type(%{interval: :yearly, amount: Decimal.new("150.00")}, actor)

      # Create member with join_date = today to avoid past cycles
      # This ensures no overdue cycles exist
      member = create_member(%{join_date: today}, actor)

      # Manually assign fee type (this will trigger cycle generation)
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: yearly_type1.id},
              actor: actor
            )

          updated
        end)

      # Cycle generation runs synchronously in the same transaction
      # No need to wait for async completion

      # Get current cycle start
      current_cycle_start = CalendarCycles.calculate_cycle_start(today, :yearly)

      # Ensure only one cycle exists (the current one)
      # Delete all cycles except the current one
      existing_cycles =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id)
        |> Ash.read!(actor: actor)

      Enum.each(existing_cycles, fn cycle ->
        if cycle.cycle_start != current_cycle_start do
          Ash.destroy!(cycle, actor: actor)
        end
      end)

      # Ensure current cycle exists and is unpaid
      case MembershipFeeCycle
           |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^current_cycle_start)
           |> Ash.read_one(actor: actor) do
        {:ok, existing_cycle} when not is_nil(existing_cycle) ->
          # Update to unpaid if it's not
          if existing_cycle.status != :unpaid do
            existing_cycle
            |> Ash.Changeset.for_update(:mark_as_unpaid)
            |> Ash.update!(actor: actor)
          end

        _ ->
          # Create if it doesn't exist
          create_cycle(
            member,
            yearly_type1,
            %{
              cycle_start: current_cycle_start,
              status: :unpaid,
              amount: Decimal.new("100.00")
            },
            actor
          )
      end

      # Load calculations before change
      member = Ash.load!(member, [:current_cycle_status, :overdue_count])
      assert member.current_cycle_status == :unpaid
      assert member.overdue_count == 0

      # Change membership fee type
      assert {:ok, updated_member} =
               Mv.Membership.update_member(member, %{membership_fee_type_id: yearly_type2.id},
                 actor: actor
               )

      # Cycle regeneration runs synchronously in the same transaction
      # No need to wait for async completion

      # Reload member with calculations
      updated_member = Ash.load!(updated_member, [:current_cycle_status, :overdue_count])

      # Calculations should still work (cycle was regenerated)
      assert updated_member.current_cycle_status == :unpaid
      assert updated_member.overdue_count == 0
    end
  end
end
