defmodule Mv.Membership.GroupTest do
  @moduledoc """
  Tests for Group resource validations, CRUD operations, and relationships.
  Uses async: true; no shared DB state or sandbox constraints.
  """
  use Mv.DataCase, async: true

  alias Mv.Membership

  require Ash.Query
  import Ash.Expr

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

  describe "Validations - Name & Description" do
    @valid_attrs %{
      name: "Test Group",
      description: "Test description"
    }

    test "create group with valid attributes", %{actor: actor} do
      assert {:ok, group} = Membership.create_group(@valid_attrs, actor: actor)
      assert group.name == "Test Group"
      assert group.description == "Test description"
      assert group.slug != nil
    end

    test "create group with name only (description nil)", %{actor: actor} do
      attrs = Map.delete(@valid_attrs, :description)
      assert {:ok, group} = Membership.create_group(attrs, actor: actor)
      assert group.name == "Test Group"
      assert group.description == nil
    end

    test "return error when name is missing", %{actor: actor} do
      attrs = Map.delete(@valid_attrs, :name)

      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(attrs, actor: actor)

      assert error_message(errors, :name) =~ "must be present"
    end

    test "return error when name exceeds 100 characters", %{actor: actor} do
      long_name = String.duplicate("a", 101)
      attrs = Map.put(@valid_attrs, :name, long_name)

      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(attrs, actor: actor)

      assert error_message(errors, :name) =~ "100" or error_message(errors, :name) =~ "length"
    end

    test "return error when name is not unique (case-insensitive) - application level validation",
         %{
           actor: actor
         } do
      {:ok, _group1} = Membership.create_group(@valid_attrs, actor: actor)

      # Try to create with same name, different case
      # This tests application-level validation (Ash validations)
      attrs2 = Map.put(@valid_attrs, :name, "TEST GROUP")

      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(attrs2, actor: actor)

      error_msg = error_message(errors, :name)
      assert error_msg =~ "already been taken" || error_msg =~ "already exists"
    end

    test "description max length is 500 characters", %{actor: actor} do
      long_description = String.duplicate("a", 501)
      attrs = Map.put(@valid_attrs, :description, long_description)

      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(attrs, actor: actor)

      assert error_message(errors, :description) =~ "500" or
               error_message(errors, :description) =~ "length"
    end
  end

  describe "Slug Generation & Validation" do
    test "slug is automatically generated from name on create", %{actor: actor} do
      {:ok, group} =
        Membership.create_group(%{name: "Test Group Name"}, actor: actor)

      assert group.slug == "test-group-name"
    end

    test "slug is unique (prevents duplicate slugs from different names)", %{actor: actor} do
      {:ok, _group1} =
        Membership.create_group(%{name: "Test!!!"}, actor: actor)

      # Second group with name that generates same slug should fail
      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(%{name: "Test???"}, actor: actor)

      assert Enum.any?(errors, fn err ->
               (err.field == :slug or err.field == :name) and
                 (String.contains?(err.message, "already been taken") or
                    String.contains?(err.message, "already exists"))
             end)
    end

    test "slug is immutable (doesn't change when name is updated)", %{actor: actor} do
      {:ok, group} =
        Membership.create_group(%{name: "Original Name"}, actor: actor)

      original_slug = group.slug
      assert original_slug == "original-name"

      {:ok, updated_group} =
        Membership.update_group(group, %{name: "New Different Name"}, actor: actor)

      assert updated_group.slug == original_slug
      assert updated_group.name == "New Different Name"
    end

    test "slug cannot be empty (rejects name with only special characters)", %{actor: actor} do
      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.create_group(%{name: "!!!"}, actor: actor)

      assert Enum.any?(errors, fn err ->
               field = Map.get(err, :field)
               message = Map.get(err, :message, Exception.message(err))

               (field == :slug or field == :name) and
                 (String.contains?(message, "cannot be empty") or
                    String.contains?(message, "is required") or
                    String.contains?(message, "must be present"))
             end)
    end
  end

  describe "CRUD Operations" do
    test "create group with name and description", %{actor: actor} do
      attrs = %{name: "New Group", description: "Description"}

      assert {:ok, group} = Membership.create_group(attrs, actor: actor)
      assert group.name == "New Group"
      assert group.description == "Description"
    end

    test "update group name (slug remains unchanged)", %{actor: actor} do
      {:ok, group} = Membership.create_group(%{name: "Original"}, actor: actor)
      original_slug = group.slug

      {:ok, updated} = Membership.update_group(group, %{name: "Updated"}, actor: actor)

      assert updated.name == "Updated"
      assert updated.slug == original_slug
    end

    test "update group description", %{actor: actor} do
      {:ok, group} =
        Membership.create_group(%{name: "Test", description: "Old"}, actor: actor)

      {:ok, updated} =
        Membership.update_group(group, %{description: "New Description"}, actor: actor)

      assert updated.description == "New Description"
    end

    test "prevent duplicate name on update (case-insensitive)", %{actor: actor} do
      {:ok, _group1} = Membership.create_group(%{name: "Group One"}, actor: actor)
      {:ok, group2} = Membership.create_group(%{name: "Group Two"}, actor: actor)

      # Try to update group2 with group1's name (different case)
      assert {:error, %Ash.Error.Invalid{errors: errors}} =
               Membership.update_group(group2, %{name: "GROUP ONE"}, actor: actor)

      error_msg = error_message(errors, :name)
      assert error_msg =~ "already been taken" || error_msg =~ "already exists"
    end
  end

  describe "Calculations" do
    test "member count calculation returns 0 for empty group", %{actor: actor} do
      {:ok, group} = Membership.create_group(%{name: "Empty Group"}, actor: actor)

      # Load with calculation
      {:ok, group_with_count} =
        Ash.load(group, :member_count, actor: actor, domain: Mv.Membership)

      assert group_with_count.member_count == 0
    end

    test "member count calculation returns correct count when members added/removed", %{
      actor: actor
    } do
      {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor)
      {:ok, member1} = Membership.create_member(%{email: "member1@test.com"}, actor: actor)
      {:ok, member2} = Membership.create_member(%{email: "member2@test.com"}, actor: actor)

      # Add members to group
      {:ok, _mg1} =
        Membership.create_member_group(%{member_id: member1.id, group_id: group.id},
          actor: actor
        )

      {:ok, _mg2} =
        Membership.create_member_group(%{member_id: member2.id, group_id: group.id},
          actor: actor
        )

      # Check count
      {:ok, group_with_count} =
        Ash.load(group, :member_count, actor: actor, domain: Mv.Membership)

      assert group_with_count.member_count == 2

      # Remove one member
      {:ok, mg_to_delete} =
        Ash.read_one(
          Mv.Membership.MemberGroup
          |> Ash.Query.filter(expr(member_id == ^member1.id and group_id == ^group.id)),
          actor: actor,
          domain: Mv.Membership
        )

      :ok = Membership.destroy_member_group(mg_to_delete, actor: actor)

      # Check count again
      {:ok, group_with_count_updated} =
        Ash.load(group, :member_count, actor: actor, domain: Mv.Membership)

      assert group_with_count_updated.member_count == 1
    end
  end

  describe "Relationships & Deletion" do
    # We test business/data rules (CASCADE), not Ash relationship loading (framework).
    test "delete group cascades to member_groups (members remain intact)", %{actor: actor} do
      {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor)
      {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor)

      {:ok, _mg} =
        Membership.create_member_group(%{member_id: member.id, group_id: group.id},
          actor: actor
        )

      # Delete group
      :ok = Membership.destroy_group(group, actor: actor)

      # Member should still exist
      {:ok, member_reloaded} = Ash.get(Mv.Membership.Member, member.id, actor: actor)
      assert member_reloaded != nil

      # MemberGroup should be deleted
      {:ok, mgs} =
        Ash.read(
          Mv.Membership.MemberGroup
          |> Ash.Query.filter(expr(group_id == ^group.id)),
          actor: actor,
          domain: Mv.Membership
        )

      assert mgs == []
    end
  end

  # Helper function for error evaluation
  # Returns the error message for a given field, or empty string if not found
  defp error_message(errors, field) do
    case Enum.find(errors, fn err -> Map.get(err, :field) == field end) do
      nil ->
        ""

      err ->
        # Handle different error types (Ash.Error.Changes.Required doesn't have :message)
        case Map.get(err, :message) do
          nil -> Exception.message(err)
          message -> message
        end
    end
  end
end
