defmodule Mv.StatisticsTest do
  @moduledoc """
  Tests for Mv.Statistics module (member and membership fee cycle statistics).
  """
  use Mv.DataCase, async: true

  require Ash.Query
  import Ash.Expr

  alias Mv.Membership.Member
  alias Mv.Statistics
  alias Mv.MembershipFees
  alias Mv.MembershipFees.MembershipFeeCycle
  alias Mv.MembershipFees.MembershipFeeType

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

  defp create_fee_type(actor, attrs) do
    MembershipFeeType
    |> Ash.Changeset.for_create(
      :create,
      Map.merge(
        %{
          name: "Test Fee #{System.unique_integer([:positive])}",
          amount: Decimal.new("50.00"),
          interval: :yearly
        },
        attrs
      )
    )
    |> Ash.create!(actor: actor)
  end

  describe "first_join_year/1" do
    test "returns the year of the earliest join_date", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2019-03-15]})
      Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01]})
      assert Statistics.first_join_year(actor: actor) == 2019
    end

    test "returns the only member's join year when one member exists", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2021-06-01]})
      assert Statistics.first_join_year(actor: actor) == 2021
    end

    test "returns nil when no members exist", %{actor: actor} do
      # Guarantee empty member table so the assertion is deterministic
      Member
      |> Ash.read!(actor: actor)
      |> Enum.each(&Ash.destroy!(&1, actor: actor))

      result = Statistics.first_join_year(actor: actor)
      assert is_nil(result)
    end
  end

  describe "active_member_count/1" do
    test "returns 0 when there are no members", %{actor: actor} do
      assert Statistics.active_member_count(actor: actor) == 0
    end

    test "returns 1 when one member has no exit_date", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-15]})
      assert Statistics.active_member_count(actor: actor) == 1
    end

    test "returns 0 for that member when exit_date is set", %{actor: actor} do
      _member =
        Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-15], exit_date: ~D[2024-06-01]})

      assert Statistics.active_member_count(actor: actor) == 0
    end

    test "counts only active members when mix of active and inactive", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01]})
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01], exit_date: ~D[2024-01-01]})
      assert Statistics.active_member_count(actor: actor) == 1
    end
  end

  describe "inactive_member_count/1" do
    test "returns 0 when all members are active", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01]})
      assert Statistics.inactive_member_count(actor: actor) == 0
    end

    test "returns 1 when one member has exit_date set", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2022-01-01], exit_date: ~D[2024-06-01]})
      assert Statistics.inactive_member_count(actor: actor) == 1
    end
  end

  describe "joins_by_year/2" do
    test "returns 0 for year with no joins", %{actor: actor} do
      assert Statistics.joins_by_year(1999, actor: actor) == 0
    end

    test "returns 1 when one member has join_date in that year", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-06-15]})
      assert Statistics.joins_by_year(2023, actor: actor) == 1
    end

    test "returns 2 when two members joined in that year", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-01-01]})
      Mv.Fixtures.member_fixture(%{join_date: ~D[2023-12-31]})
      assert Statistics.joins_by_year(2023, actor: actor) == 2
    end
  end

  describe "exits_by_year/2" do
    test "returns 0 for year with no exits", %{actor: actor} do
      assert Statistics.exits_by_year(1999, actor: actor) == 0
    end

    test "returns 1 when one member has exit_date in that year", %{actor: actor} do
      Mv.Fixtures.member_fixture(%{join_date: ~D[2020-01-01], exit_date: ~D[2023-06-15]})
      assert Statistics.exits_by_year(2023, actor: actor) == 1
    end
  end

  describe "cycle_totals_by_year/2" do
    test "returns zero totals for year with no cycles", %{actor: actor} do
      result = Statistics.cycle_totals_by_year(1999, actor: actor)
      assert result.total == Decimal.new(0)
      assert result.paid == Decimal.new(0)
      assert result.unpaid == Decimal.new(0)
      assert result.suspended == Decimal.new(0)
    end

    test "returns totals by status for cycles in that year", %{actor: actor} do
      fee_type = create_fee_type(actor, %{amount: Decimal.new("50.00")})

      # Creating members with fee type triggers cycle generation (2020..today). We use 2024 cycles.
      _member1 =
        Mv.Fixtures.member_fixture(%{
          join_date: ~D[2020-01-01],
          membership_fee_type_id: fee_type.id
        })

      _member2 =
        Mv.Fixtures.member_fixture(%{
          join_date: ~D[2020-01-01],
          membership_fee_type_id: fee_type.id
        })

      # Get 2024 cycles and set status (each member has one 2024 yearly cycle from generator)
      cycles_2024 =
        MembershipFeeCycle
        |> Ash.Query.filter(
          expr(cycle_start >= ^~D[2024-01-01] and cycle_start < ^~D[2025-01-01])
        )
        |> Ash.read!(actor: actor)
        |> Enum.sort_by(& &1.member_id)

      [c1, c2] = cycles_2024
      assert {:ok, _} = Ash.update(c1, %{status: :paid}, domain: MembershipFees, actor: actor)

      assert {:ok, _} =
               Ash.update(c2, %{status: :suspended}, domain: MembershipFees, actor: actor)

      result = Statistics.cycle_totals_by_year(2024, actor: actor)
      assert Decimal.equal?(result.total, Decimal.new("100.00"))
      assert Decimal.equal?(result.paid, Decimal.new("50.00"))
      assert Decimal.equal?(result.unpaid, Decimal.new(0))
      assert Decimal.equal?(result.suspended, Decimal.new("50.00"))
    end

    test "when fee_type_id is passed in opts, returns only cycles of that fee type", %{
      actor: actor
    } do
      fee_type_a = create_fee_type(actor, %{amount: Decimal.new("30.00")})
      fee_type_b = create_fee_type(actor, %{amount: Decimal.new("70.00")})

      _m1 =
        Mv.Fixtures.member_fixture(%{
          join_date: ~D[2020-01-01],
          membership_fee_type_id: fee_type_a.id
        })

      _m2 =
        Mv.Fixtures.member_fixture(%{
          join_date: ~D[2020-01-01],
          membership_fee_type_id: fee_type_b.id
        })

      # Without filter: both fee types' cycles (2024)
      all_result = Statistics.cycle_totals_by_year(2024, actor: actor)
      assert Decimal.equal?(all_result.total, Decimal.new("100.00"))

      # With fee_type_id as string (as from form/URL): only that type's cycles
      opts_a = [actor: actor, fee_type_id: to_string(fee_type_a.id)]
      result_a = Statistics.cycle_totals_by_year(2024, opts_a)
      assert Decimal.equal?(result_a.total, Decimal.new("30.00"))

      opts_b = [actor: actor, fee_type_id: to_string(fee_type_b.id)]
      result_b = Statistics.cycle_totals_by_year(2024, opts_b)
      assert Decimal.equal?(result_b.total, Decimal.new("70.00"))
    end
  end

  describe "open_amount_total/1" do
    test "returns 0 when there are no unpaid cycles", %{actor: actor} do
      assert Statistics.open_amount_total(actor: actor) == Decimal.new(0)
    end

    test "returns sum of amount for all unpaid cycles", %{actor: actor} do
      fee_type = create_fee_type(actor, %{amount: Decimal.new("50.00")})

      _member =
        Mv.Fixtures.member_fixture(%{
          join_date: ~D[2020-01-01],
          membership_fee_type_id: fee_type.id
        })

      # Cycle generator creates yearly cycles (2020..today), all unpaid by default
      unpaid_sum = Statistics.open_amount_total(actor: actor)
      assert Decimal.compare(unpaid_sum, Decimal.new(0)) == :gt
      # Should be 50 * number of years from 2020 to current year
      current_year = Date.utc_today().year
      expected_count = current_year - 2020 + 1
      assert Decimal.equal?(unpaid_sum, Decimal.new(50 * expected_count))
    end
  end
end
