Fix 1.4 warning

Fix 1.4 warning

defmodule Keepex.Mixfile do
  use Mix.Project

  @version "0.1.0"

  def project do
    [app: :os_gateway,
     description: "Add description to your package.",
     package: package(),
     version: @version,
     elixir: "~> 1.3",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps(),
     test_coverage: [tool: ExCoveralls],
     preferred_cli_env: [coveralls: :test],
     docs: [source_ref: "v#\{@version\}", main: "readme", extras: ["README.md"]]]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger, :confex]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # To depend on another app inside the umbrella:
  #
  #   {:myapp, in_umbrella: true}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [{:distillery, "~> 0.9"},
     {:confex, "~> 1.4"},
     {:benchfella, "~> 0.3", only: [:dev, :test]},
     {:ex_doc, ">= 0.0.0", only: [:dev, :test]},
     {:excoveralls, "~> 0.5", only: [:dev, :test]},
     {:dogma, "> 0.1.0", only: [:dev, :test]},
     {:credo, ">= 0.4.8", only: [:dev, :test]}]
  end

  # Settings for publishing in Hex package manager:
  defp package do
    [contributors: ["Nebo #15"],
     maintainers: ["Nebo #15"],
     licenses: ["LISENSE.md"],
     links: %{github: "https://github.com/Nebo15/os.gateway"},
     files: ~w(lib LICENSE.md mix.exs README.md)]
  end
end

defmodule Keepex.Mixfile do
  use Mix.Project

  @version "0.1.0"

  def project do
    [app: :os_gateway,
     description: "Add description to your package.",
     package: package,
     version: @version,
     elixir: "~> 1.3",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps(),
     test_coverage: [tool: ExCoveralls],
     preferred_cli_env: [coveralls: :test],
     docs: [source_ref: "v#\{@version\}", main: "readme", extras: ["README.md"]]]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger, :confex]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # To depend on another app inside the umbrella:
  #
  #   {:myapp, in_umbrella: true}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [{:distillery, "~> 0.9"},
     {:confex, "~> 1.4"},
     {:benchfella, "~> 0.3", only: [:dev, :test]},
     {:ex_doc, ">= 0.0.0", only: [:dev, :test]},
     {:excoveralls, "~> 0.5", only: [:dev, :test]},
     {:dogma, "> 0.1.0", only: [:dev, :test]},
     {:credo, ">= 0.4.8", only: [:dev, :test]}]
  end

  # Settings for publishing in Hex package manager:
  defp package do
    [contributors: ["Nebo #15"],
     maintainers: ["Nebo #15"],
     licenses: ["LISENSE.md"],
     links: %{github: "https://github.com/Nebo15/os.gateway"},
     files: ~w(lib LICENSE.md mix.exs README.md)]
  end
end


Use mingw32-make on Windows

Use mingw32-make on Windows

It turns out that I have make on my system from something besides
mingw.

defmodule Mix.Tasks.Compile.NervesUart do
  @shortdoc "Compiles Nerves.UART C Code"

  def run(_) do
    File.mkdir("priv")
    {exec, args} = case :os.type do
      {:unix, os} when os in [:freebsd, :openbsd] ->
        {"gmake", ["all"]}
      {:win32, _} ->
        {"mingw32-make", ["all"]}
      _ ->
        {"make", ["all"]}
    end

    if System.find_executable(exec) do
      build(exec, args)
      Mix.Project.build_structure
      :ok
    else
      nocompiler_error(exec)
    end
  end

  def build(exec, args) do
    {result, error_code} = System.cmd(exec, args, stderr_to_stdout: true)
    IO.binwrite result
    if error_code != 0, do: build_error
  end

  defp nocompiler_error(exec) do
    raise Mix.Error, message: nocompiler_message(exec) <> help_message(:os.type)
  end
  defp build_error() do
    raise Mix.Error, message: build_message <> help_message(:os.type)
  end

  defp nocompiler_message(exec) do
    """
    Could not find the program `#{exec}`.

    You will need to install the C compiler `#{exec}` to be able to build
    Nerves.UART.

    """
  end

  defp build_message do
    """
    Could not compile Nerves.UART.

    Please make sure that you are using Erlang / OTP version 18.0 or later
    and that you have a C compiler installed.

    """
  end

  defp help_message({:win32, _}) do
    """
    You need to install MinGW and make sure that it is in your PATH. Test this by
    running `gcc --version` on the command line.

    If you do not have MinGW, one method to get it is to install it through
    Chocolatey. See http://chocolatey.org to install Chocolatey and run the
    following from and command prompt with administrative privileges:

    `choco install mingw`
    """
  end
  defp help_message(_) do
    """
    Please follow the directions below for the operating system you are
    using:

    Mac OS X: You need to have gcc and make installed. Try running the
    commands `gcc --version` and / or `make --version`. If these programs
    are not installed, you will be prompted to install them.

    Linux: You need to have gcc and make installed. If you are using
    Ubuntu or any other Debian-based system, install the packages
    `build-essential`. Also install `erlang-dev` package if not
    included in your Erlang/OTP version.

    """
  end
end

defmodule NervesUart.Mixfile do
  use Mix.Project

  @version "0.0.1"

  @description """
  Discover and use UARTs and serial ports in Elixir.
  """

  def project do
    [app: :nerves_uart,
     version: "0.0.1",
     elixir: "~> 1.2",
     name: "Nerves.UART",
     description: @description,
     package: package,
     source_url: "https://github.com/nerves-project/nerves_uart",
     compilers: [:nerves_uart] ++ Mix.compilers,
     docs: [extras: ["README.md"]],
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
      {:earmark, "~> 0.2", only: :dev},
      {:ex_doc,  "~> 0.11", only: :dev}
    ]
  end

  defp package do
    [
      files: ["lib", "c_src", "mix.exs", "Makefile*", "README.md", "LICENSE"],
      maintainers: ["Frank Hunleth"],
      licenses: ["Apache-2"],
      links: %{"GitHub" => "https://github.com/nerves-project/nerves_uart",
        "Docs" => "http://hexdocs.pm/nerves_uart"}
    ]
  end
end

defmodule Mix.Tasks.Compile.NervesUart do
  @shortdoc "Compiles Nerves.UART C Code"

  def run(_) do
    File.mkdir("priv")
    {exec, args} = case :os.type do
      {:unix, os} when os in [:freebsd, :openbsd] ->
        {"gmake", ["all"]}
      _ ->
        {"make", ["all"]}
    end

    if System.find_executable(exec) do
      build(exec, args)
      Mix.Project.build_structure
      :ok
    else
      nocompiler_error(exec)
    end
  end

  def build(exec, args) do
    {result, error_code} = System.cmd(exec, args, stderr_to_stdout: true)
    IO.binwrite result
    if error_code != 0, do: build_error
  end

  defp nocompiler_error(exec) do
    raise Mix.Error, message: nocompiler_message(exec) <> help_message(:os.type)
  end
  defp build_error() do
    raise Mix.Error, message: build_message <> help_message(:os.type)
  end

  defp nocompiler_message(exec) do
    """
    Could not find the program `#{exec}`.

    You will need to install the C compiler `#{exec}` to be able to build
    Nerves.UART.

    """
  end

  defp build_message do
    """
    Could not compile Nerves.UART.

    Please make sure that you are using Erlang / OTP version 18.0 or later
    and that you have a C compiler installed.

    """
  end

  defp help_message({:win32, _}) do
    """
    You need to install MinGW and make sure that it is in your PATH. Test this by
    running `gcc --version` on the command line.

    If you do not have MinGW, one method to get it is to install it through
    Chocolatey. See http://chocolatey.org to install Chocolatey and run the
    following from and command prompt with administrative privileges:

    `choco install mingw`
    """
  end
  defp help_message(_) do
    """
    Please follow the directions below for the operating system you are
    using:

    Mac OS X: You need to have gcc and make installed. Try running the
    commands `gcc --version` and / or `make --version`. If these programs
    are not installed, you will be prompted to install them.

    Linux: You need to have gcc and make installed. If you are using
    Ubuntu or any other Debian-based system, install the packages
    `build-essential`. Also install `erlang-dev` package if not
    included in your Erlang/OTP version.

    """
  end
end

defmodule NervesUart.Mixfile do
  use Mix.Project

  @version "0.0.1"

  @description """
  Discover and use UARTs and serial ports in Elixir.
  """

  def project do
    [app: :nerves_uart,
     version: "0.0.1",
     elixir: "~> 1.2",
     name: "Nerves.UART",
     description: @description,
     package: package,
     source_url: "https://github.com/nerves-project/nerves_uart",
     compilers: [:nerves_uart] ++ Mix.compilers,
     docs: [extras: ["README.md"]],
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
      {:earmark, "~> 0.2", only: :dev},
      {:ex_doc,  "~> 0.11", only: :dev}
    ]
  end

  defp package do
    [
      files: ["lib", "c_src", "mix.exs", "Makefile*", "README.md", "LICENSE"],
      maintainers: ["Frank Hunleth"],
      licenses: ["Apache-2"],
      links: %{"GitHub" => "https://github.com/nerves-project/nerves_uart",
        "Docs" => "http://hexdocs.pm/nerves_uart"}
    ]
  end
end


Fix bug in build_info_end

Fix bug in build_info_end

