defmodule Mv.Membership.MemberSearchWithCustomFieldsTest do
  @moduledoc """
  Tests for full-text search including custom_field_values.

  Tests verify that custom field values are included in the search_vector
  and can be found through the fuzzy_search functionality.
  """
  use Mv.DataCase, async: false

  alias Mv.Membership.{CustomField, CustomFieldValue, Member}

  setup do
    system_actor = Mv.Helpers.SystemActor.get_system_actor()

    # Create test members
    {:ok, member1} =
      Mv.Membership.create_member(
        %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
        actor: system_actor
      )

    {:ok, member2} =
      Mv.Membership.create_member(
        %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
        actor: system_actor
      )

    {:ok, member3} =
      Mv.Membership.create_member(
        %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"},
        actor: system_actor
      )

    # Create custom fields for different types
    {:ok, string_field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "membership_number",
        value_type: :string
      })
      |> Ash.create(actor: system_actor)

    {:ok, integer_field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "member_id_number",
        value_type: :integer
      })
      |> Ash.create(actor: system_actor)

    {:ok, email_field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "secondary_email",
        value_type: :email
      })
      |> Ash.create(actor: system_actor)

    {:ok, date_field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "birthday",
        value_type: :date
      })
      |> Ash.create(actor: system_actor)

    {:ok, boolean_field} =
      CustomField
      |> Ash.Changeset.for_create(:create, %{
        name: "newsletter",
        value_type: :boolean
      })
      |> Ash.create(actor: system_actor)

    %{
      member1: member1,
      member2: member2,
      member3: member3,
      string_field: string_field,
      integer_field: integer_field,
      email_field: email_field,
      date_field: date_field,
      boolean_field: boolean_field,
      system_actor: system_actor
    }
  end

  describe "search with custom field values" do
    test "finds member by string custom field value", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "MEMBER12345"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update by reloading member
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for the custom field value
      results =
        Member
        |> Member.fuzzy_search(%{query: "MEMBER12345"})
        |> Ash.read!(actor: system_actor)

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

    test "finds member by integer custom field value", %{
      system_actor: system_actor,
      member1: member1,
      integer_field: integer_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: integer_field.id,
          value: %{"_union_type" => "integer", "_union_value" => 42_424}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for the custom field value
      results =
        Member
        |> Member.fuzzy_search(%{query: "42424"})
        |> Ash.read!(actor: system_actor)

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

    test "finds member by email custom field value", %{
      system_actor: system_actor,
      member1: member1,
      email_field: email_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: email_field.id,
          value: %{"_union_type" => "email", "_union_value" => "alice.secondary@example.com"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for partial custom field value (should work via FTS or custom field filter)
      results =
        Member
        |> Member.fuzzy_search(%{query: "alice.secondary"})
        |> Ash.read!(actor: system_actor)

      assert length(results) == 1
      assert List.first(results).id == member1.id

      # Search for full email address (should work via custom field filter LIKE)
      results_full =
        Member
        |> Member.fuzzy_search(%{query: "alice.secondary@example.com"})
        |> Ash.read!(actor: system_actor)

      assert length(results_full) == 1
      assert List.first(results_full).id == member1.id

      # Search for domain part (should work via FTS or custom field filter)
      # Note: May return multiple results if other members have same domain
      results_domain =
        Member
        |> Member.fuzzy_search(%{query: "example.com"})
        |> Ash.read!(actor: system_actor)

      # Verify that member1 is in the results (may have other members too)
      ids = Enum.map(results_domain, & &1.id)
      assert member1.id in ids
    end

    test "finds member by date custom field value", %{
      system_actor: system_actor,
      member1: member1,
      date_field: date_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: date_field.id,
          value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for the custom field value (date is stored as text in search_vector)
      results =
        Member
        |> Member.fuzzy_search(%{query: "1990-05-15"})
        |> Ash.read!(actor: system_actor)

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

    test "finds member by boolean custom field value", %{
      system_actor: system_actor,
      member1: member1,
      boolean_field: boolean_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: boolean_field.id,
          value: %{"_union_type" => "boolean", "_union_value" => true}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for the custom field value (boolean is stored as "true" or "false" text)
      results =
        Member
        |> Member.fuzzy_search(%{query: "true"})
        |> Ash.read!(actor: system_actor)

      # Note: "true" might match other things, so we check that member1 is in results
      assert Enum.any?(results, fn m -> m.id == member1.id end)
    end

    test "custom field value update triggers search_vector update", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create initial custom field value
      {:ok, cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "OLDVALUE"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Update custom field value
      {:ok, _updated_cfv} =
        cfv
        |> Ash.Changeset.for_update(:update, %{
          value: %{"_union_type" => "string", "_union_value" => "NEWVALUE123"}
        })
        |> Ash.update(actor: system_actor)

      # Search for the new value
      results =
        Member
        |> Member.fuzzy_search(%{query: "NEWVALUE123"})
        |> Ash.read!(actor: system_actor)

      assert length(results) == 1
      assert List.first(results).id == member1.id

      # Old value should not be found
      old_results =
        Member
        |> Member.fuzzy_search(%{query: "OLDVALUE"})
        |> Ash.read!(actor: system_actor)

      refute Enum.any?(old_results, fn m -> m.id == member1.id end)
    end

    test "custom field value delete triggers search_vector update", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value
      {:ok, cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "TOBEDELETED"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Verify it's searchable
      results =
        Member
        |> Member.fuzzy_search(%{query: "TOBEDELETED"})
        |> Ash.read!(actor: system_actor)

      assert length(results) == 1
      assert List.first(results).id == member1.id

      # Delete custom field value
      assert :ok = Ash.destroy(cfv, actor: system_actor)

      # Value should no longer be found
      deleted_results =
        Member
        |> Member.fuzzy_search(%{query: "TOBEDELETED"})
        |> Ash.read!(actor: system_actor)

      refute Enum.any?(deleted_results, fn m -> m.id == member1.id end)
    end

    test "custom field value create triggers search_vector update", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value (trigger should update search_vector automatically)
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "AUTOUPDATE"}
        })
        |> Ash.create(actor: system_actor)

      # Search should find it immediately (trigger should have updated search_vector)
      results =
        Member
        |> Member.fuzzy_search(%{query: "AUTOUPDATE"})
        |> Ash.read!(actor: system_actor)

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

    test "member update includes custom field values in search_vector", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "MEMBERUPDATE"}
        })
        |> Ash.create(actor: system_actor)

      # Update member (should trigger search_vector update including custom fields)
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{notes: "Updated notes"}, actor: system_actor)

      # Search should find the custom field value
      results =
        Member
        |> Member.fuzzy_search(%{query: "MEMBERUPDATE"})
        |> Ash.read!(actor: system_actor)

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

    test "multiple custom field values are all searchable", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field,
      integer_field: integer_field,
      email_field: email_field
    } do
      # Create multiple custom field values
      {:ok, _cfv1} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "MULTI1"}
        })
        |> Ash.create(actor: system_actor)

      {:ok, _cfv2} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: integer_field.id,
          value: %{"_union_type" => "integer", "_union_value" => 99_999}
        })
        |> Ash.create(actor: system_actor)

      {:ok, _cfv3} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: email_field.id,
          value: %{"_union_type" => "email", "_union_value" => "multi@test.com"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # All values should be searchable
      results1 =
        Member
        |> Member.fuzzy_search(%{query: "MULTI1"})
        |> Ash.read!(actor: system_actor)

      assert Enum.any?(results1, fn m -> m.id == member1.id end)

      results2 =
        Member
        |> Member.fuzzy_search(%{query: "99999"})
        |> Ash.read!(actor: system_actor)

      assert Enum.any?(results2, fn m -> m.id == member1.id end)

      results3 =
        Member
        |> Member.fuzzy_search(%{query: "multi@test.com"})
        |> Ash.read!(actor: system_actor)

      assert Enum.any?(results3, fn m -> m.id == member1.id end)
    end

    test "finds member by custom field value with numbers in text field (e.g. phone number)", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value with numbers and text (like phone number or ID)
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "M-123-456"}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for full value (should work via search_vector)
      results_full =
        Member
        |> Member.fuzzy_search(%{query: "M-123-456"})
        |> Ash.read!(actor: system_actor)

      assert Enum.any?(results_full, fn m -> m.id == member1.id end),
             "Full value search should find member via search_vector"

      # Note: Partial substring search may require additional implementation
      # For now, we test that the full value is searchable, which is the primary use case
      # Substring matching for custom fields may need to be implemented separately
    end

    test "finds member by phone number in Emergency Contact custom field", %{
      system_actor: system_actor,
      member1: member1
    } do
      # Create Emergency Contact custom field
      {:ok, emergency_contact_field} =
        CustomField
        |> Ash.Changeset.for_create(:create, %{
          name: "Emergency Contact",
          value_type: :string
        })
        |> Ash.create(actor: system_actor)

      # Create custom field value with phone number
      phone_number = "+49 123 456789"

      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: emergency_contact_field.id,
          value: %{"_union_type" => "string", "_union_value" => phone_number}
        })
        |> Ash.create(actor: system_actor)

      # Force search_vector update
      {:ok, _updated_member} =
        Mv.Membership.update_member(member1, %{}, actor: system_actor)

      # Search for full phone number (should work via search_vector)
      results_full =
        Member
        |> Member.fuzzy_search(%{query: phone_number})
        |> Ash.read!(actor: system_actor)

      assert Enum.any?(results_full, fn m -> m.id == member1.id end),
             "Full phone number search should find member via search_vector"

      # Note: Partial substring search may require additional implementation
      # For now, we test that the full phone number is searchable, which is the primary use case
      # Substring matching for custom fields may need to be implemented separately
    end
  end

  describe "custom field substring search (ILIKE)" do
    test "finds member by prefix of custom field value", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value with a distinct word
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "Premium"}
        })
        |> Ash.create(actor: system_actor)

      # Test prefix searches - should all find the member
      for prefix <- ["Premium", "Premiu", "Premi", "Prem", "Pre"] do
        results =
          Member
          |> Member.fuzzy_search(%{query: prefix})
          |> Ash.read!(actor: system_actor)

        assert Enum.any?(results, fn m -> m.id == member1.id end),
               "Prefix '#{prefix}' should find member with custom field 'Premium'"
      end
    end

    test "custom field search is case-insensitive", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "GoldMember"}
        })
        |> Ash.create(actor: system_actor)

      # Test case variations - should all find the member
      for variant <- [
            "GoldMember",
            "goldmember",
            "GOLDMEMBER",
            "GoLdMeMbEr",
            "gold",
            "GOLD",
            "Gold"
          ] do
        results =
          Member
          |> Member.fuzzy_search(%{query: variant})
          |> Ash.read!(actor: system_actor)

        assert Enum.any?(results, fn m -> m.id == member1.id end),
               "Case variant '#{variant}' should find member with custom field 'GoldMember'"
      end
    end

    test "finds member by suffix/middle of custom field value", %{
      system_actor: system_actor,
      member1: member1,
      string_field: string_field
    } do
      # Create custom field value
      {:ok, _cfv} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "ActiveMember"}
        })
        |> Ash.create(actor: system_actor)

      # Test suffix and middle substring searches
      for substring <- ["Member", "ember", "tiveMem", "ctive"] do
        results =
          Member
          |> Member.fuzzy_search(%{query: substring})
          |> Ash.read!(actor: system_actor)

        assert Enum.any?(results, fn m -> m.id == member1.id end),
               "Substring '#{substring}' should find member with custom field 'ActiveMember'"
      end
    end

    test "finds correct member among multiple with different custom field values", %{
      system_actor: system_actor,
      member1: member1,
      member2: member2,
      member3: member3,
      string_field: string_field
    } do
      # Create different custom field values for each member
      {:ok, _cfv1} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member1.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "Beginner"}
        })
        |> Ash.create(actor: system_actor)

      {:ok, _cfv2} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member2.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "Advanced"}
        })
        |> Ash.create(actor: system_actor)

      {:ok, _cfv3} =
        CustomFieldValue
        |> Ash.Changeset.for_create(:create, %{
          member_id: member3.id,
          custom_field_id: string_field.id,
          value: %{"_union_type" => "string", "_union_value" => "Expert"}
        })
        |> Ash.create(actor: system_actor)

      # Search for "Begin" - should only find member1
      results_begin =
        Member
        |> Member.fuzzy_search(%{query: "Begin"})
        |> Ash.read!(actor: system_actor)

      assert length(results_begin) == 1
      assert List.first(results_begin).id == member1.id

      # Search for "Advan" - should only find member2
      results_advan =
        Member
        |> Member.fuzzy_search(%{query: "Advan"})
        |> Ash.read!(actor: system_actor)

      assert length(results_advan) == 1
      assert List.first(results_advan).id == member2.id

      # Search for "Exper" - should only find member3
      results_exper =
        Member
        |> Member.fuzzy_search(%{query: "Exper"})
        |> Ash.read!(actor: system_actor)

      assert length(results_exper) == 1
      assert List.first(results_exper).id == member3.id
    end

    # Note: Legacy format (type/value) is supported via the SQL ILIKE query on value->>'value'
    # This is tested implicitly by the migration trigger which handles both formats.
    # The Ash union type only accepts the new format (_union_type/_union_value) for creation,
    # but the search works on existing legacy data.
  end
end
