defmodule DiscUnionTest.Specs do
  use ExUnit.Case, async: true
  alias Code.Typespec

  test "autogenerated specs for c! functions" do
    specs =
    for arr <- 1..3 do function_specs(ExampleDU, :c!, arr) end
    |> List.flatten
    |> Enum.sort
    correct_specs = [
      "@spec c!(Asd) :: %ExampleDU{case: Asd}",
      "@spec c!(Qwe, any()) :: %ExampleDU{case: {Qwe, any()}}",
      "@spec c!(Rty, integer(), atom()) :: %ExampleDU{case: {Rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs

    specs =
    for arr <- 1..3 do function_specs(ExampleDUa, :c!, arr) end
    |> List.flatten
    |> Enum.sort
    correct_specs = [
      "@spec c!(:asd) :: %ExampleDUa{case: :asd}",
      "@spec c!(:qwe, any()) :: %ExampleDUa{case: {:qwe, any()}}",
      "@spec c!(:rty, integer(), atom()) :: %ExampleDUa{case: {:rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs
  end

  test "autogenerated specs for from!/1" do
    specs = function_specs(ExampleDU, :from!, 1) |> Enum.sort
    correct_specs = [
      "@spec from!(Asd) :: %ExampleDU{case: Asd}",
      "@spec from!({Qwe, any()}) :: %ExampleDU{case: {Qwe, any()}}",
      "@spec from!({Rty, integer(), atom()}) :: %ExampleDU{case: {Rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs

    specs = function_specs(ExampleDUa, :from!, 1) |> Enum.sort
    correct_specs = [
      "@spec from!(:asd) :: %ExampleDUa{case: :asd}",
      "@spec from!({:qwe, any()}) :: %ExampleDUa{case: {:qwe, any()}}",
      "@spec from!({:rty, integer(), atom()}) :: %ExampleDUa{case: {:rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs
  end

  test "autogenerated specs for from!/2" do
    specs = function_specs(ExampleDU, :from!, 2) |> Enum.sort
    correct_specs = [
      "@spec from!(Asd, any()) :: %ExampleDU{case: Asd}",
      "@spec from!({Qwe, any()}, any()) :: %ExampleDU{case: {Qwe, any()}}",
      "@spec from!({Rty, integer(), atom()}, any()) :: %ExampleDU{case: {Rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs

    specs = function_specs(ExampleDUa, :from!, 2) |> Enum.sort
    correct_specs = [
      "@spec from!(:asd, any()) :: %ExampleDUa{case: :asd}",
      "@spec from!({:qwe, any()}, any()) :: %ExampleDUa{case: {:qwe, any()}}",
      "@spec from!({:rty, integer(), atom()}, any()) :: %ExampleDUa{case: {:rty, integer(), atom()}}",
    ] |> Enum.sort
    assert specs == correct_specs
  end


  # helper functions

  def function_specs(module, function, arity) when is_atom(module) and is_atom(function) and is_integer(arity) do
    case beam_specs(module) do
      nil   -> []
      specs ->
        for {_kind, {{^function, ^arity}, _spec}} = spec <- specs do
          spec_to_string(spec)
        end
        |> List.flatten
    end
  end

  defp beam_specs(module) do
    specs = beam_specs_tag(Typespec.fetch_specs(module), :spec)
    callbacks = beam_specs_tag(Typespec.fetch_callbacks(module), :callback)
    specs && callbacks && Enum.concat(specs, callbacks)
  end

  defp beam_specs_tag(:error, _), do: nil
  defp beam_specs_tag({:ok, specs}, tag) do
    Enum.map(specs, &{tag, &1})
  end

  defp spec_to_string({kind, {{name, _arity}, specs}}) do
    Enum.map specs, fn(spec) ->
      binary = Macro.to_string Typespec.spec_to_quoted(name, spec)
      "@#{kind} #{binary}"
    end
  end

end