defmodule Api.Suffrages do
  import Ecto.Query, warn: false

  alias Api.Repo
  alias Api.Accounts.User
  alias Api.Teams.{Team, Membership}
  alias Api.Competitions
  alias Api.Competitions.Attendance
  alias Api.Suffrages.{Suffrage, Vote, PaperVote, Candidate}
  alias Ecto.{Multi, Changeset}

  def all_suffrages do
    Repo.all(Suffrage)
  end

  def create_suffrage(params) do
    changeset = Suffrage.changeset(
      %Suffrage{competition_id: Competitions.default_competition().id},
      params
    )

    Repo.insert(changeset)
  end

  def get_suffrage(id) do
    Repo.get(Suffrage, id)
  end

  def by_slug(slug) do
    Repo.get_by(Suffrage, slug: slug)
  end

  def update_suffrage(id, params) do
    suffrage = get_suffrage(id)

    Suffrage.changeset(suffrage, params)
    |> Repo.update
  end

  def delete_suffrage(id) do
    suffrage = get_suffrage(id)

    suffrage
    |> Repo.delete
  end

  def suffrage_status(id) do
    s = get_suffrage(id)
    now = DateTime.utc_now

    cond do
      s.voting_ended_at && DateTime.compare(s.voting_ended_at, now) == :lt -> :ended
      s.voting_started_at && DateTime.compare(s.voting_started_at, now) == :lt -> :started
      true -> :not_started
    end
  end

  def suffrage_voting_started_at(id) do
    get_suffrage(id).voting_started_at
  end

  def suffrage_voting_ended_at(id) do
    get_suffrage(id).voting_ended_at
  end

  def suffrage_summary(id) do
    %{
      status: suffrage_status(id),
      unredeemed_paper_votes: unredeemed_paper_votes(id),
      missing_voters: missing_voters(),
    }
  end

  def start_suffrage(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    case suffrage_status(suffrage_id) do
      :not_started ->
        create_candidates(suffrage_id)
        assign_tie_breakers(suffrage_id)
        assign_missing_preferences(suffrage.competition_id)
        update_suffrage(suffrage_id, %{voting_started_at: DateTime.utc_now})
      :started -> :already_started
      :ended -> :already_ended
    end
  end

  def candidates(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: c
    )
  end

  def create_candidate(params) do
    changeset = Candidate.changeset(%Candidate{}, params)

    Repo.insert(changeset)
  end

  def update_candidate(candidate, params) do
    Candidate.changeset(candidate, params) |> Repo.update()
  end

  def create_candidates(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    from(t in Team, where: t.competition_id == ^suffrage.competition_id and t.eligible == ^true)
    |> Repo.all()
    |> Enum.each(fn team ->
      create_candidate(%{team_id: team.id, suffrage_id: suffrage_id})
    end)
  end

  def end_suffrage(suffrage_id) do
    case suffrage_status(suffrage_id) do
      :not_started -> :not_started
      :started ->
        at = DateTime.utc_now
        resolve_suffrage!(suffrage_id, at)
        update_suffrage(suffrage_id, %{voting_ended_at: at})
      :ended -> :already_ended
    end
  end

  def upsert_votes(user, votes) do
    multi = Multi.new()

    attendance = Competitions.get_attendance(Competitions.default_competition.id, user.id)

    votes_length = Enum.reduce(votes, 0, fn {_, ballot}, acc -> acc + length(ballot) end)

    valid_votes_length = Enum.reduce(votes, 0, fn {suffrage_id, ballot}, acc ->
      if validate_ballot(suffrage_id, ballot, user), do: acc + length(ballot), else: acc
    end)

    if votes_length != valid_votes_length do
      throw {:error, "Invalid vote"}
    end

    multi = Enum.reduce(votes, multi, fn {suffrage_id, ballot}, acc ->

      case suffrage_status(suffrage_id) do
        :not_started -> throw {:error, :not_started}
        :ended -> throw {:error, :already_ended}
        _ -> nil
      end

      Multi.insert_or_update(acc,
        suffrage_id,
        Vote.changeset(
          get_struct(user, suffrage_id),
          %{
            voter_identity: attendance.voter_identity,
            suffrage_id: suffrage_id,
            ballot: ballot,
          }
        )
      )
    end)

    Repo.transaction(multi)
  catch
    e -> e
  end

  def get_votes(user) do
    votes = from(
      v in Vote,
      join: a in Attendance,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^Competitions.default_competition.id,
      where: a.voter_identity == v.voter_identity,
    )
    |> Repo.all

    {:ok, votes}
  end

  def valid_voters(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      join: s in assoc(c, :suffrage),
      left_join: t in assoc(c, :team),
      left_join: m in assoc(t, :members),
      where: s.competition_id == ^competition_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: m
    )
  end

  def build_info_start(competition_id) do
    suffrages = all_suffrages()
    suffrage = List.first(suffrages)
    begun_at = suffrage_voting_started_at(suffrage.id)
    teams =
      candidates(suffrage.id, begun_at)
      |> Repo.all
      |> Repo.preload(:team) |> Enum.map(&(&1.team))

    %{
      participants: %{
        initial_count:
          Repo.aggregate(valid_voters(competition_id, begun_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(competition_id, begun_at), :count, :id),
      },
      teams: teams |> Enum.map(&(&1.name)),
    } |> Poison.encode!
  end

  def build_info_end(competition_id) do
    suffrages = all_suffrages()
    suffrage = List.first(suffrages)

    case suffrage_status(suffrage.id) do
      :not_started -> throw {:error, :not_started}
      :started -> throw {:error, :ongoing}
      :ended -> nil
    end

    begun_at = suffrage_voting_started_at(suffrage.id)
    ended_at = suffrage_voting_ended_at(suffrage.id)
    teams = candidates(suffrage.id, begun_at) |> Repo.all |> Repo.preload(:team) |> Enum.map(&(&1.team))
    team_name_map =
      teams
      |> Map.new(&{&1.id, &1.name})

    %{
      participants: %{
        initial_count:
          Repo.aggregate(valid_voters(competition_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(valid_voters(competition_id, ended_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(competition_id, begun_at), :count, :id) ,
        final_count:
          Repo.aggregate(redeemed_paper_votes(competition_id, ended_at), :count, :id),
      },
      teams: teams |> Enum.map(&(&1.name)),
      podiums: suffrages |> Map.new(fn s -> {s.name, s.podium |> Enum.map(&team_name_map[&1])} end),
      categories_to_votes: suffrages |> Map.new(fn s ->
        {
          s.name,
          ballots(s.id, ended_at)
          |> Map.new(fn {id, ballot} ->
              {
                id,
                ballot
                |> Enum.map(&team_name_map[&1])
              }
            end),
        }
      end),
    } |> Poison.encode!
  catch
    {:error, :not_started} -> nil
    {:error, :ongoing} -> nil
  end

  defp validate_ballot(suffrage_id, votes, user), do: validate_ballot(suffrage_id, votes, user, [])
  defp validate_ballot(_, [], _, acc), do: Enum.all?(acc)
  defp validate_ballot(suffrage_id, [vote|rest], user, acc), do: validate_ballot(
    suffrage_id,
    rest,
    user,
    acc ++ [validate_vote(suffrage_id, vote, user)]
  )

  defp validate_vote(suffrage_id, vote, user) do
    vote
    |> on_valid_team()
    |> on_votable_team(suffrage_id)
    |> not_on_own_team(user)
  end

  defp on_valid_team(vote) do
    Repo.get(Team, vote)
  end

  defp on_votable_team(nil, _), do: nil
  defp on_votable_team(team, suffrage_id) do
    teams = candidates(suffrage_id) |> Repo.all() |> Enum.map(&(&1.team_id))

    case team.id in teams do
      true -> team
      false -> nil
    end
  end

  defp not_on_own_team(nil, _), do: false
  defp not_on_own_team(team, user) do
    user_team = from(
      m in Membership,
      join: t in Team,
      where: m.user_id == ^user.id,
      where: t.competition_id == ^Competitions.default_competition().id,
      where: m.team_id == t.id
    ) |> Repo.one()

    user_team == nil || user_team.team_id != team.id
  end

  defp get_struct(user, suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    query = from v in Vote,
      join: a in Attendance,
      where: v.suffrage_id == ^suffrage_id,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^suffrage.competition_id,
      where: a.voter_identity == v.voter_identity

    case Repo.one(query) do
      nil -> %Vote{}
      vote -> vote
    end
  end

  def ballots(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    votes =
      Repo.all(from(
        v in Vote,
        where: v.suffrage_id == ^suffrage_id,
      ))
      |> Enum.map(&({&1.voter_identity, &1.ballot}))

    paper_votes =
      Repo.all(from(
        pv in redeemed_paper_votes_in_suffrage(suffrage_id, at),
        where: pv.suffrage_id == ^(suffrage_id)
      ))
      |> Enum.map(&({&1.id, [&1.team_id]}))

    (paper_votes ++ votes)
  end

  def resolve_suffrage!(suffrage_id, at \\ nil) do
    get_suffrage(suffrage_id)
    |> Changeset.change(podium: calculate_podium(suffrage_id, at))
    |> Repo.update!
  end

  def calculate_podium(suffrage_id, at \\ nil) do
    candidates = candidates(suffrage_id, at)
    |> Repo.all()

    valid_team_ids =
      candidates
      |> Enum.map(&(&1.team_id))

    votes =
      ballots(suffrage_id, at)
      |> clean_votes_into_ballots(valid_team_ids)

    tie_breakers =
      candidates
      |> Map.new(fn t ->
        {
          t.team_id,
          t.tie_breaker,
        }
      end)

    calculate_podium(votes, valid_team_ids, tie_breakers)
  end

  def clean_votes_into_ballots(votes, valid_team_ids) do
    votes
    |> Enum.map(fn {_id, ballot} ->
      ballot
      |> Enum.filter(&Enum.member?(valid_team_ids, &1))
    end)
    |> Enum.reject(&Enum.empty?/1)
  end

  def calculate_podium(votes, team_ids, tie_breakers) do
    votes
    |> Enum.flat_map(&Markus.ballot_to_pairs(&1, team_ids))
    |> Markus.pairs_to_preferences(team_ids)
    |> Markus.normalize_margins(team_ids)
    |> Markus.widen_paths(team_ids)
    |> Markus.sort_candidates_with_tie_breakers(team_ids, tie_breakers)
    |> Enum.take(3)
  end

  def missing_voters do
    competition_id = Competitions.default_competition().id

    voters = from(
      v in Vote,
      join: u in User,
      join: a in assoc(u, :attendances),
      where: v.voter_identity == a.voter_identity,
      where: a.competition_id == ^competition_id,
      select: u.id
    ) |> Repo.all()

    from(
      u2 in User,
      join: a in assoc(u2, :attendances),
      where: a.competition_id == ^competition_id,
      where: not u2.id in ^voters,
    )
    |> Repo.all()
  end

  def valid_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      join: s in assoc(pv, :suffrage),
      where: s.competition_id == ^competition_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def valid_paper_votes_in_suffrage(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      where: pv.suffrage_id == ^suffrage_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def redeemed_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(competition_id, at),
      where: not is_nil(pv.team_id)
  end

  def redeemed_paper_votes_in_suffrage(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes_in_suffrage(suffrage_id, at),
      where: not is_nil(pv.team_id)
  end

  def unredeemed_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(competition_id, at),
      where: is_nil(pv.redeemed_at)
  end

  def get_paper_vote(id) do
    Repo.get!(PaperVote, id)
    |> Repo.preload(:suffrage)
  end

  def create_paper_vote(suffrage_id, admin) do
    case suffrage_status(suffrage_id) do
      :ended -> :already_ended
      _ ->
        {
          :ok,
          %PaperVote{}
          |> PaperVote.changeset(%{
            created_by_id: admin.id,
            suffrage_id: suffrage_id,
          })
          |> Repo.insert!
        }
    end
  end

  def redeem_paper_vote(paper_vote, team, member, suffrage, admin, at \\ nil) do
    at = at || DateTime.utc_now

    candidate = from(
      c in Candidate,
      where: c.team_id == ^team.id,
      where: c.suffrage_id == ^suffrage.id,
      where: is_nil(c.disqualified_at)
    ) |> Repo.one()

    cond do
      suffrage_status(paper_vote.suffrage_id) == :not_started -> {:error, :not_started}
      suffrage_status(paper_vote.suffrage_id) == :ended -> {:error, :already_ended}
      paper_vote.redeemed_at -> {:error, :already_redeemed}
      paper_vote.annulled_at -> {:error, :annulled}
      is_nil(candidate) -> {:error, :team_not_candidate}
      !is_nil(candidate.disqualified_at) -> {:error, :team_disqualified}
      true ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            redeemed_at: at,
            redeeming_admin_id: admin.id,
            redeeming_member_id: member.id,
            team_id: team.id,
          })
          |> Repo.update!
        }
    end
  end

  def annul_paper_vote(paper_vote, admin, suffrage, at \\ nil) do
    at = at || DateTime.utc_now

    case suffrage_status(suffrage.id) do
      :ended -> {:error, :already_ended}
      _ ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            annulled_at: at,
            annulled_by_id: admin.id,
          })
          |> Repo.update!
          |> Repo.preload(:suffrage)
        }
    end
  end

  def disqualify_team(team_id, suffrage_id, admin) do
    from(
      c in Candidate,
      where: c.team_id == ^team_id,
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at),
      update: [set: [
        disqualified_at: ^(DateTime.utc_now),
        disqualified_by_id: ^(admin.id),
      ]]
    )
    |> Repo.update_all([])
  end

  def is_disqualified(team_id) do
    candidates = from(
      c in Candidate,
      join: s in assoc(c, :suffrage),
      join: comp in assoc(s, :competition),
      where: comp.id == ^Competitions.default_competition.id,
      where: c.team_id == ^team_id,
      where: not is_nil(c.disqualified_at),
    )
    |> Repo.all()

    length(candidates) > 0
  end

  def assign_tie_breakers(suffrage_id) do
    candidates = candidates(suffrage_id) |> Repo.all()

    tie_breakers = 1..Enum.count(candidates) |> Enum.shuffle()

    Enum.reduce(candidates, tie_breakers, fn candidate, acc ->
      {number, remaining} = List.pop_at(acc, 0)
      update_candidate(candidate, %{tie_breaker: number})
      remaining
    end)
  end

  def assign_missing_preferences(competition_id) do
    suffrages = Repo.all(Suffrage) |> Enum.map(&(&1.id))

    Repo.all(from(t in Team,
      where: t.competition_id == ^competition_id and is_nil(t.prize_preference))
    )
    |> Enum.map(fn t ->
      t
      |> Changeset.change(prize_preference: suffrages |> Enum.shuffle)
      |> Repo.update!
    end)
  end
end

defmodule Api.Suffrages do
  import Ecto.Query, warn: false

  alias Api.Repo
  alias Api.Accounts.User
  alias Api.Teams.{Team, Membership}
  alias Api.Competitions
  alias Api.Competitions.Attendance
  alias Api.Suffrages.{Suffrage, Vote, PaperVote, Candidate}
  alias Ecto.{Multi, Changeset}

  def all_suffrages do
    Repo.all(Suffrage)
  end

  def create_suffrage(params) do
    changeset = Suffrage.changeset(
      %Suffrage{competition_id: Competitions.default_competition().id},
      params
    )

    Repo.insert(changeset)
  end

  def get_suffrage(id) do
    Repo.get(Suffrage, id)
  end

  def by_slug(slug) do
    Repo.get_by(Suffrage, slug: slug)
  end

  def update_suffrage(id, params) do
    suffrage = get_suffrage(id)

    Suffrage.changeset(suffrage, params)
    |> Repo.update
  end

  def delete_suffrage(id) do
    suffrage = get_suffrage(id)

    suffrage
    |> Repo.delete
  end

  def suffrage_status(id) do
    s = get_suffrage(id)
    now = DateTime.utc_now

    cond do
      s.voting_ended_at && DateTime.compare(s.voting_ended_at, now) == :lt -> :ended
      s.voting_started_at && DateTime.compare(s.voting_started_at, now) == :lt -> :started
      true -> :not_started
    end
  end

  def suffrage_voting_started_at(id) do
    get_suffrage(id).voting_started_at
  end

  def suffrage_voting_ended_at(id) do
    get_suffrage(id).voting_ended_at
  end

  def suffrage_summary(id) do
    %{
      status: suffrage_status(id),
      unredeemed_paper_votes: unredeemed_paper_votes(id),
      missing_voters: missing_voters(),
    }
  end

  def start_suffrage(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    case suffrage_status(suffrage_id) do
      :not_started ->
        create_candidates(suffrage_id)
        assign_tie_breakers(suffrage_id)
        assign_missing_preferences(suffrage.competition_id)
        update_suffrage(suffrage_id, %{voting_started_at: DateTime.utc_now})
      :started -> :already_started
      :ended -> :already_ended
    end
  end

  def candidates(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      left_join: t in assoc(c, :team),
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: c
    )
  end

  def create_candidate(params) do
    changeset = Candidate.changeset(%Candidate{}, params)

    Repo.insert(changeset)
  end

  def update_candidate(candidate, params) do
    Candidate.changeset(candidate, params) |> Repo.update()
  end

  def create_candidates(suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    from(t in Team, where: t.competition_id == ^suffrage.competition_id and t.eligible == ^true)
    |> Repo.all()
    |> Enum.each(fn team ->
      create_candidate(%{team_id: team.id, suffrage_id: suffrage_id})
    end)
  end

  def end_suffrage(suffrage_id) do
    case suffrage_status(suffrage_id) do
      :not_started -> :not_started
      :started ->
        at = DateTime.utc_now
        resolve_suffrage!(suffrage_id, at)
        update_suffrage(suffrage_id, %{voting_ended_at: at})
      :ended -> :already_ended
    end
  end

  def upsert_votes(user, votes) do
    multi = Multi.new()

    attendance = Competitions.get_attendance(Competitions.default_competition.id, user.id)

    votes_length = Enum.reduce(votes, 0, fn {_, ballot}, acc -> acc + length(ballot) end)

    valid_votes_length = Enum.reduce(votes, 0, fn {suffrage_id, ballot}, acc ->
      if validate_ballot(suffrage_id, ballot, user), do: acc + length(ballot), else: acc
    end)

    if votes_length != valid_votes_length do
      throw {:error, "Invalid vote"}
    end

    multi = Enum.reduce(votes, multi, fn {suffrage_id, ballot}, acc ->

      case suffrage_status(suffrage_id) do
        :not_started -> throw {:error, :not_started}
        :ended -> throw {:error, :already_ended}
        _ -> nil
      end

      Multi.insert_or_update(acc,
        suffrage_id,
        Vote.changeset(
          get_struct(user, suffrage_id),
          %{
            voter_identity: attendance.voter_identity,
            suffrage_id: suffrage_id,
            ballot: ballot,
          }
        )
      )
    end)

    Repo.transaction(multi)
  catch
    e -> e
  end

  def get_votes(user) do
    votes = from(
      v in Vote,
      join: a in Attendance,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^Competitions.default_competition.id,
      where: a.voter_identity == v.voter_identity,
    )
    |> Repo.all

    {:ok, votes}
  end

  def valid_voters(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      c in Candidate,
      join: s in assoc(c, :suffrage),
      left_join: t in assoc(c, :team),
      left_join: m in assoc(t, :members),
      where: s.competition_id == ^competition_id,
      where: is_nil(c.disqualified_at) or c.disqualified_at > ^at,
      select: m
    )
  end

  def build_info_start(competition_id) do
    suffrages = all_suffrages()
    suffrage = List.first(suffrages)
    begun_at = suffrage_voting_started_at(suffrage.id)
    teams =
      candidates(suffrage.id, begun_at)
      |> Repo.all
      |> Repo.preload(:team) |> Enum.map(&(&1.team))

    %{
      participants: %{
        initial_count:
          Repo.aggregate(valid_voters(competition_id, begun_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(competition_id, begun_at), :count, :id),
      },
      teams: teams |> Enum.map(&(&1.name)),
    } |> Poison.encode!
  end

  def build_info_end(competition_id) do
    suffrages = all_suffrages()
    suffrage = List.first(suffrages)

    case suffrage_status(suffrage.id) do
      :not_started -> throw {:error, :not_started}
      :started -> throw {:error, :ongoing}
      :ended -> nil
    end

    begun_at = suffrage_voting_started_at(suffrage.id)
    ended_at = suffrage_voting_ended_at(suffrage.id)
    teams = candidates(suffrage.id, begun_at) |> Repo.all |> Repo.preload(:team) |> Enum.map(&(&1.team))
    team_name_map =
      teams
      |> Map.new(&{&1.id, &1.name})

    %{
      participants: %{
        initial_count:
          Repo.aggregate(valid_voters(competition_id, begun_at), :count, :id),
        final_count:
          Repo.aggregate(valid_voters(competition_id, ended_at), :count, :id),
      },
      paper_votes: %{
        initial_count:
          Repo.aggregate(valid_paper_votes(competition_id, begun_at), :count, :id) ,
        final_count:
          Repo.aggregate(redeemed_paper_votes(competition_id, ended_at), :count, :id),
      },
      teams: teams |> Enum.map(&(&1.name)),
      podiums: suffrages |> Map.new(fn s -> {s.name, s.podium |> Enum.map(&team_name_map[&1])} end),
      categories_to_votes: suffrages |> Map.new(fn s ->
        {
          s.name,
          ballots(suffrage.id, ended_at)
          |> Map.new(fn {id, ballot} ->
              {
                id,
                ballot
                |> Enum.map(&team_name_map[&1])
              }
            end),
        }
      end),
    } |> Poison.encode!
  catch
    {:error, :not_started} -> nil
    {:error, :ongoing} -> nil
  end

  defp validate_ballot(suffrage_id, votes, user), do: validate_ballot(suffrage_id, votes, user, [])
  defp validate_ballot(_, [], _, acc), do: Enum.all?(acc)
  defp validate_ballot(suffrage_id, [vote|rest], user, acc), do: validate_ballot(
    suffrage_id,
    rest,
    user,
    acc ++ [validate_vote(suffrage_id, vote, user)]
  )

  defp validate_vote(suffrage_id, vote, user) do
    vote
    |> on_valid_team()
    |> on_votable_team(suffrage_id)
    |> not_on_own_team(user)
  end

  defp on_valid_team(vote) do
    Repo.get(Team, vote)
  end

  defp on_votable_team(nil, _), do: nil
  defp on_votable_team(team, suffrage_id) do
    teams = candidates(suffrage_id) |> Repo.all() |> Enum.map(&(&1.team_id))

    case team.id in teams do
      true -> team
      false -> nil
    end
  end

  defp not_on_own_team(nil, _), do: false
  defp not_on_own_team(team, user) do
    user_team = from(
      m in Membership,
      join: t in Team,
      where: m.user_id == ^user.id,
      where: t.competition_id == ^Competitions.default_competition().id,
      where: m.team_id == t.id
    ) |> Repo.one()

    user_team == nil || user_team.team_id != team.id
  end

  defp get_struct(user, suffrage_id) do
    suffrage = get_suffrage(suffrage_id)
    query = from v in Vote,
      join: a in Attendance,
      where: v.suffrage_id == ^suffrage_id,
      where: a.attendee == ^user.id,
      where: a.competition_id == ^suffrage.competition_id,
      where: a.voter_identity == v.voter_identity

    case Repo.one(query) do
      nil -> %Vote{}
      vote -> vote
    end
  end

  def ballots(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    votes =
      Repo.all(from(
        v in Vote,
        where: v.suffrage_id == ^suffrage_id,
      ))
      |> Enum.map(&({&1.voter_identity, &1.ballot}))

    paper_votes =
      Repo.all(from(
        pv in redeemed_paper_votes_in_suffrage(suffrage_id, at),
        where: pv.suffrage_id == ^(suffrage_id)
      ))
      |> Enum.map(&({&1.id, [&1.team_id]}))

    (paper_votes ++ votes)
  end

  def resolve_suffrage!(suffrage_id, at \\ nil) do
    get_suffrage(suffrage_id)
    |> Changeset.change(podium: calculate_podium(suffrage_id, at))
    |> Repo.update!
  end

  def calculate_podium(suffrage_id, at \\ nil) do
    candidates = candidates(suffrage_id, at)
    |> Repo.all()

    valid_team_ids =
      candidates
      |> Enum.map(&(&1.team_id))

    votes =
      ballots(suffrage_id, at)
      |> clean_votes_into_ballots(valid_team_ids)

    tie_breakers =
      candidates
      |> Map.new(fn t ->
        {
          t.team_id,
          t.tie_breaker,
        }
      end)

    calculate_podium(votes, valid_team_ids, tie_breakers)
  end

  def clean_votes_into_ballots(votes, valid_team_ids) do
    votes
    |> Enum.map(fn {_id, ballot} ->
      ballot
      |> Enum.filter(&Enum.member?(valid_team_ids, &1))
    end)
    |> Enum.reject(&Enum.empty?/1)
  end

  def calculate_podium(votes, team_ids, tie_breakers) do
    votes
    |> Enum.flat_map(&Markus.ballot_to_pairs(&1, team_ids))
    |> Markus.pairs_to_preferences(team_ids)
    |> Markus.normalize_margins(team_ids)
    |> Markus.widen_paths(team_ids)
    |> Markus.sort_candidates_with_tie_breakers(team_ids, tie_breakers)
    |> Enum.take(3)
  end

  def missing_voters do
    competition_id = Competitions.default_competition().id

    voters = from(
      v in Vote,
      join: u in User,
      join: a in assoc(u, :attendances),
      where: v.voter_identity == a.voter_identity,
      where: a.competition_id == ^competition_id,
      select: u.id
    ) |> Repo.all()

    from(
      u2 in User,
      join: a in assoc(u2, :attendances),
      where: a.competition_id == ^competition_id,
      where: not u2.id in ^voters,
    )
    |> Repo.all()
  end

  def valid_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      join: s in assoc(pv, :suffrage),
      where: s.competition_id == ^competition_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def valid_paper_votes_in_suffrage(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from(
      pv in PaperVote,
      where: pv.suffrage_id == ^suffrage_id,
      where: is_nil(pv.annulled_at) or pv.annulled_at > ^at
    )
  end

  def redeemed_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(competition_id, at),
      where: not is_nil(pv.team_id)
  end

  def redeemed_paper_votes_in_suffrage(suffrage_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes_in_suffrage(suffrage_id, at),
      where: not is_nil(pv.team_id)
  end

  def unredeemed_paper_votes(competition_id, at \\ nil) do
    at = at || DateTime.utc_now

    from pv in valid_paper_votes(competition_id, at),
      where: is_nil(pv.redeemed_at)
  end

  def get_paper_vote(id) do
    Repo.get!(PaperVote, id)
    |> Repo.preload(:suffrage)
  end

  def create_paper_vote(suffrage_id, admin) do
    case suffrage_status(suffrage_id) do
      :ended -> :already_ended
      _ ->
        {
          :ok,
          %PaperVote{}
          |> PaperVote.changeset(%{
            created_by_id: admin.id,
            suffrage_id: suffrage_id,
          })
          |> Repo.insert!
        }
    end
  end

  def redeem_paper_vote(paper_vote, team, member, suffrage, admin, at \\ nil) do
    at = at || DateTime.utc_now

    candidate = from(
      c in Candidate,
      where: c.team_id == ^team.id,
      where: c.suffrage_id == ^suffrage.id,
      where: is_nil(c.disqualified_at)
    ) |> Repo.one()

    cond do
      suffrage_status(paper_vote.suffrage_id) == :not_started -> {:error, :not_started}
      suffrage_status(paper_vote.suffrage_id) == :ended -> {:error, :already_ended}
      paper_vote.redeemed_at -> {:error, :already_redeemed}
      paper_vote.annulled_at -> {:error, :annulled}
      is_nil(candidate) -> {:error, :team_not_candidate}
      !is_nil(candidate.disqualified_at) -> {:error, :team_disqualified}
      true ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            redeemed_at: at,
            redeeming_admin_id: admin.id,
            redeeming_member_id: member.id,
            team_id: team.id,
          })
          |> Repo.update!
        }
    end
  end

  def annul_paper_vote(paper_vote, admin, suffrage, at \\ nil) do
    at = at || DateTime.utc_now

    case suffrage_status(suffrage.id) do
      :ended -> {:error, :already_ended}
      _ ->
        {
          :ok,
          paper_vote
          |> PaperVote.changeset(%{
            annulled_at: at,
            annulled_by_id: admin.id,
          })
          |> Repo.update!
          |> Repo.preload(:suffrage)
        }
    end
  end

  def disqualify_team(team_id, suffrage_id, admin) do
    from(
      c in Candidate,
      where: c.team_id == ^team_id,
      where: c.suffrage_id == ^suffrage_id,
      where: is_nil(c.disqualified_at),
      update: [set: [
        disqualified_at: ^(DateTime.utc_now),
        disqualified_by_id: ^(admin.id),
      ]]
    )
    |> Repo.update_all([])
  end

  def is_disqualified(team_id) do
    candidates = from(
      c in Candidate,
      join: s in assoc(c, :suffrage),
      join: comp in assoc(s, :competition),
      where: comp.id == ^Competitions.default_competition.id,
      where: c.team_id == ^team_id,
      where: not is_nil(c.disqualified_at),
    )
    |> Repo.all()

    length(candidates) > 0
  end

  def assign_tie_breakers(suffrage_id) do
    candidates = candidates(suffrage_id) |> Repo.all()

    tie_breakers = 1..Enum.count(candidates) |> Enum.shuffle()

    Enum.reduce(candidates, tie_breakers, fn candidate, acc ->
      {number, remaining} = List.pop_at(acc, 0)
      update_candidate(candidate, %{tie_breaker: number})
      remaining
    end)
  end

  def assign_missing_preferences(competition_id) do
    suffrages = Repo.all(Suffrage) |> Enum.map(&(&1.id))

    Repo.all(from(t in Team,
      where: t.competition_id == ^competition_id and is_nil(t.prize_preference))
    )
    |> Enum.map(fn t ->
      t
      |> Changeset.change(prize_preference: suffrages |> Enum.shuffle)
      |> Repo.update!
    end)
  end
end


fixing a test to also work in Windows
used to do this:
'MIX_ENV' is not recognized as an internal or external command,
operable program or batch file.
Tested to work on both Windows and Linux.
However the following is seen on console:
Mix requires the compile path "ebin" to be inside a directory with the
 same name as the application name :no_mixfile,
got "s:/workspace.elixir.2013/elixir/lib/mix/tmp/Mix.CLITest/test env/ebin"

quick test with `make test_mix`

Code.require_file "../test_helper.exs", __DIR__

defmodule Mix.CLITest do
  use MixTest.Case

  test "env" do
    in_fixture "only_mixfile", fn ->
      if match? { :win32, _ }, :os.type do
        temp_env = "set MIX_ENV=prod &"
      else # ie. linux
        temp_env = "MIX_ENV=prod"
      end

      env = System.cmd %b(#{temp_env} #{elixir_executable} #{mix_executable} run -e "IO.puts Mix.env")
      assert env =~ "prod"
    end
  end

  test "default task" do
    in_fixture "no_mixfile", fn ->
      output = mix ""
      assert File.regular?("ebin/Elixir.A.beam")
      assert output =~ "Compiled lib/a.ex"
    end
  end

  test "invoke simple task from CLI" do
    in_fixture "only_mixfile", fn ->
      assert mix("hello") == "Hello from MyProject!\n"
    end
  end

  test "--help smoke test" do
    in_fixture "only_mixfile", fn ->
      output = mix "--help"
      assert output =~ %r/mix compile\s+# Compile source files/
      refute output =~ "mix invalid"
    end
  end

  test "--version smoke test" do
    in_fixture "only_mixfile", fn ->
      output = mix "--version"
      assert output =~ %r/Elixir [0-9\.a-z]+/
    end
  end

  test "new with tests smoke test" do
    in_tmp "new_with_tests", fn ->
      output = mix "new ."
      assert output =~ "* creating lib/new_with_tests.ex"

      output = mix "test"
      assert File.regular?("ebin/Elixir.NewWithTests.beam")
      assert output =~ "1 tests, 0 failures"
    end
  end

  test "new --sup with tests smoke test" do
    in_tmp "new_with_tests", fn ->
      output = mix "new . --sup"
      assert output =~ "* creating lib/new_with_tests.ex"
      assert output =~ "* creating lib/new_with_tests/supervisor.ex"

      output = mix "test test/new_with_tests_test.exs --cover"
      assert File.regular?("ebin/Elixir.NewWithTests.beam")
      assert output =~ "1 tests, 0 failures"
      assert output =~ "Generating cover results ... ok"
      assert File.regular?("cover/Elixir.NewWithTests.html")
    end
  end

  defp mix(args) do
    System.cmd "#{elixir_executable} #{mix_executable} #{args}"
  end

  defp mix_executable do
    Path.expand("../../../../bin/mix", __DIR__)
  end

  defp elixir_executable do
    Path.expand("../../../../bin/elixir", __DIR__)
  end
end

Code.require_file "../test_helper.exs", __DIR__

defmodule Mix.CLITest do
  use MixTest.Case

  test "env" do
    in_fixture "only_mixfile", fn ->
      env = System.cmd %b(MIX_ENV=prod #{elixir_executable} #{mix_executable} run "IO.puts Mix.env")
      assert env =~ "prod"
    end
  end

  test "default task" do
    in_fixture "no_mixfile", fn ->
      output = mix ""
      assert File.regular?("ebin/Elixir.A.beam")
      assert output =~ "Compiled lib/a.ex"
    end
  end

  test "invoke simple task from CLI" do
    in_fixture "only_mixfile", fn ->
      assert mix("hello") == "Hello from MyProject!\n"
    end
  end

  test "--help smoke test" do
    in_fixture "only_mixfile", fn ->
      output = mix "--help"
      assert output =~ %r/mix compile\s+# Compile source files/
      refute output =~ "mix invalid"
    end
  end

  test "--version smoke test" do
    in_fixture "only_mixfile", fn ->
      output = mix "--version"
      assert output =~ %r/Elixir [0-9\.a-z]+/
    end
  end

  test "new with tests smoke test" do
    in_tmp "new_with_tests", fn ->
      output = mix "new ."
      assert output =~ "* creating lib/new_with_tests.ex"

      output = mix "test"
      assert File.regular?("ebin/Elixir.NewWithTests.beam")
      assert output =~ "1 tests, 0 failures"
    end
  end

  test "new --sup with tests smoke test" do
    in_tmp "new_with_tests", fn ->
      output = mix "new . --sup"
      assert output =~ "* creating lib/new_with_tests.ex"
      assert output =~ "* creating lib/new_with_tests/supervisor.ex"

      output = mix "test test/new_with_tests_test.exs --cover"
      assert File.regular?("ebin/Elixir.NewWithTests.beam")
      assert output =~ "1 tests, 0 failures"
      assert output =~ "Generating cover results ... ok"
      assert File.regular?("cover/Elixir.NewWithTests.html")
    end
  end

  defp mix(args) do
    System.cmd "#{elixir_executable} #{mix_executable} #{args}"
  end

  defp mix_executable do
    Path.expand("../../../../bin/mix", __DIR__)
  end

  defp elixir_executable do
    Path.expand("../../../../bin/elixir", __DIR__)
  end
end


Refactor ExTwilio.ResultStream to simplify streaming code

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator
  alias ExTwilio.Config

  @type url :: String.t

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(
      fn -> {url, module, options} end,
      &process_page/1,
      fn _ -> nil end
    )
  end

  @spec fetch_page(url, module, options::list) :: {list, {url, module, options::list}}
  defp fetch_page(url, module, options) do
    results = Api.get!(url, Api.auth_header(options))
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, {meta["next_page_uri"], module, options}}
  end

  @spec process_page({url | nil, module, options :: list}) :: {:halt, nil} |
                                                         {list, {url, module}}
  defp process_page({nil, _module, _options}), do: {:halt, nil}

  defp process_page({next_page_uri, module, options}) do
    next_page_uri
    |> next_page_url
    |> fetch_page(module, options)
  end

  defp next_page_url(uri), do: "https://#{Config.api_domain}" <> uri
end

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator
  alias ExTwilio.Config

  @type url :: String.t

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(
      fn -> fetch_page(url, module, options) end,
      &process_page/1,
      fn _ -> nil end
    )
  end

  @spec fetch_page(url, module, options::list) :: {list, url, module, options::list}
  defp fetch_page(url, module, options) do
    results = Api.get!(url, Api.auth_header(options))
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, meta["next_page_uri"], module, options}
  end

  @spec process_page({list | nil, url | nil, module, options :: list}) :: {:halt, nil} |
                                                         {list, {nil, url, module}}
  defp process_page({nil, nil, _module, _options}), do: {:halt, nil}

  defp process_page({nil, next_page_uri, module, options}) do
    next_page_uri
    |> next_page_url
    |> fetch_page(module, options)
    |> process_page
  end

  defp process_page({items, next_page_uri, module, options}) do
    {items, {nil, next_page_uri, module, options}}
  end

  defp next_page_url(uri), do: "https://#{Config.api_domain}" <> uri
end


Improve Markdown test coverage

Improve Markdown test coverage

Co-authored-by: Vinicius Negrisolo <ba1803924e282df8047f70265f90275689b24fe3@hashrocket.com>
Co-authored-by: Andrew Vogel <19527a6b0ece2f83780b63cf7eb00b11f3af9dbc@hashrocket.com>


Rename next_turn to apply_reducer

Rename next_turn to apply_reducer

defmodule BattleSnake.GameServer do
  use GenServer

  defmodule State do
    defstruct [:world, :reducer, opts: []]
  end

  # Client

  def start_link(%State{} = state, opts \\ []) do
    GenServer.start_link(__MODULE__, {:suspend, state}, opts)
  end

  def resume(pid) do
    GenServer.call(pid, :resume)
  end

  def pause(pid) do
    GenServer.call(pid, :pause)
  end

  def next(pid) do
    GenServer.call(pid, :next)
  end

  # Server (callbacks)

  # Calls
  def handle_call(:pause, _from, {:cont, state}) do
    {:reply, :ok, {:suspend, state}}
  end

  # calling pause on an already paused or stopped game has no effect
  def handle_call(:pause, _from, state) do
    {:reply, :ok, state}
  end

  def handle_call(:resume, _from, {:suspend, state}) do
    tick(state)
    {:reply, :ok, {:cont, state}}
  end

  # calling resume on an already running game or stopped game doesn't do
    # anything
  def handle_call(:resume, _from, state) do
    {:reply, :ok, state}
  end

  def handle_call(:next, _from, {:halted, state}) do
    {:reply, :ok, {:halted, state}}
  end

  def handle_call(:next, _from, {_, state}) do
    state = apply_reducer(state)
    {:reply, :ok, {:suspend, state}}
  end

  def handle_call(request, from, state) do
    # Call the default implementation from GenServer
    super(request, from, state)
  end

  # Casts

  def handle_cast(request, state) do
    super(request, state)
  end

  #
  def handle_info(:tick, {:cont, state}) do
    state = apply_reducer(state)
    if done?(state) do
      {:noreply, {:halted, state}}
    else
      tick(state)
      {:noreply, {:cont, state}}
    end
  end

  def handle_info(:tick, {:suspend, state}) do
    {:noreply, {:suspend, state}}
  end

  def handle_info(:tick, {:halted, state}) do
    {:noreply, {:halted, state}}
  end

  # Private

  defp delay(state) do
    opts = state.opts
    Dict.fetch!(opts, :delay)
  end

  defp tick(state) do
    Process.send_after(self(), :tick, delay(state))
  end

  defp apply_reducer(state) do
    %{state| world: state.reducer.(state.world)}
  end

  # check if the game is over
  defp done?(state) do
    opts = state.opts
    world = state.world
    fun = Dict.fetch!(opts, :objective)
    fun.(world)
  end
end

defmodule BattleSnake.GameServer do
  use GenServer

  defmodule State do
    defstruct [:world, :reducer, opts: []]
  end

  # Client

  def start_link(%State{} = state, opts \\ []) do
    GenServer.start_link(__MODULE__, {:suspend, state}, opts)
  end

  def resume(pid) do
    GenServer.call(pid, :resume)
  end

  def pause(pid) do
    GenServer.call(pid, :pause)
  end

  def next(pid) do
    GenServer.call(pid, :next)
  end

  # Server (callbacks)

  # Calls
  def handle_call(:pause, _from, {:cont, state}) do
    {:reply, :ok, {:suspend, state}}
  end

  # calling pause on an already paused or stopped game has no effect
  def handle_call(:pause, _from, state) do
    {:reply, :ok, state}
  end

  def handle_call(:resume, _from, {:suspend, state}) do
    tick(state)
    {:reply, :ok, {:cont, state}}
  end

  # calling resume on an already running game or stopped game doesn't do
    # anything
  def handle_call(:resume, _from, state) do
    {:reply, :ok, state}
  end

  def handle_call(:next, _from, {:halted, state}) do
    {:reply, :ok, {:halted, state}}
  end

  def handle_call(:next, _from, {_, state}) do
    state = next_turn(state)
    {:reply, :ok, {:suspend, state}}
  end

  def handle_call(request, from, state) do
    # Call the default implementation from GenServer
    super(request, from, state)
  end

  # Casts

  def handle_cast(request, state) do
    super(request, state)
  end

  #
  def handle_info(:tick, {:cont, state}) do
    state = next_turn(state)
    if done?(state) do
      {:noreply, {:halted, state}}
    else
      tick(state)
      {:noreply, {:cont, state}}
    end
  end

  def handle_info(:tick, {:suspend, state}) do
    {:noreply, {:suspend, state}}
  end

  def handle_info(:tick, {:halted, state}) do
    {:noreply, {:halted, state}}
  end

  # Private

  defp delay(state) do
    opts = state.opts
    Dict.fetch!(opts, :delay)
  end

  defp tick(state) do
    Process.send_after(self(), :tick, delay(state))
  end

  defp next_turn(state) do
    reducer = state.reducer
    world = state.world
    world = reducer.(world)
    %{state| world: world}
  end

  # check if the game is over
  defp done?(state) do
    opts = state.opts
    world = state.world
    fun = Dict.fetch!(opts, :objective)
    fun.(world)
  end
end


Only show first run hint if --first-run is given

defmodule Credo.CLI.Output.Summary do
  @moduledoc false

  # This module is responsible for printing the summary at the end of the analysis.

  @category_wording [
    {:consistency, "consistency issue", "consistency issues"},
    {:warning, "warning", "warnings"},
    {:refactor, "refactoring opportunity", "refactoring opportunities"},
    {:readability, "code readability issue", "code readability issues"},
    {:design, "software design suggestion", "software design suggestions"}
  ]
  @cry_for_help "Please report incorrect results: https://github.com/rrrene/credo/issues"

  alias Credo.CLI.Output
  alias Credo.CLI.Output.UI
  alias Credo.Execution
  alias Credo.SourceFile

  def print(
        _source_files,
        %Execution{format: "flycheck"},
        _time_load,
        _time_run
      ) do
    nil
  end

  def print(_source_files, %Execution{format: "oneline"}, _time_load, _time_run) do
    nil
  end

  def print(source_files, exec, time_load, time_run) do
    issues = Execution.get_issues(exec)
    source_file_count = exec |> Execution.get_source_files() |> Enum.count()
    checks_count = count_checks(exec)

    UI.puts()
    UI.puts([:faint, @cry_for_help])
    UI.puts()
    UI.puts([:faint, format_time_spent(checks_count, source_file_count, time_load, time_run)])

    UI.puts(summary_parts(source_files, issues))
    UI.puts()

    print_priority_hint(exec)
    print_first_run_hint(exec)
  end

  defp latest_tag do
    case System.cmd("git", ~w"describe --tags --abbrev=0") do
      {output, 0} -> String.trim(output)
      _ -> nil
    end
  end

  defp default_branch do
    remote_name = default_remote_name()

    case System.cmd("git", ~w"symbolic-ref refs/remotes/#{remote_name}/HEAD") do
      {output, 0} -> Regex.run(~r"refs/remotes/#{remote_name}/(.+)$", output) |> Enum.at(1)
      _ -> nil
    end
  end

  defp default_remote_name do
    "origin"
  end

  defp latest_commit_on_default_branch do
    case System.cmd("git", ~w"rev-parse --short #{default_remote_name()}/#{default_branch()}") do
      {output, 0} -> String.trim(output)
      _ -> nil
    end
  end

  defp now do
    DateTime.utc_now()
    |> Calendar.strftime("%Y-%m-%d")
  end

  defp print_first_run_hint(%Execution{cli_options: %{switches: %{first_run: true}}}) do
    term_width = Output.term_columns()
    now = now()
    default_branch = default_branch()
    latest_commit_on_default_branch = latest_commit_on_default_branch()
    latest_tag = latest_tag()

    headline = " 8< "
    bar = String.pad_leading("", div(term_width - String.length(headline), 2), "-")

    UI.puts()
    UI.puts()
    UI.puts([:magenta, :bright, "#{bar} 8< #{bar}"])
    UI.puts()
    UI.puts()

    UI.puts([
      :reset,
      :orange,
      """
      # Where to start?
      """,
      :reset,
      """

      That's a lot of issues to deal with at once.
      """,
      """

      You can use `diff` to only show the issues that were introduced on this branch:
      """,
      :cyan,
      """

          mix credo diff #{default_branch}

      """,
      :reset,
      :orange,
      """
      ## Compare to a point in history
      """,
      :reset,
      """

      You can use `diff` to only show the issues that were introduced after a certain tag or commit:

      """,
      :cyan,
      "    mix credo diff #{latest_tag} ",
      :faint,
      "             # use the latest tag",
      "\n\n",
      :reset,
      :cyan,
      "    mix credo diff #{latest_commit_on_default_branch}",
      :faint,
      "             # use the current HEAD of #{default_branch()}",
      "\n\n",
      :reset,
      """
      Lastly, you can compare your working dir against this point in time:

      """,
      :cyan,
      "    mix credo diff --since #{now}",
      :faint,
      "  # use the current time",
      "\n\n",
      :reset,
      :orange,
      """
      ## Every project is different
      """,
      :reset,
      """

      This is true, especially when it comes to introducing code analysis to an existing codebase.
      Doing so should not be about following any "best practice" in particular, it should be about
      helping you to get to know the ropes and make the changes you want.
      """
    ])

    UI.puts("Try the options outlined above to see which one is working for this project!")
  end

  defp print_first_run_hint(exec), do: exec

  defp count_checks(exec) do
    {result, _only_matching, _ignore_matching} = Execution.checks(exec)

    Enum.count(result)
  end

  defp print_priority_hint(%Execution{min_priority: min_priority})
       when min_priority >= 0 do
    UI.puts([
      :faint,
      "Showing priority issues: ↑ ↗ →  (use `mix credo explain` to explain issues, `mix credo --help` for options)."
    ])
  end

  defp print_priority_hint(_) do
    UI.puts([
      :faint,
      "Use `mix credo explain` to explain issues, `mix credo --help` for options."
    ])
  end

  defp format_time_spent(check_count, source_file_count, time_load, time_run) do
    time_run = time_run |> div(10_000)
    time_load = time_load |> div(10_000)

    formatted_total = format_in_seconds(time_run + time_load)

    time_to_load = format_in_seconds(time_load)
    time_to_run = format_in_seconds(time_run)

    total_in_seconds =
      case formatted_total do
        "1.0" -> "1 second"
        value -> "#{value} seconds"
      end

    checks =
      if check_count == 1 do
        "1 check"
      else
        "#{check_count} checks"
      end

    source_files =
      if source_file_count == 1 do
        "1 file"
      else
        "#{source_file_count} files"
      end

    breakdown = "#{time_to_load}s to load, #{time_to_run}s running #{checks} on #{source_files}"

    "Analysis took #{total_in_seconds} (#{breakdown})"
  end

  defp format_in_seconds(t) do
    if t < 10 do
      "0.0#{t}"
    else
      t = div(t, 10)
      "#{div(t, 10)}.#{rem(t, 10)}"
    end
  end

  defp category_count(issues, category) do
    issues
    |> Enum.filter(&(&1.category == category))
    |> Enum.count()
  end

  defp summary_parts(source_files, issues) do
    parts =
      @category_wording
      |> Enum.flat_map(&summary_part(&1, issues))

    parts =
      parts
      |> List.update_at(Enum.count(parts) - 1, fn last_part ->
        String.replace(last_part, ", ", "")
      end)

    parts =
      if Enum.empty?(parts) do
        "no issues"
      else
        parts
      end

    [
      :green,
      "#{scope_count(source_files)} mods/funs, ",
      :reset,
      "found ",
      parts,
      "."
    ]
  end

  defp summary_part({category, singular, plural}, issues) do
    color = Output.check_color(category)

    case category_count(issues, category) do
      0 -> []
      1 -> [color, "1 #{singular}, "]
      x -> [color, "#{x} #{plural}, "]
    end
  end

  defp scope_count(%SourceFile{} = source_file) do
    Credo.Code.prewalk(source_file, &scope_count_traverse/2, 0)
  end

  defp scope_count([]), do: 0

  defp scope_count(source_files) when is_list(source_files) do
    source_files
    |> Enum.map(&Task.async(fn -> scope_count(&1) end))
    |> Enum.map(&Task.await/1)
    |> Enum.reduce(&(&1 + &2))
  end

  @def_ops [:defmodule, :def, :defp, :defmacro]
  for op <- @def_ops do
    defp scope_count_traverse({unquote(op), _, _} = ast, count) do
      {ast, count + 1}
    end
  end

  defp scope_count_traverse(ast, count) do
    {ast, count}
  end
end

defmodule Credo.CLI.Output.Summary do
  @moduledoc false

  # This module is responsible for printing the summary at the end of the analysis.

  @category_wording [
    {:consistency, "consistency issue", "consistency issues"},
    {:warning, "warning", "warnings"},
    {:refactor, "refactoring opportunity", "refactoring opportunities"},
    {:readability, "code readability issue", "code readability issues"},
    {:design, "software design suggestion", "software design suggestions"}
  ]
  @cry_for_help "Please report incorrect results: https://github.com/rrrene/credo/issues"

  alias Credo.CLI.Output
  alias Credo.CLI.Output.UI
  alias Credo.Execution
  alias Credo.SourceFile

  def print(
        _source_files,
        %Execution{format: "flycheck"},
        _time_load,
        _time_run
      ) do
    nil
  end

  def print(_source_files, %Execution{format: "oneline"}, _time_load, _time_run) do
    nil
  end

  def print(source_files, exec, time_load, time_run) do
    issues = Execution.get_issues(exec)
    source_file_count = exec |> Execution.get_source_files() |> Enum.count()
    checks_count = count_checks(exec)

    UI.puts()
    UI.puts([:faint, @cry_for_help])
    UI.puts()
    UI.puts([:faint, format_time_spent(checks_count, source_file_count, time_load, time_run)])

    UI.puts(summary_parts(source_files, issues))
    UI.puts()

    print_priority_hint(exec)
    print_first_run_hint(exec)
  end

  defp latest_tag do
    case System.cmd("git", ~w"describe --tags --abbrev=0") do
      {output, 0} -> String.trim(output)
      _ -> nil
    end
  end

  defp default_branch do
    remote_name = default_remote_name()

    case System.cmd("git", ~w"symbolic-ref refs/remotes/#{remote_name}/HEAD") do
      {output, 0} -> Regex.run(~r"refs/remotes/#{remote_name}/(.+)$", output) |> Enum.at(1)
      _ -> nil
    end
  end

  defp default_remote_name do
    "origin"
  end

  defp latest_commit_on_default_branch do
    case System.cmd("git", ~w"rev-parse --short #{default_remote_name()}/#{default_branch()}") do
      {output, 0} -> String.trim(output)
      _ -> nil
    end
  end

  defp now do
    DateTime.utc_now()
    |> Calendar.strftime("%Y-%m-%d")
  end

  defp print_first_run_hint(_exec) do
    term_width = Output.term_columns()
    now = now()
    default_branch = default_branch()
    latest_commit_on_default_branch = latest_commit_on_default_branch()
    latest_tag = latest_tag()

    headline = " 8< "
    bar = String.pad_leading("", div(term_width - String.length(headline), 2), "-")

    UI.puts()
    UI.puts()
    UI.puts([:magenta, :bright, "#{bar} 8< #{bar}"])
    UI.puts()
    UI.puts()

    UI.puts([
      :reset,
      :orange,
      """
      # Where to start?
      """,
      :reset,
      """

      That's a lot of issues to deal with at once.
      """,
      """

      You can use `diff` to only show the issues that were introduced on this branch:
      """,
      :cyan,
      """

          mix credo diff #{default_branch}

      """,
      :reset,
      :orange,
      """
      ## Compare to a point in history
      """,
      :reset,
      """

      You can use `diff` to only show the issues that were introduced after a certain tag or commit:

      """,
      :cyan,
      "    mix credo diff #{latest_tag} ",
      :faint,
      "             # use the latest tag",
      "\n\n",
      :reset,
      :cyan,
      "    mix credo diff #{latest_commit_on_default_branch}",
      :faint,
      "             # use the current HEAD of #{default_branch()}",
      "\n\n",
      :reset,
      """
      Lastly, you can compare your working dir against this point in time:

      """,
      :cyan,
      "    mix credo diff --since #{now}",
      :faint,
      "  # use the current time",
      "\n\n",
      :reset,
      :orange,
      """
      ## Every project is different
      """,
      :reset,
      """

      This is true, especially when it comes to introducing code analysis to an existing codebase.
      Doing so should not be about following any "best practice" in particular, it should be about
      helping you to get to know the ropes and make the changes you want.
      """
    ])

    UI.puts("Try the options outlined above to see which one is working for this project!")
  end

  defp count_checks(exec) do
    {result, _only_matching, _ignore_matching} = Execution.checks(exec)

    Enum.count(result)
  end

  defp print_priority_hint(%Execution{min_priority: min_priority})
       when min_priority >= 0 do
    UI.puts([
      :faint,
      "Showing priority issues: ↑ ↗ →  (use `mix credo explain` to explain issues, `mix credo --help` for options)."
    ])
  end

  defp print_priority_hint(_) do
    UI.puts([
      :faint,
      "Use `mix credo explain` to explain issues, `mix credo --help` for options."
    ])
  end

  defp format_time_spent(check_count, source_file_count, time_load, time_run) do
    time_run = time_run |> div(10_000)
    time_load = time_load |> div(10_000)

    formatted_total = format_in_seconds(time_run + time_load)

    time_to_load = format_in_seconds(time_load)
    time_to_run = format_in_seconds(time_run)

    total_in_seconds =
      case formatted_total do
        "1.0" -> "1 second"
        value -> "#{value} seconds"
      end

    checks =
      if check_count == 1 do
        "1 check"
      else
        "#{check_count} checks"
      end

    source_files =
      if source_file_count == 1 do
        "1 file"
      else
        "#{source_file_count} files"
      end

    breakdown = "#{time_to_load}s to load, #{time_to_run}s running #{checks} on #{source_files}"

    "Analysis took #{total_in_seconds} (#{breakdown})"
  end

  defp format_in_seconds(t) do
    if t < 10 do
      "0.0#{t}"
    else
      t = div(t, 10)
      "#{div(t, 10)}.#{rem(t, 10)}"
    end
  end

  defp category_count(issues, category) do
    issues
    |> Enum.filter(&(&1.category == category))
    |> Enum.count()
  end

  defp summary_parts(source_files, issues) do
    parts =
      @category_wording
      |> Enum.flat_map(&summary_part(&1, issues))

    parts =
      parts
      |> List.update_at(Enum.count(parts) - 1, fn last_part ->
        String.replace(last_part, ", ", "")
      end)

    parts =
      if Enum.empty?(parts) do
        "no issues"
      else
        parts
      end

    [
      :green,
      "#{scope_count(source_files)} mods/funs, ",
      :reset,
      "found ",
      parts,
      "."
    ]
  end

  defp summary_part({category, singular, plural}, issues) do
    color = Output.check_color(category)

    case category_count(issues, category) do
      0 -> []
      1 -> [color, "1 #{singular}, "]
      x -> [color, "#{x} #{plural}, "]
    end
  end

  defp scope_count(%SourceFile{} = source_file) do
    Credo.Code.prewalk(source_file, &scope_count_traverse/2, 0)
  end

  defp scope_count([]), do: 0

  defp scope_count(source_files) when is_list(source_files) do
    source_files
    |> Enum.map(&Task.async(fn -> scope_count(&1) end))
    |> Enum.map(&Task.await/1)
    |> Enum.reduce(&(&1 + &2))
  end

  @def_ops [:defmodule, :def, :defp, :defmacro]
  for op <- @def_ops do
    defp scope_count_traverse({unquote(op), _, _} = ast, count) do
      {ast, count + 1}
    end
  end

  defp scope_count_traverse(ast, count) do
    {ast, count}
  end
end


Fix typo in Config.Provider link (#11618)

defmodule Config.Reader do
  @moduledoc """
  API for reading config files defined with `Config`.

  ## As a provider

  `Config.Reader` can also be used as a `Config.Provider`. A config
  provider is used during releases to customize how applications are
  configured. When used as a provider, it expects a single argument:
  the configuration path (as outlined in `t:Config.Provider.config_path/0`)
  for the file to be read and loaded during the system boot.

  For example, if you expect the target system to have a config file
  in an absolute path, you can add this inside the `def project` portion
  of  your `mix.exs`:

      releases: [
        demo: [
          config_providers: [
            {Config.Reader, "/etc/config.exs"}
          ]
        ]
      ]

  Or if you want to read a custom path inside the release:

      config_providers: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]

  You can also pass a keyword list of options to the reader,
  where the `:path` is a required key:

      config_providers: [
        {Config.Reader,
         path: "/etc/config.exs",
         env: :prod,
         imports: :disabled}
      ]

  Remember Mix already loads `config/runtime.exs` by default.
  For more examples and scenarios, see the `Config.Provider` module.
  """

  @behaviour Config.Provider

  @impl true
  def init(opts) when is_list(opts) do
    {path, opts} = Keyword.pop!(opts, :path)
    Config.Provider.validate_config_path!(path)
    {path, opts}
  end

  def init(path) do
    init(path: path)
  end

  @impl true
  def load(config, {path, opts}) do
    merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
  end

  @doc """
  Evaluates the configuration `contents` for the given `file`.

  Accepts the same options as `read!/2`.
  """
  @doc since: "1.11.0"
  @spec eval!(Path.t(), binary, keyword) :: keyword
  def eval!(file, contents, opts \\ [])
      when is_binary(file) and is_binary(contents) and is_list(opts) do
    Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
  end

  @doc """
  Reads the configuration file.

  ## Options

    * `:imports` - a list of already imported paths or `:disabled`
      to disable imports

    * `:env` - the environment the configuration file runs on.
      See `Config.config_env/0` for sample usage

    * `:target` - the target the configuration file runs on.
      See `Config.config_target/0` for sample usage

  """
  @doc since: "1.9.0"
  @spec read!(Path.t(), keyword) :: keyword
  def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
    file = Path.expand(file)
    Config.__eval__!(file, File.read!(file), opts) |> elem(0)
  end

  @doc """
  Reads the given configuration file and returns the configuration
  with its imports.

  Accepts the same options as `read!/2`. Although note the `:imports`
  option cannot be disabled in `read_imports!/2`.
  """
  @doc since: "1.9.0"
  @spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
  def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
    if opts[:imports] == :disabled do
      raise ArgumentError, ":imports must be a list of paths"
    end

    file = Path.expand(file)
    Config.__eval__!(file, File.read!(file), opts)
  end

  @doc """
  Merges two configurations.

  The configurations are merged together with the values in
  the second one having higher preference than the first in
  case of conflicts. In case both values are set to keyword
  lists, it deep merges them.

  ## Examples

      iex> Config.Reader.merge([app: [k: :v1]], [app: [k: :v2]])
      [app: [k: :v2]]

      iex> Config.Reader.merge([app: [k: [v1: 1, v2: 2]]], [app: [k: [v2: :a, v3: :b]]])
      [app: [k: [v1: 1, v2: :a, v3: :b]]]

      iex> Config.Reader.merge([app1: []], [app2: []])
      [app1: [], app2: []]

  """
  @doc since: "1.9.0"
  @spec merge(keyword, keyword) :: keyword
  def merge(config1, config2) when is_list(config1) and is_list(config2) do
    Config.__merge__(config1, config2)
  end
end

defmodule Config.Reader do
  @moduledoc """
  API for reading config files defined with `Config`.

  ## As a provider

  `Config.Reader` can also be used as a `Config.Provider`. A config
  provider is used during releases to customize how applications are
  configured. When used as a provider, it expects a single argument:
  the configuration path (as outlined in `t:Config.Provider.config_path/0`)
  for the file to be read and loaded during the system boot.

  For example, if you expect the target system to have a config file
  in an absolute path, you can add this inside the `def project` portion
  of  your `mix.exs`:

      releases: [
        demo: [
          config_providers: [
            {Config.Reader, "/etc/config.exs"}
          ]
        ]
      ]

  Or if you want to read a custom path inside the release:

      config_providers: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]

  You can also pass a keyword list of options to the reader,
  where the `:path` is a required key:

      config_providers: [
        {Config.Reader,
         path: "/etc/config.exs",
         env: :prod,
         imports: :disabled}
      ]

  Remember Mix already loads `config/runtime.exs` by default.
  For more examples and scenarios, see the `Config.Providers` module.
  """

  @behaviour Config.Provider

  @impl true
  def init(opts) when is_list(opts) do
    {path, opts} = Keyword.pop!(opts, :path)
    Config.Provider.validate_config_path!(path)
    {path, opts}
  end

  def init(path) do
    init(path: path)
  end

  @impl true
  def load(config, {path, opts}) do
    merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
  end

  @doc """
  Evaluates the configuration `contents` for the given `file`.

  Accepts the same options as `read!/2`.
  """
  @doc since: "1.11.0"
  @spec eval!(Path.t(), binary, keyword) :: keyword
  def eval!(file, contents, opts \\ [])
      when is_binary(file) and is_binary(contents) and is_list(opts) do
    Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
  end

  @doc """
  Reads the configuration file.

  ## Options

    * `:imports` - a list of already imported paths or `:disabled`
      to disable imports

    * `:env` - the environment the configuration file runs on.
      See `Config.config_env/0` for sample usage

    * `:target` - the target the configuration file runs on.
      See `Config.config_target/0` for sample usage

  """
  @doc since: "1.9.0"
  @spec read!(Path.t(), keyword) :: keyword
  def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
    file = Path.expand(file)
    Config.__eval__!(file, File.read!(file), opts) |> elem(0)
  end

  @doc """
  Reads the given configuration file and returns the configuration
  with its imports.

  Accepts the same options as `read!/2`. Although note the `:imports`
  option cannot be disabled in `read_imports!/2`.
  """
  @doc since: "1.9.0"
  @spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
  def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
    if opts[:imports] == :disabled do
      raise ArgumentError, ":imports must be a list of paths"
    end

    file = Path.expand(file)
    Config.__eval__!(file, File.read!(file), opts)
  end

  @doc """
  Merges two configurations.

  The configurations are merged together with the values in
  the second one having higher preference than the first in
  case of conflicts. In case both values are set to keyword
  lists, it deep merges them.

  ## Examples

      iex> Config.Reader.merge([app: [k: :v1]], [app: [k: :v2]])
      [app: [k: :v2]]

      iex> Config.Reader.merge([app: [k: [v1: 1, v2: 2]]], [app: [k: [v2: :a, v3: :b]]])
      [app: [k: [v1: 1, v2: :a, v3: :b]]]

      iex> Config.Reader.merge([app1: []], [app2: []])
      [app1: [], app2: []]

  """
  @doc since: "1.9.0"
  @spec merge(keyword, keyword) :: keyword
  def merge(config1, config2) when is_list(config1) and is_list(config2) do
    Config.__merge__(config1, config2)
  end
end