defmodule Mv.Membership.MembersCSVTest do
  use ExUnit.Case, async: true

  alias Mv.Membership.MembersCSV

  describe "export/2" do
    test "returns CSV with header and one data row (member fields only)" do
      member = %{first_name: "Jane", email: "jane@example.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "First Name"
      assert csv =~ "Email"
      assert csv =~ "Jane"
      assert csv =~ "jane@example.com"
      lines = String.split(csv, "\n", trim: true)
      assert length(lines) == 2
    end

    test "header uses display labels not raw field names (regression guard)" do
      member = %{first_name: "Jane", email: "jane@example.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)
      header_line = csv |> String.split("\n", trim: true) |> hd()

      assert header_line =~ "First Name"
      assert header_line =~ "Email"
      refute header_line =~ "first_name"
      refute header_line =~ "email"
    end

    test "escapes cell containing comma (RFC 4180 quoted)" do
      member = %{first_name: "Doe, John", email: "john@example.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ ~s("Doe, John")
      assert csv =~ "john@example.com"
    end

    test "escapes cell containing double-quote (RFC 4180 doubled and quoted)" do
      member = %{first_name: ~s(He said "Hi"), email: "a@b.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ ~s("He said ""Hi""")
      assert csv =~ "a@b.com"
    end

    test "formats date as ISO8601 for member fields" do
      member = %{first_name: "D", email: "d@d.com", join_date: ~D[2024-03-15]}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "Join Date", kind: :member_field, key: "join_date"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "2024-03-15"
      assert csv =~ "Join Date"
    end

    test "formats nil as empty string" do
      member = %{first_name: "Only", last_name: nil, email: "x@y.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Last Name", kind: :member_field, key: "last_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "First Name"
      assert csv =~ "Only"
      assert csv =~ "x@y.com"
      assert csv =~ "Only,,x@y"
    end

    test "custom field column uses header and formats value" do
      custom_cf = %{id: "cf-1", name: "Active", value_type: :boolean}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "Active", kind: :custom_field, key: "cf-1", custom_field: custom_cf}
      ]

      member = %{
        first_name: "Test",
        email: "e@e.com",
        custom_field_values: [
          %{custom_field_id: "cf-1", value: true, custom_field: custom_cf}
        ]
      }

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "Active"
      assert csv =~ "Yes"
    end

    test "custom field uses display_name when present, else name" do
      custom_cf = %{id: "cf-a", name: "FieldA", value_type: :string}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{
          header: "Display Label",
          kind: :custom_field,
          key: "cf-a",
          custom_field: Map.put(custom_cf, :display_name, "Display Label")
        }
      ]

      member = %{
        first_name: "X",
        email: "x@x.com",
        custom_field_values: [
          %{custom_field_id: "cf-a", value: "only_a", custom_field: custom_cf}
        ]
      }

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "Display Label"
      assert csv =~ "only_a"
    end

    test "missing custom field value yields empty cell" do
      cf1 = %{id: "cf-a", name: "FieldA", value_type: :string}
      cf2 = %{id: "cf-b", name: "FieldB", value_type: :string}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "FieldA", kind: :custom_field, key: "cf-a", custom_field: cf1},
        %{header: "FieldB", kind: :custom_field, key: "cf-b", custom_field: cf2}
      ]

      member = %{
        first_name: "X",
        email: "x@x.com",
        custom_field_values: [%{custom_field_id: "cf-a", value: "only_a", custom_field: cf1}]
      }

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "First Name,Email,FieldA,FieldB"
      assert csv =~ "only_a"
      assert csv =~ "X,x@x.com,only_a,"
    end

    test "computed column exports membership fee status label" do
      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "Membership Fee Status", kind: :computed, key: :membership_fee_status}
      ]

      member = %{first_name: "M", email: "m@m.com", membership_fee_status: "Paid"}

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "Membership Fee Status"
      assert csv =~ "Paid"
      assert csv =~ "M,m@m.com,Paid"
    end

    test "CSV injection: formula-like and dangerous prefixes are escaped with apostrophe" do
      member = %{
        first_name: "=SUM(A1:A10)",
        last_name: "+1",
        email: "@cmd|evil"
      }

      custom_cf = %{id: "cf-1", name: "Note", value_type: :string}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Last Name", kind: :member_field, key: "last_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "Note", kind: :custom_field, key: "cf-1", custom_field: custom_cf}
      ]

      member_with_cf =
        Map.put(member, :custom_field_values, [
          %{custom_field_id: "cf-1", value: "normal text", custom_field: custom_cf}
        ])

      iodata = MembersCSV.export([member_with_cf], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "'=SUM(A1:A10)"
      assert csv =~ "'+1"
      assert csv =~ "'@cmd|evil"
      assert csv =~ "normal text"
      refute csv =~ ",'normal text"
    end

    test "CSV injection: minus and tab prefix are escaped" do
      member = %{first_name: "-2", last_name: "\tleading", email: "safe@x.com"}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Last Name", kind: :member_field, key: "last_name"},
        %{header: "Email", kind: :member_field, key: "email"}
      ]

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "'-2"
      assert csv =~ "'\tleading"
      assert csv =~ "safe@x.com"
    end

    test "column order is preserved (headers and values)" do
      cf1 = %{id: "a", name: "Custom1", value_type: :string}
      cf2 = %{id: "b", name: "Custom2", value_type: :string}

      columns = [
        %{header: "First Name", kind: :member_field, key: "first_name"},
        %{header: "Email", kind: :member_field, key: "email"},
        %{header: "Custom2", kind: :custom_field, key: "b", custom_field: cf2},
        %{header: "Custom1", kind: :custom_field, key: "a", custom_field: cf1}
      ]

      member = %{
        first_name: "M",
        email: "m@m.com",
        custom_field_values: [
          %{custom_field_id: "a", value: "v1", custom_field: cf1},
          %{custom_field_id: "b", value: "v2", custom_field: cf2}
        ]
      }

      iodata = MembersCSV.export([member], columns)
      csv = IO.iodata_to_binary(iodata)

      assert csv =~ "First Name,Email,Custom2,Custom1"
      assert csv =~ "M,m@m.com,v2,v1"
    end
  end
end
