defmodule Mv.Membership.MemberSearchGroupsIntegrationTest do
  @moduledoc """
  Tests for member search integration with group names (Issue #375).

  Verifies that:
  - Group names are included in member search (via search_vector / FTS)
  - Searching by group name returns all members in that group
  - Search vector updates when member-group associations change (trigger on member_groups)
  - Edge cases (multiple groups, no groups, special characters) and authorization

  Implementation: search_vector trigger and trigger on member_groups
  (see migration 20260217120000_add_group_names_to_member_search_vector.exs, Issue #375).
  """
  use Mv.DataCase, async: false

  alias Mv.Helpers.SystemActor
  alias Mv.Membership.{Group, Member, MemberGroup}

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

  describe "search by group name" do
    test "search by group name finds member in that group", %{system_actor: actor} do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Anna", last_name: "Arbeiter", email: "anna@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Vorstand"})
        |> Ash.create(actor: actor)

      {:ok, _mg} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
        |> Ash.create(actor: actor)

      results =
        Member
        |> Member.fuzzy_search(%{query: "Vorstand"})
        |> Ash.read!(actor: actor)

      assert length(results) == 1
      assert List.first(results).id == member.id
    end

    test "search by group name finds all members in that group", %{system_actor: actor} do
      {:ok, m1} =
        Mv.Membership.create_member(
          %{first_name: "Bob", last_name: "Brown", email: "bob1@example.com"},
          actor: actor
        )

      {:ok, m2} =
        Mv.Membership.create_member(
          %{first_name: "Beth", last_name: "Blue", email: "beth@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
        |> Ash.create(actor: actor)

      for member <- [m1, m2] do
        {:ok, _} =
          MemberGroup
          |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
          |> Ash.create(actor: actor)
      end

      results =
        Member
        |> Member.fuzzy_search(%{query: "Board Members"})
        |> Ash.read!(actor: actor)

      ids = Enum.map(results, & &1.id)
      assert m1.id in ids
      assert m2.id in ids
      assert length(results) == 2
    end

    test "member in multiple groups is findable by any of those group names", %{
      system_actor: actor
    } do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Clara", last_name: "Clark", email: "clara@example.com"},
          actor: actor
        )

      {:ok, g1} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Alpha Team"})
        |> Ash.create(actor: actor)

      {:ok, g2} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Beta Team"})
        |> Ash.create(actor: actor)

      for {m, g} <- [{member, g1}, {member, g2}] do
        {:ok, _} =
          MemberGroup
          |> Ash.Changeset.for_create(:create, %{member_id: m.id, group_id: g.id})
          |> Ash.create(actor: actor)
      end

      for group_name <- ["Alpha Team", "Beta Team"] do
        results =
          Member
          |> Member.fuzzy_search(%{query: group_name})
          |> Ash.read!(actor: actor)

        assert Enum.any?(results, fn r -> r.id == member.id end),
               "Search for #{group_name} should find member"
      end
    end

    test "search by group name does not return members not in that group", %{
      system_actor: actor
    } do
      {:ok, member_in_x} =
        Mv.Membership.create_member(
          %{first_name: "Xavier", last_name: "X", email: "xavier@example.com"},
          actor: actor
        )

      {:ok, member_in_y} =
        Mv.Membership.create_member(
          %{first_name: "Yvonne", last_name: "Y", email: "yvonne@example.com"},
          actor: actor
        )

      {:ok, group_x} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "GroupXOnly"})
        |> Ash.create(actor: actor)

      {:ok, group_y} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "GroupYOnly"})
        |> Ash.create(actor: actor)

      {:ok, _} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member_in_x.id, group_id: group_x.id})
        |> Ash.create(actor: actor)

      {:ok, _} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member_in_y.id, group_id: group_y.id})
        |> Ash.create(actor: actor)

      results_x =
        Member
        |> Member.fuzzy_search(%{query: "GroupXOnly"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results_x, fn r -> r.id == member_in_x.id end)
      refute Enum.any?(results_x, fn r -> r.id == member_in_y.id end)

      results_y =
        Member
        |> Member.fuzzy_search(%{query: "GroupYOnly"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results_y, fn r -> r.id == member_in_y.id end)
      refute Enum.any?(results_y, fn r -> r.id == member_in_x.id end)
    end

    test "member with no groups is not found by unrelated group name", %{system_actor: actor} do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Solo", last_name: "User", email: "solo@example.com"},
          actor: actor
        )

      {:ok, _group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "SomeOtherGroup"})
        |> Ash.create(actor: actor)

      # Member is not in any group; search for the group name should not return this member
      results =
        Member
        |> Member.fuzzy_search(%{query: "SomeOtherGroup"})
        |> Ash.read!(actor: actor)

      refute Enum.any?(results, fn r -> r.id == member.id end)
    end
  end

  describe "search vector update on member_groups changes" do
    test "adding member to group updates search vector (INSERT on member_groups)", %{
      system_actor: actor
    } do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "New", last_name: "Member", email: "new@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "NewlyAddedGroup"})
        |> Ash.create(actor: actor)

      # Before adding to group, search should not find by group name
      results_before =
        Member
        |> Member.fuzzy_search(%{query: "NewlyAddedGroup"})
        |> Ash.read!(actor: actor)

      refute Enum.any?(results_before, fn r -> r.id == member.id end)

      {:ok, _mg} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
        |> Ash.create(actor: actor)

      # After adding, search should find member (trigger on member_groups INSERT)
      results_after =
        Member
        |> Member.fuzzy_search(%{query: "NewlyAddedGroup"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results_after, fn r -> r.id == member.id end)
    end

    test "removing member from group updates search vector (DELETE on member_groups)", %{
      system_actor: actor
    } do
      # Use a member name that does not overlap with the group name so that the only
      # way to find them is via search_vector (group name). Otherwise trigram fuzzy
      # match on first_name would still find "Remove" when searching "RemovedGroup".
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Zara", last_name: "None", email: "zara.remove@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "RemovedGroup"})
        |> Ash.create(actor: actor)

      {:ok, mg} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
        |> Ash.create(actor: actor)

      results_before =
        Member
        |> Member.fuzzy_search(%{query: "RemovedGroup"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results_before, fn r -> r.id == member.id end)

      :ok = Mv.Membership.destroy_member_group(mg, actor: actor)

      results_after =
        Member
        |> Member.fuzzy_search(%{query: "RemovedGroup"})
        |> Ash.read!(actor: actor)

      refute Enum.any?(results_after, fn r -> r.id == member.id end)
    end
  end

  describe "edge cases" do
    test "token match: single word in group name matches (e.g. Board in Board Members)", %{
      system_actor: actor
    } do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Partial", last_name: "Test", email: "partial@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Board Members"})
        |> Ash.create(actor: actor)

      {:ok, _mg} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
        |> Ash.create(actor: actor)

      # FTS with 'simple' config: full word "Board" or "Members" should match
      results =
        Member
        |> Member.fuzzy_search(%{query: "Board"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results, fn r -> r.id == member.id end),
             "Search for 'Board' should find member in group 'Board Members'"
    end

    test "search with token from group name containing special characters does not crash", %{
      system_actor: actor
    } do
      {:ok, member} =
        Mv.Membership.create_member(
          %{first_name: "Special", last_name: "Char", email: "special@example.com"},
          actor: actor
        )

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "Team A&B"})
        |> Ash.create(actor: actor)

      {:ok, _mg} =
        MemberGroup
        |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
        |> Ash.create(actor: actor)

      # Search for a token from the group name; proves tokenization does not crash on "A&B"
      results =
        Member
        |> Member.fuzzy_search(%{query: "Team"})
        |> Ash.read!(actor: actor)

      assert Enum.any?(results, fn r -> r.id == member.id end),
             "Search for 'Team' should find member in group 'Team A&B'"
    end
  end

  describe "authorization" do
    test "search respects authorization (actor sees only allowed members)", %{
      system_actor: system_actor
    } do
      # own_data user linked to member1 can only read member1; member2 is in same group
      admin = Mv.Fixtures.user_with_role_fixture("admin")
      user_own_data = Mv.Fixtures.user_with_role_fixture("own_data")

      member1 =
        Mv.Fixtures.member_fixture(%{
          first_name: "Linked",
          last_name: "User",
          email: "linked@example.com"
        })

      member2 =
        Mv.Fixtures.member_fixture(%{
          first_name: "Other",
          last_name: "User",
          email: "other@example.com"
        })

      {:ok, user_own_data} =
        user_own_data
        |> Ash.Changeset.for_update(:update, %{})
        |> Ash.Changeset.force_change_attribute(:member_id, member1.id)
        |> Ash.update(actor: admin)

      {:ok, group} =
        Group
        |> Ash.Changeset.for_create(:create, %{name: "SharedGroupName"})
        |> Ash.create(actor: system_actor)

      for member <- [member1, member2] do
        {:ok, _} =
          MemberGroup
          |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
          |> Ash.create(actor: admin)
      end

      # Search as own_data user: should only return member1 (linked), not member2
      results =
        Member
        |> Member.fuzzy_search(%{query: "SharedGroupName"})
        |> Ash.read!(actor: user_own_data)

      ids = Enum.map(results, & &1.id)
      assert member1.id in ids
      refute member2.id in ids
    end
  end
end
