defmodule Blogex.Test.FakeBlog do
  @moduledoc """
  A test double that implements the same interface as a `use Blogex.Blog`
  module, but backed by an Agent so tests can control the post data.

  ## Usage in tests

      setup do
        posts = [PostBuilder.build(id: "hello"), PostBuilder.build(id: "world")]
        Blogex.Test.FakeBlog.start(posts, blog_id: :engineering, title: "Eng Blog")
        :ok
      end

  Then pass `Blogex.Test.FakeBlog` anywhere a blog module is expected.
  """

  use Agent

  @defaults [
    blog_id: :test_blog,
    title: "Test Blog",
    description: "A blog for tests",
    base_path: "/blog/test"
  ]

  def start(posts \\ [], opts \\ []) do
    opts = Keyword.merge(@defaults, opts)

    state = %{
      posts: posts,
      blog_id: opts[:blog_id],
      title: opts[:title],
      description: opts[:description],
      base_path: opts[:base_path]
    }

    case Agent.start(fn -> state end, name: __MODULE__) do
      {:ok, pid} ->
        {:ok, pid}

      {:error, {:already_started, pid}} ->
        Agent.update(__MODULE__, fn _ -> state end)
        {:ok, pid}
    end
  end

  def stop, do: Agent.stop(__MODULE__)

  defp get(key), do: Agent.get(__MODULE__, &Map.fetch!(&1, key))

  def blog_id, do: get(:blog_id)
  def title, do: get(:title)
  def description, do: get(:description)
  def base_path, do: get(:base_path)

  def unfiltered_posts do
    get(:posts)
    |> Enum.sort_by(& &1.date, {:desc, Date})
  end

  def all_posts do
    today = Date.utc_today()

    get(:posts)
    |> Enum.filter(&(&1.published and not Date.after?(&1.date, today)))
    |> Enum.sort_by(& &1.date, {:desc, Date})
  end

  def recent_posts(n \\ 5), do: Enum.take(all_posts(), n)

  def all_tags do
    all_posts()
    |> Enum.flat_map(& &1.tags)
    |> Enum.uniq()
    |> Enum.sort()
  end

  def posts_by_tag(tag) do
    Enum.filter(all_posts(), fn post -> tag in post.tags end)
  end

  def get_post!(id) do
    Enum.find(unfiltered_posts(), &(&1.id == id)) ||
      raise Blogex.NotFoundError, "post #{inspect(id)} not found"
  end

  def get_post(id) do
    Enum.find(unfiltered_posts(), &(&1.id == id))
  end

  def paginate(page \\ 1, per_page \\ 10) do
    posts = all_posts()
    total = length(posts)
    total_pages = max(ceil(total / per_page), 1)

    entries =
      posts
      |> Enum.drop((page - 1) * per_page)
      |> Enum.take(per_page)

    %{
      entries: entries,
      page: page,
      per_page: per_page,
      total_entries: total,
      total_pages: total_pages
    }
  end
end
