defmodule Mix.Tasks.Finitomata.Generate.Test do
  @shortdoc "Generates the test scaffold for the `Finitomata` instance"
  @moduledoc """
  Mix task to generate the `Finitomata.ExUnit` test scaffold.

  ### Allowed arguments

  - **`--module: :string`** __[mandatory]__ the name of the module the test to be generated for
  - **`--file: :string`** __[optional, default: derived from `module` name]__ the name of the file
    to store the test module definition in
  - **`--dir: :string`** __[optional, default: `test/finitomata`]__ the name of the directory
    to put the file in

  ### Example

  ```sh
  mix finitomata.generate.test --module MyApp.MyFSM
  ```
  """

  use Mix.Task

  @finitomata_tests_path "test/finitomata"

  @impl Mix.Task
  @doc false
  def run(args) do
    Mix.Task.run("compile")
    Finitomata.Mix.load_app()

    {opts, _pass_thru, []} =
      OptionParser.parse(args, strict: [module: :string, file: :string, dir: :string])

    module = Keyword.fetch!(opts, :module)

    test_dir = Keyword.get(opts, :dir, @finitomata_tests_path)
    module = Module.concat([module])

    test_file =
      Keyword.get(
        opts,
        :file,
        module |> Macro.underscore() |> Kernel.<>("_test.exs")
      )

    if module?(module) do
      paths =
        module.fsm()
        |> Finitomata.Transition.paths()
        |> Enum.map(&{&1, transform_path(module.__config__(:auto_terminate), &1)})

      test_module = Module.concat([module, "Test"])
      target_file = Path.join(test_dir, test_file)

      Mix.Generator.copy_template(
        Path.expand("test_template.eex", __DIR__),
        target_file,
        module: module,
        test_module: test_module,
        paths: paths
      )

      File.write!(target_file, Code.format_file!(target_file))

      Mix.shell().info([
        [:bright, :blue, "* #{inspect(test_module)}", :reset],
        " has been created for ",
        [:blue, inspect(module), :reset],
        ", do not forget to:\n",
        "  ▹ amend assertions to fit your business logic\n",
        "  ▹ add `listener: :mox` (or actual listener) to `Finitomata` declaration"
      ])
    end
  end

  @doc false
  defp transform_path(auto_terminate?, %Finitomata.Transition.Path{path: path}),
    do: transform_path(auto_terminate?, path)

  defp transform_path(auto_terminate?, path) do
    path
    |> Enum.reduce([], fn
      {event, state}, [] ->
        [[{event, state}]]

      {event, state}, [curr | rest] ->
        if event |> to_string() |> String.ends_with?("!"),
          do: [curr ++ [{event, state}] | rest],
          else: [[{event, state}], curr | rest]
    end)
    |> then(&maybe_join_ending(auto_terminate?, &1))
    |> Enum.reverse()
  end

  defp maybe_join_ending(false, path), do: path
  defp maybe_join_ending(true, [[{:__end__, :*}] = last, prev | rest]), do: [prev ++ last | rest]

  defp module?(module) do
    with {:module, ^module} <- Code.ensure_compiled(module),
         true <- function_exported?(module, :fsm, 0) do
      true
    else
      _ ->
        Mix.shell().error([
          :yellow,
          "* #{inspect(module)}",
          :reset,
          " is not found or is not a `Finitomata` instance"
        ])

        false
    end
  end
end
