defmodule Mv.MembershipFees.CycleGeneratorTest do
  @moduledoc """
  Tests for the CycleGenerator module.
  """
  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 without triggering cycle generation
  defp create_member_without_cycles(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 set up settings with specific include_joining_cycle value
  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

  # 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

  describe "generate_cycles_for_member/2" do
    test "generates cycles from start date to today", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

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

      # Assign fee type
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)

          updated
        end)

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

      # Verify cycles were generated
      all_cycles = get_member_cycles(member.id, actor)
      cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort() |> Enum.uniq()

      # With include_joining_cycle=true and join_date=2022-03-15,
      # start_date should be 2022-01-01
      # Should have cycles for 2022, 2023, 2024
      assert 2022 in cycle_years
      assert 2023 in cycle_years
      assert 2024 in cycle_years
    end

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

      # Create member without fee type first to avoid auto-generation
      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2022-03-15],
            membership_fee_start_date: ~D[2022-01-01]
          },
          actor
        )

      # Manually create a 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 to member
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)

          updated
        end)

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

      # Should generate only 2023 and 2024 (2022 already exists)
      new_cycle_years = Enum.map(new_cycles, & &1.cycle_start.year) |> Enum.sort()

      assert 2022 not in new_cycle_years
    end

    test "respects left_at boundary (stops generation)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2022-03-15],
            exit_date: ~D[2023-06-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2022-01-01]
          },
          actor
        )

      # Generate cycles with specific "today" date far in the future
      today = ~D[2025-06-15]
      {:ok, cycles, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # With exit_date in 2023, should only generate 2022 and 2023 cycles
      cycle_years = Enum.map(cycles, & &1.cycle_start.year) |> Enum.sort()

      # Should not have 2024 or 2025 cycles
      assert 2024 not in cycle_years
      assert 2025 not in cycle_years
    end

    test "skips existing cycles (idempotent)", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2023-03-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2023-01-01]
          },
          actor
        )

      today = ~D[2024-06-15]

      # First generation
      {:ok, _first_cycles, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # Second generation (should be idempotent)
      {:ok, second_cycles, _} = CycleGenerator.generate_cycles_for_member(member.id, today: today)

      # Second call should return empty list (no new cycles)
      assert second_cycles == []
    end

    test "does not fill gaps when cycles were deleted", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      # Create member without fee type first to control which cycles exist
      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2020-03-15],
            membership_fee_start_date: ~D[2020-01-01]
          },
          actor
        )

      # Manually create cycles for 2020, 2021, 2022, 2023
      for year <- [2020, 2021, 2022, 2023] do
        MembershipFeeCycle
        |> Ash.Changeset.for_create(
          :create,
          %{
            cycle_start: Date.new!(year, 1, 1),
            member_id: member.id,
            membership_fee_type_id: fee_type.id,
            amount: fee_type.amount,
            status: :unpaid
          }
        )
        |> Ash.create!(actor: actor)
      end

      # Delete the 2021 cycle (create a gap)
      cycle_2021 =
        MembershipFeeCycle
        |> Ash.Query.filter(member_id == ^member.id and cycle_start == ^~D[2021-01-01])
        |> Ash.read_one!(actor: actor)

      Ash.destroy!(cycle_2021, actor: actor)

      # Now assign fee type to member (this triggers generation)
      # Since cycles already exist (2020, 2022, 2023), the generator will
      # start from the last existing cycle (2023) and go forward
      member =
        member
        |> then(fn m ->
          {:ok, updated} =
            Mv.Membership.update_member(m, %{membership_fee_type_id: fee_type.id}, actor: actor)

          updated
        end)

      # Verify gap was NOT filled and new cycles were generated from last existing
      all_cycles = get_member_cycles(member.id, actor)
      all_cycle_years = Enum.map(all_cycles, & &1.cycle_start.year) |> Enum.sort()

      # 2021 should NOT exist (gap was not filled)
      refute 2021 in all_cycle_years, "Gap at 2021 should NOT be filled"

      # 2020, 2022, 2023 should exist (original cycles)
      assert 2020 in all_cycle_years
      assert 2022 in all_cycle_years
      assert 2023 in all_cycle_years

      # 2024 and 2025 should exist (generated after last existing cycle 2023)
      assert 2024 in all_cycle_years
      assert 2025 in all_cycle_years
    end

    test "sets correct amount from membership fee type", %{actor: actor} do
      setup_settings(true, actor)
      amount = Decimal.new("75.50")
      fee_type = create_fee_type(%{interval: :yearly, amount: amount}, actor)

      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2024-03-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-01-01]
          },
          actor
        )

      # Verify cycles were generated with correct amount
      all_cycles = get_member_cycles(member.id, actor)
      refute Enum.empty?(all_cycles), "Expected cycles to be generated"

      # All cycles should have the correct amount
      Enum.each(all_cycles, fn cycle ->
        assert Decimal.equal?(cycle.amount, amount)
      end)
    end

    test "handles NULL membership_fee_start_date by calculating from join_date", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :quarterly}, actor)

      # Create member without membership_fee_start_date - it will be auto-calculated
      # and cycles will be auto-generated
      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2024-02-15],
            membership_fee_type_id: fee_type.id
            # No membership_fee_start_date - should be calculated
          },
          actor
        )

      # Verify cycles were auto-generated
      all_cycles = get_member_cycles(member.id, actor)

      # With include_joining_cycle=true and join_date=2024-02-15 (quarterly),
      # start_date should be 2024-01-01 (Q1 start)
      # Should have Q1, Q2, Q3, Q4 2024 cycles (based on current date)
      refute Enum.empty?(all_cycles), "Expected cycles to be generated"

      cycle_starts = Enum.map(all_cycles, & &1.cycle_start) |> Enum.sort(Date)
      first_cycle_start = List.first(cycle_starts)

      # First cycle should start in Q1 2024 (2024-01-01)
      assert first_cycle_start == ~D[2024-01-01]
    end

    test "returns error when member has no membership_fee_type", %{actor: actor} do
      # Create member without fee type - no auto-generation will occur
      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2024-03-15]
            # No membership_fee_type_id
          },
          actor
        )

      {:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
      assert reason == :no_membership_fee_type
    end

    test "returns error when member has no join_date", %{actor: actor} do
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      # Create member without join_date - no auto-generation will occur
      # (after_action hook checks for join_date)
      member =
        create_member_without_cycles(
          %{
            membership_fee_type_id: fee_type.id
            # No join_date
          },
          actor
        )

      {:error, reason} = CycleGenerator.generate_cycles_for_member(member.id)
      assert reason == :no_join_date
    end

    test "returns error when member not found" do
      fake_id = Ash.UUID.generate()
      {:error, reason} = CycleGenerator.generate_cycles_for_member(fake_id)
      assert reason == :member_not_found
    end
  end

  describe "generate_cycle_starts/3" do
    test "generates correct cycle starts for yearly interval" do
      starts = CycleGenerator.generate_cycle_starts(~D[2022-01-01], ~D[2024-06-15], :yearly)

      assert starts == [~D[2022-01-01], ~D[2023-01-01], ~D[2024-01-01]]
    end

    test "generates correct cycle starts for quarterly interval" do
      starts = CycleGenerator.generate_cycle_starts(~D[2024-01-01], ~D[2024-09-15], :quarterly)

      assert starts == [~D[2024-01-01], ~D[2024-04-01], ~D[2024-07-01]]
    end

    test "generates correct cycle starts for monthly interval" do
      starts = CycleGenerator.generate_cycle_starts(~D[2024-01-01], ~D[2024-03-15], :monthly)

      assert starts == [~D[2024-01-01], ~D[2024-02-01], ~D[2024-03-01]]
    end

    test "generates correct cycle starts for half_yearly interval" do
      starts = CycleGenerator.generate_cycle_starts(~D[2023-01-01], ~D[2024-09-15], :half_yearly)

      assert starts == [~D[2023-01-01], ~D[2023-07-01], ~D[2024-01-01], ~D[2024-07-01]]
    end

    test "returns empty list when start_date is after end_date" do
      starts = CycleGenerator.generate_cycle_starts(~D[2025-01-01], ~D[2024-06-15], :yearly)

      assert starts == []
    end

    test "includes cycle when end_date is on cycle start" do
      starts = CycleGenerator.generate_cycle_starts(~D[2024-01-01], ~D[2024-01-01], :yearly)

      assert starts == [~D[2024-01-01]]
    end
  end

  describe "generate_cycles_for_all_members/1" do
    test "generates cycles for multiple members", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      # Create multiple members
      _member1 =
        create_member_without_cycles(
          %{
            join_date: ~D[2024-01-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-01-01]
          },
          actor
        )

      _member2 =
        create_member_without_cycles(
          %{
            join_date: ~D[2024-02-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2024-01-01]
          },
          actor
        )

      today = ~D[2024-06-15]
      {:ok, results} = CycleGenerator.generate_cycles_for_all_members(today: today)

      assert is_map(results)
      assert Map.has_key?(results, :success)
      assert Map.has_key?(results, :failed)
      assert Map.has_key?(results, :total)
    end
  end

  describe "lock mechanism" do
    test "prevents concurrent generation for same member", %{actor: actor} do
      setup_settings(true, actor)
      fee_type = create_fee_type(%{interval: :yearly}, actor)

      member =
        create_member_without_cycles(
          %{
            join_date: ~D[2022-03-15],
            membership_fee_type_id: fee_type.id,
            membership_fee_start_date: ~D[2022-01-01]
          },
          actor
        )

      today = ~D[2024-06-15]

      # Run two concurrent generations
      task1 =
        Task.async(fn -> CycleGenerator.generate_cycles_for_member(member.id, today: today) end)

      task2 =
        Task.async(fn -> CycleGenerator.generate_cycles_for_member(member.id, today: today) end)

      result1 = Task.await(task1)
      result2 = Task.await(task2)

      # Both should succeed
      assert match?({:ok, _, _}, result1)
      assert match?({:ok, _, _}, result2)

      # One should have created cycles, the other should have empty list (idempotent)
      {:ok, cycles1, _} = result1
      {:ok, cycles2, _} = result2

      # Combined should not have duplicates
      all_cycles = cycles1 ++ cycles2
      unique_starts = all_cycles |> Enum.map(& &1.cycle_start) |> Enum.uniq()

      assert length(all_cycles) == length(unique_starts)
    end
  end
end
