defmodule Mv.MembershipFees.CycleGeneratorEdgeCasesTest do
  @moduledoc """
  Edge case tests for the CycleGenerator module.

  Tests cover:
  - Member joins today
  - Member left yesterday
  - Year boundary handling
  - Leap year handling
  - Members with no existing cycles
  - Members with existing cycles
  """
  use Mv.DataCase, async: false

  alias Mv.MembershipFees.CycleGenerator
  alias Mv.MembershipFees.MembershipFeeCycle
  alias Mv.MembershipFees.MembershipFeeType

  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. Note: If membership_fee_type_id is provided,
  # cycles will be auto-generated during creation in test environment.
  defp create_member(attrs, actor) do
    default_attrs = %{
      first_name: "Test",
      last_name: "User",
      email: "test#{System.unique_integer([:positive])}@example.com"
    }

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

  # Helper to create a member and explicitly generate cycles with a fixed "today" date.
  # This avoids date dependency issues in tests.
  #
  # Note: We first create the member without fee_type_id, then assign it via update,
  # which triggers the after_action hook. However, we then explicitly regenerate
  # cycles with the fixed "today" date to ensure consistency.
  defp create_member_with_cycles(attrs, today, actor) do
    # Extract membership_fee_type_id if present
    fee_type_id = Map.get(attrs, :membership_fee_type_id)

    # Create member WITHOUT fee type first to avoid auto-generation with real today
    attrs_without_fee_type = Map.delete(attrs, :membership_fee_type_id)

    member =
      create_member(attrs_without_fee_type, actor)

    # Assign fee type if provided (this will trigger auto-generation with real today)
    member =
      if fee_type_id do
        {:ok, updated} =
          Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type_id},
            actor: actor
          )

        updated
      else
        member
      end

    # Explicitly regenerate cycles with fixed "today" date to override any auto-generated cycles
    # This ensures the test uses the fixed date, not the real current date
    if fee_type_id && member.join_date do
      # Delete any existing cycles first to ensure clean state
      existing_cycles = get_member_cycles(member.id, actor)
      Enum.each(existing_cycles, &Ash.destroy!(&1, actor: actor))

      # Generate cycles with fixed "today" date
      {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)
    end

    member
  end

  # Helper to get cycles for a member
  defp get_member_cycles(member_id, actor) do
    MembershipFeeCycle
    |> Ash.Query.filter(member_id == ^member_id)
    |> Ash.Query.sort(cycle_start: :asc)
    |> Ash.read!(actor: actor)
  end

  # Helper to set up settings
  defp setup_settings(include_joining_cycle, actor) do
    {:ok, settings} = Mv.Membership.get_settings()

    settings
    |> Ash.Changeset.for_update(:update, %{include_joining_cycle: include_joining_cycle})
    |> Ash.update!(actor: actor)
  end

  describe "member joins today" do
    test "current cycle is generated (yearly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]

      # Create member WITHOUT fee type first to avoid auto-generation with real today
      member =
        create_member(
          %{
            join_date: today,
            membership_fee_start_date: ~D[2024-01-01]
          },
          actor
        )

      # Assign fee type
      {:ok, member} =
        Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)

      # Explicitly generate cycles with fixed "today" date
      {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have the current year's cycle
      cycle_years = Enum.map(cycles, & &1.cycle_start.year)
      assert 2024 in cycle_years
    end

    test "current cycle is generated (monthly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :monthly}, actor)

      today = ~D[2024-06-15]

      # Create member WITHOUT fee type first to avoid auto-generation with real today
      member =
        create_member(
          %{
            join_date: today,
            membership_fee_start_date: ~D[2024-06-01]
          },
          actor
        )

      # Assign fee type
      {:ok, member} =
        Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)

      # Explicitly generate cycles with fixed "today" date
      {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have June 2024 cycle
      assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-06-01] end)
    end

    test "current cycle is generated (quarterly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :quarterly}, actor)

      today = ~D[2024-05-15]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: today,
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-04-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have Q2 2024 cycle
      assert Enum.any?(cycles, fn c -> c.cycle_start == ~D[2024-04-01] end)
    end
  end

  describe "member left yesterday" do
    test "no future cycles are generated", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]
      yesterday = Date.add(today, -1)

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2022-03-15],
            exit_date: yesterday,
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2022-01-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # 2024 should be included because the member was still active during that cycle
      assert 2022 in cycle_years
      assert 2023 in cycle_years
      assert 2024 in cycle_years

      # 2025 should NOT be included
      refute 2025 in cycle_years
    end

    test "exit during first month of year stops at that year (monthly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :monthly}, actor)

      # Create member - cycles will be auto-generated
      member =
        create_member(
          %{
            join_date: ~D[2024-01-15],
            exit_date: ~D[2024-03-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-01-01]
          },
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_months = Enum.map(cycles, & &1.cycle_start.month) |> Enum.sort()

      assert 1 in cycle_months
      assert 2 in cycle_months
      assert 3 in cycle_months

      # April and beyond should NOT be included
      refute 4 in cycle_months
      refute 5 in cycle_months
    end
  end

  describe "member has no cycles initially" do
    test "returns error when fee type is not assigned", %{actor: actor} do
      setup_settings(true, actor)

      # Create member WITHOUT fee type (no auto-generation)
      member =
        create_member(
          %{
            join_date: ~D[2022-03-15],
            membership_fee_start_date: ~D[2022-01-01]
          },
          actor
        )

      # Verify no cycles exist initially
      initial_cycles = get_member_cycles(member.id, actor)
      assert initial_cycles == []

      # Trying to generate cycles without fee type should return error
      result = CycleGenerator.generate_cycles_for_member(member.id)
      assert result == {:error, :no_membership_fee_type}
    end

    test "generates all cycles when member is created with fee type", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2022-03-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2022-01-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have generated all cycles from 2022 to 2024 (3 cycles)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()
      assert 2022 in cycle_years
      assert 2023 in cycle_years
      assert 2024 in cycle_years
      # Should NOT have 2025 (today is 2024-06-15)
      refute 2025 in cycle_years
    end
  end

  describe "member has existing cycles" do
    test "generates from last cycle (not duplicating existing)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      # Create member WITHOUT fee type first
      member =
        create_member(
          %{
            join_date: ~D[2022-03-15],
            membership_fee_start_date: ~D[2022-01-01]
          },
          actor
        )

      # Manually create an existing cycle for 2022
      MembershipFeeCycle
      |> Ash.Changeset.for_create(:create, %{
        cycle_start: ~D[2022-01-01],
        member_id: member.id,
        membership_fee_type_id: fee_type.id,
        amount: fee_type.amount,
        status: :paid
      })
      |> Ash.create!(actor: actor)

      # Now assign fee type
      {:ok, member} =
        Mv.Membership.update_member(member, %{membership_fee_type_id: fee_type.id}, actor: actor)

      # Explicitly generate cycles with fixed "today" date
      today = ~D[2024-06-15]
      {:ok, _, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # Check all cycles
      all_cycles = get_member_cycles(member.id, actor)
      all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()

      # Should have 2022 (manually created), 2023 and 2024 (auto-generated)
      assert 2022 in all_cycle_years
      assert 2023 in all_cycle_years
      assert 2024 in all_cycle_years

      # Verify no duplicates
      assert length(all_cycles) == length(all_cycle_years)
    end
  end

  describe "year boundary handling" do
    test "cycles span across year boundaries correctly (yearly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2023-11-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2023-01-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # Should have 2023 and 2024
      assert 2023 in cycle_years
      assert 2024 in cycle_years
    end

    test "cycles span across year boundaries correctly (quarterly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :quarterly}, actor)

      today = ~D[2024-12-15]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2024-10-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-10-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)

      # Should have Q4 2024
      assert ~D[2024-10-01] in cycle_starts
    end

    test "December to January transition (monthly)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :monthly}, actor)

      today = ~D[2024-12-31]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2024-12-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-12-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_starts = Enum.map(cycles, & &1.cycle_start) |> Enum.sort(Date)

      # Should have Dec 2024
      assert ~D[2024-12-01] in cycle_starts
    end
  end

  describe "leap year handling" do
    test "February cycles in leap year", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :monthly}, actor)

      today = ~D[2024-03-15]

      # 2024 is a leap year
      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2024-02-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-02-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have February 2024 cycle
      feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-02-01] end)

      assert feb_cycle != nil
    end

    test "February cycles in non-leap year", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :monthly}, actor)

      today = ~D[2023-03-15]

      # 2023 is NOT a leap year
      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2023-02-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2023-02-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have February 2023 cycle
      feb_cycle = Enum.find(cycles, fn c -> c.cycle_start == ~D[2023-02-01] end)

      assert feb_cycle != nil
    end

    test "yearly cycle in leap year", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-12-31]

      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2024-02-29],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-01-01]
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)

      # Should have 2024 cycle
      cycle_2024 = Enum.find(cycles, fn c -> c.cycle_start == ~D[2024-01-01] end)

      assert cycle_2024 != nil
    end
  end

  describe "include_joining_cycle variations" do
    test "include_joining_cycle = true starts from joining cycle", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]

      # Member joins mid-2023, should get 2023 cycle with include_joining_cycle=true
      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2023-06-15],
            membership_fee_type_id: fee_type.id
            # membership_fee_start_date will be auto-calculated
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # Should include 2023 (joining year)
      assert 2023 in cycle_years
    end

    test "include_joining_cycle = false starts from next cycle", %{actor: actor} do
      setup_settings(false, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-06-15]

      # Member joins mid-2023, should start from 2024 with include_joining_cycle=false
      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2023-06-15],
            membership_fee_type_id: fee_type.id
            # membership_fee_start_date will be auto-calculated
          },
          today,
          actor
        )

      # Check all cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # Should NOT include 2023 (joining year)
      refute 2023 in cycle_years

      # Should start from 2024
      assert 2024 in cycle_years
    end
  end

  describe "inactive member processing" do
    test "inactive members receive cycles up to exit_date via generate_cycles_for_all_members", %{
      actor: actor
    } do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      # Create an inactive member (left in 2023) WITHOUT fee type initially
      # This simulates a member that was created before the fee system existed
      member =
        create_member(
          %{
            join_date: ~D[2021-03-15],
            exit_date: ~D[2023-06-15]
          },
          actor
        )

      # Now assign fee type (simulating a retroactive assignment)
      {:ok, member} =
        Mv.Membership.update_member(
          member,
          %{
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2021-01-01]
          },
          actor: actor
        )

      # Run batch generation with a "today" date after the member left
      today = ~D[2024-06-15]
      {:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today)

      # The inactive member should have been processed
      assert results.total >= 1

      # Check the member's cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()

      # Should have 2021, 2022, 2023 (exit year included)
      assert 2021 in cycle_years
      assert 2022 in cycle_years
      assert 2023 in cycle_years

      # Should NOT have 2024 (after exit)
      refute 2024 in cycle_years
    end

    test "exit_date on cycle_start still generates that cycle", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      today = ~D[2024-12-31]

      # Member exits exactly on cycle start (2024-01-01)
      # Create member and generate cycles with fixed "today" date
      member =
        create_member_with_cycles(
          %{
            join_date: ~D[2022-03-15],
            exit_date: ~D[2024-01-01],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2022-01-01]
          },
          today,
          actor
        )

      # Check cycles
      cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # 2024 should be included because exit_date == cycle_start means
      # the member was still a member on that day
      assert 2022 in cycle_years
      assert 2023 in cycle_years
      assert 2024 in cycle_years

      # 2025 should NOT be included
      refute 2025 in cycle_years
    end
  end
end
