lib/money/input/visualizer/parse_view.ex

defmodule Money.Input.Visualizer.ParseView do
  @moduledoc false

  # Cross-locale parse table. Take one input string and show
  # what `Localize.Number.Parser.parse/2` (number mode) or
  # `Money.parse/2` (money mode) returns under every demo locale,
  # with the currency fixed. Useful for seeing how `1.234,56`
  # means different things to en vs. de.

  alias Money.Input.Visualizer.Render

  def render(params, base) do
    input = params.input
    currency = params.currency
    mode = params.mode

    rows =
      for {locale, label} <- Render.locale_options() do
        result =
          case mode do
            :number ->
              Localize.Number.Parser.parse(input, locale: locale, number: :decimal)

            :money ->
              case Money.parse(input, locale: locale, default_currency: currency) do
                %Money{} = money -> {:ok, money}
                {:error, _} = err -> err
              end
          end

        {locale, label, result}
      end

    body = [
      "<section class=\"mi-card\">",
      "<h2>Cross-locale parsing</h2>",
      "<p class=\"mi-desc\">Same input, every locale. Demonstrates how ",
      "<code>Money.parse</code> interprets the decimal and ",
      "grouping separators per locale — and why a US user pasting ",
      "<code>1,234.56</code> while their app is on <code>de</code> ",
      "needs tolerant handling.</p>",
      "<form method=\"get\" action=\"",
      Render.escape(base),
      "/parse\" class=\"mi-form\">",
      "<div class=\"mi-field mi-field-wide\">",
      "<label><span>Input</span>",
      "<input type=\"text\" name=\"input\" value=\"",
      Render.escape(input),
      "\" autocomplete=\"off\"></label>",
      "</div>",
      Render.field("Mode", mode_select(mode)),
      Render.field("Currency (money mode)", Render.currency_select("currency", currency)),
      "<div class=\"mi-actions\">",
      "<button class=\"mi-btn\" type=\"submit\">Parse across locales</button>",
      "<span class=\"mi-hint\">Try <code>1,234.56</code>, <code>1.234,56</code>, ",
      "<code>1 234,56</code>, <code>(1234.56)</code>, <code>$1,234.56</code></span>",
      "</div>",
      "</form>",
      result_table(rows, mode),
      "</section>"
    ]

    Render.page(
      title: "Parse",
      active: "parse",
      base: base,
      body: body
    )
  end

  defp mode_select(selected) do
    options = [{"number", "Plain number"}, {"money", "Money"}]

    [
      "<select name=\"mode\">",
      Enum.map(options, fn {value, label} ->
        sel = if value == to_string(selected), do: " selected", else: ""

        [
          "<option value=\"",
          Render.escape(value),
          "\"",
          sel,
          ">",
          Render.escape(label),
          "</option>"
        ]
      end),
      "</select>"
    ]
  end

  defp result_table(rows, mode) do
    [
      "<table class=\"mi-table\">",
      "<thead><tr>",
      "<th>Locale</th><th>Result</th><th>Canonical</th><th>Round-trip</th>",
      "</tr></thead>",
      "<tbody>",
      Enum.map(rows, &result_row(&1, mode)),
      "</tbody>",
      "</table>"
    ]
  end

  defp result_row({locale, label, {:ok, nil}}, _mode) do
    [
      "<tr>",
      "<td>",
      Render.escape(locale),
      " <small>(",
      Render.escape(label),
      ")</small></td>",
      "<td colspan=\"3\"><em>(empty)</em></td>",
      "</tr>"
    ]
  end

  defp result_row({locale, label, {:ok, value}}, mode) do
    canonical = canonical_amount(value)

    round_trip =
      case mode do
        :number -> Localize.Number.to_string!(value, locale: locale)
        :money -> Money.to_string!(value, locale: locale)
      end

    [
      "<tr>",
      "<td>",
      Render.escape(locale),
      " <small>(",
      Render.escape(label),
      ")</small></td>",
      "<td class=\"mi-mono\">",
      Render.escape(describe_value(value)),
      "</td>",
      "<td class=\"mi-mono\">",
      Render.escape(canonical),
      "</td>",
      "<td class=\"mi-mono\">",
      Render.escape(round_trip),
      "</td>",
      "</tr>"
    ]
  end

  defp result_row({locale, label, {:error, reason}}, _mode) do
    [
      "<tr>",
      "<td>",
      Render.escape(locale),
      " <small>(",
      Render.escape(label),
      ")</small></td>",
      "<td colspan=\"3\" class=\"mi-bad mi-mono\">",
      Render.escape(inspect(reason)),
      "</td>",
      "</tr>"
    ]
  end

  defp canonical_amount(%Money{amount: amount}), do: Decimal.to_string(amount, :normal)
  defp canonical_amount(%Decimal{} = decimal), do: Decimal.to_string(decimal, :normal)
  defp canonical_amount(value) when is_integer(value), do: Integer.to_string(value)

  defp describe_value(%Money{} = money) do
    "#{money.currency} #{Decimal.to_string(money.amount, :normal)}"
  end

  defp describe_value(%Decimal{} = decimal), do: Decimal.to_string(decimal, :normal)
  defp describe_value(value), do: inspect(value)
end