defmodule Mv.Membership.Import.HeaderMapperTest do use ExUnit.Case, async: true alias Mv.Membership.Import.HeaderMapper describe "normalize_header/1" do test "trims whitespace" do assert HeaderMapper.normalize_header(" email ") == "email" end test "converts to lowercase" do assert HeaderMapper.normalize_header("EMAIL") == "email" assert HeaderMapper.normalize_header("E-Mail") == "e-mail" end test "normalizes Unicode characters" do # ß -> ss assert HeaderMapper.normalize_header("Straße") == "strasse" # Umlaute transliteration (ä -> ae, ö -> oe, ü -> ue) assert HeaderMapper.normalize_header("Müller") == "mueller" assert HeaderMapper.normalize_header("Köln") == "koeln" assert HeaderMapper.normalize_header("Grün") == "gruen" end test "compresses and removes whitespace" do # Whitespace is removed entirely to ensure "first name" == "firstname" assert HeaderMapper.normalize_header("first name") == "firstname" assert HeaderMapper.normalize_header("email address") == "emailaddress" end test "unifies hyphen variants" do # Different Unicode hyphen characters should become standard hyphen # en dash assert HeaderMapper.normalize_header("E–Mail") == "e-mail" # minus sign assert HeaderMapper.normalize_header("E−Mail") == "e-mail" # standard hyphen assert HeaderMapper.normalize_header("E-Mail") == "e-mail" end test "removes or unifies punctuation" do # Parentheses, slashes, etc. are removed (whitespace is also removed) assert HeaderMapper.normalize_header("E-Mail (privat)") == "e-mailprivat" assert HeaderMapper.normalize_header("Telefon / Mobil") == "telefonmobil" end test "handles empty strings" do assert HeaderMapper.normalize_header("") == "" assert HeaderMapper.normalize_header(" ") == "" end end describe "build_maps/2" do test "maps English email variant correctly" do headers = ["Email"] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert custom_map == %{} assert unknown == [] end test "maps German email variant correctly" do headers = ["E-Mail"] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert custom_map == %{} assert unknown == [] end test "maps multiple member fields" do headers = ["Email", "First Name", "Last Name"] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert member_map[:first_name] == 1 assert member_map[:last_name] == 2 assert custom_map == %{} assert unknown == [] end test "handles Unicode and whitespace in headers" do headers = [" E-Mail ", "Straße", " Telefon / Mobil "] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert member_map[:street] == 1 # "Telefon / Mobil" is not a known member field, so it should be unknown assert length(unknown) == 1 assert custom_map == %{} end test "returns error when duplicate headers normalize to same field" do headers = ["Email", "E-Mail"] assert {:error, reason} = HeaderMapper.build_maps(headers, []) assert reason =~ "duplicate" assert reason =~ "email" end test "returns error when required field email is missing" do headers = ["First Name", "Last Name"] assert {:error, reason} = HeaderMapper.build_maps(headers, []) assert reason =~ "Missing required header" assert reason =~ "email" assert reason =~ "accepted" end test "collects unknown columns" do headers = ["Email", "FooBar", "UnknownColumn"] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert length(unknown) == 2 assert "FooBar" in unknown or "foobar" in unknown assert "UnknownColumn" in unknown or "unknowncolumn" in unknown assert custom_map == %{} end test "ignores empty headers after normalization" do headers = ["Email", " ", ""] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert custom_map == %{} assert unknown == [] end test "maps custom field columns correctly" do headers = ["Email", "Lieblingsfarbe"] custom_fields = [%{id: "cf1", name: "Lieblingsfarbe"}] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, custom_fields) assert member_map[:email] == 0 assert custom_map["cf1"] == 1 assert unknown == [] end test "custom field collision: member field wins" do headers = ["Email"] # Custom field with name "Email" should not override member field custom_fields = [%{id: "cf1", name: "Email"}] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, custom_fields) assert member_map[:email] == 0 # Custom field should not be in custom_map because member field has priority assert custom_map == %{} assert unknown == [] end test "handles custom field with Unicode normalization" do headers = ["Email", "Straße"] custom_fields = [%{id: "cf1", name: "Straße"}] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, custom_fields) assert member_map[:email] == 0 # "Straße" is a member field (street), so it should be in member_map, not custom_map assert member_map[:street] == 1 assert custom_map == %{} assert unknown == [] end test "handles unknown custom field columns" do headers = ["Email", "UnknownCustomField"] custom_fields = [%{id: "cf1", name: "KnownField"}] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, custom_fields) assert member_map[:email] == 0 assert custom_map == %{} # UnknownCustomField should be in unknown list assert length(unknown) == 1 end test "handles duplicate custom field names after normalization" do headers = ["Email", "CustomField", "Custom Field"] custom_fields = [%{id: "cf1", name: "CustomField"}] # Both "CustomField" and "Custom Field" normalize to the same, so this should error assert {:error, reason} = HeaderMapper.build_maps(headers, custom_fields) assert reason =~ "duplicate" end test "maps all supported member fields" do headers = [ "Email", "First Name", "Last Name", "Street", "Postal Code", "City" ] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert member_map[:first_name] == 1 assert member_map[:last_name] == 2 assert member_map[:street] == 3 assert member_map[:postal_code] == 4 assert member_map[:city] == 5 assert custom_map == %{} assert unknown == [] end test "maps German member field variants" do headers = ["E-Mail", "Vorname", "Nachname", "Straße", "PLZ", "Stadt"] assert {:ok, %{member: member_map, custom: custom_map, unknown: unknown}} = HeaderMapper.build_maps(headers, []) assert member_map[:email] == 0 assert member_map[:first_name] == 1 assert member_map[:last_name] == 2 assert member_map[:street] == 3 assert member_map[:postal_code] == 4 assert member_map[:city] == 5 assert custom_map == %{} assert unknown == [] end end end