lib/mix/tasks/comm_bus.compare_engines.ex

defmodule Mix.Tasks.CommBus.CompareEngines do
  use Mix.Task

  @shortdoc "Compare template engine outputs for golden fixtures"

  alias CommBus.Template
  alias CommBus.Template.Engine.{BbMustache, ExMustache}

  @doc """
  Compares BbMustache and ExMustache template engine outputs against golden
  fixture files, reporting matches and differences.

  ## Parameters

    - `_args` — Ignored.
  """
  @impl true
  def run(_args) do
    Mix.Task.run("app.start")

    configs = fixture_configs()

    fixture_paths()
    |> Enum.each(fn path ->
      {values, opts} = Map.get(configs, path, {%{}, [strict_mode: false]})
      compare_engines_and_report(path, values, opts)
    end)
  end

  defp compare_engines_and_report(rel_path, values, opts) do
    body = load_fixture_body(rel_path)
    opts = Keyword.merge([engine: BbMustache], opts)

    result_bb = Template.render(body, values, opts)
    result_ex = Template.render(body, values, Keyword.put(opts, :engine, ExMustache))

    case {result_bb, result_ex} do
      {{:ok, %{content: content_bb}}, {:ok, %{content: content_ex}}} ->
        if content_bb != content_ex do
          IO.puts("\n[template diff] #{rel_path}")
          print_diff(content_bb, content_ex)
        else
          IO.puts("[template match] #{rel_path}")
        end

      {error_bb, error_ex} ->
        IO.puts("\n[template error] #{rel_path}")
        IO.inspect(error_bb, label: "bbmustache")
        IO.inspect(error_ex, label: "ex_mustache")
    end
  end

  defp load_fixture_body(rel_path) do
    content = File.read!(Path.join(fixtures_dir(), rel_path))

    case String.split(content, "---\n", parts: 3) do
      ["", _front, body] -> body
      _other -> content
    end
  end

  defp print_diff(left, right) do
    left_lines = String.split(left, "\n")
    right_lines = String.split(right, "\n")

    Enum.zip(left_lines, right_lines)
    |> Enum.with_index(1)
    |> Enum.find(fn {{l, r}, _idx} -> l != r end)
    |> case do
      nil ->
        IO.puts("  No line differences detected.")

      {{l, r}, idx} ->
        IO.puts("  First diff at line #{idx}")
        IO.puts("  bb: #{l}")
        IO.puts("  ex: #{r}")
    end
  end

  defp milestone_values do
    %{
      "VERSION" => "1.0",
      "MILESTONE_NAME" => "Alpha",
      "DATE" => "2026-01-01",
      "PHASE_START" => "01",
      "PHASE_END" => "03",
      "TOTAL_PLANS" => "9",
      "MILESTONE_DESCRIPTION" => "Alpha milestone details.",
      "PHASES_SECTION" => "Phase overview section.",
      "PHASE_NUM" => "1",
      "PHASE_NAME" => "Discovery",
      "PHASE_GOAL" => "Clarify requirements",
      "DEPENDS_ON" => "None",
      "PLAN_COUNT" => "3",
      "PHASE" => "01",
      "PLAN_DESCRIPTION" => "Sample plan",
      "PHASE_DETAILS_FROM_ROADMAP" => "Roadmap details.",
      "DECISIONS_FROM_PROJECT_STATE" => "Decision log.",
      "ISSUES_RESOLVED_DURING_MILESTONE" => "Resolved issues.",
      "ISSUES_DEFERRED_TO_LATER" => "Deferred issues.",
      "SHORTCUTS_NEEDING_FUTURE_WORK" => "Tech debt.",
      "PLACEHOLDERS" => "PLACEHOLDERS"
    }
  end

  defp fixture_paths do
    Path.wildcard(Path.join(fixtures_dir(), "**/*.{md,yml,yaml}"))
    |> Enum.map(&Path.relative_to(&1, fixtures_dir()))
    |> Enum.sort()
  end

  defp fixtures_dir do
    Path.expand("test/fixtures/golden", File.cwd!())
  end

  defp fixture_configs do
    %{
      "devman/test_hello.md" => {%{"name" => "Ada"}, []},
      "devman/workflow_test_step_types.yml" =>
        {%{"step_1" => %{"output" => "x"}, "step_2" => %{"output" => "y"}}, []},
      "devman/partials_example.md" =>
        {%{"input" => "analysis"},
         [partials: %{"common/system_context" => "SYSTEM", "common/output_format" => "FORMAT"}]},
      "human/milestone_archive.md" => {milestone_values(), []}
    }
  end
end