defmodule Blogex.BlogTest do
  use ExUnit.Case

  import Blogex.Test.PostBuilder
  alias Blogex.Test.FakeBlog

  setup do
    Blogex.Test.Setup.with_blog()
  end

  describe "all_posts/0" do
    test "excludes drafts", %{blog: blog} do
      ids = blog.all_posts() |> Enum.map(& &1.id)

      assert "draft-post" not in ids
    end

    test "excludes future-dated posts", %{blog: blog} do
      ids = blog.all_posts() |> Enum.map(& &1.id)

      refute "future-post" in ids
    end

    test "includes today-dated published posts" do
      {:ok, _} =
        FakeBlog.start([
          build(id: "today", date: Date.utc_today(), published: true),
          build(id: "tomorrow", date: Date.add(Date.utc_today(), 1), published: true)
        ])

      ids = FakeBlog.all_posts() |> Enum.map(& &1.id)

      assert "today" in ids
      refute "tomorrow" in ids
    end

    test "returns posts newest first", %{blog: blog} do
      dates = blog.all_posts() |> Enum.map(& &1.date)

      assert dates == Enum.sort(dates, {:desc, Date})
    end
  end

  describe "recent_posts/1" do
    test "excludes future-dated posts", %{blog: blog} do
      posts = blog.recent_posts(100)
      ids = Enum.map(posts, & &1.id)

      refute "future-post" in ids
    end

    test "returns at most n posts", %{blog: blog} do
      assert length(blog.recent_posts(2)) == 2
    end

    test "returns newest posts", %{blog: blog} do
      [first | _] = blog.recent_posts(1)

      assert first.id == "newest-post"
    end
  end

  describe "posts_by_tag/1" do
    test "excludes future-dated posts", %{blog: blog} do
      posts = blog.posts_by_tag("future-only")

      assert posts == []
    end

    test "returns only posts with the given tag", %{blog: blog} do
      posts = blog.posts_by_tag("testing")

      assert [%{id: "middle-post"}] = posts
    end

    test "returns empty list for unknown tag", %{blog: blog} do
      assert blog.posts_by_tag("nonexistent") == []
    end

    test "excludes drafts even if tag matches" do
      {:ok, _} =
        FakeBlog.start([
          build(id: "pub", tags: ["elixir"], published: true),
          build(id: "draft", tags: ["elixir"], published: false)
        ])

      ids = FakeBlog.posts_by_tag("elixir") |> Enum.map(& &1.id)

      assert "pub" in ids
      refute "draft" in ids
    end
  end

  describe "all_tags/0" do
    test "excludes tags only on future-dated posts", %{blog: blog} do
      refute "future-only" in blog.all_tags()
    end

    test "returns unique sorted tags from published posts", %{blog: blog} do
      tags = blog.all_tags()

      assert tags == Enum.sort(tags)
      assert "elixir" in tags
      assert "devops" in tags
    end

    test "excludes tags only appearing on drafts" do
      {:ok, _} =
        FakeBlog.start([
          build(tags: ["visible"], published: true),
          build(id: "d", tags: ["hidden"], published: false)
        ])

      refute "hidden" in FakeBlog.all_tags()
    end
  end

  describe "get_post!/1" do
    test "returns post by id", %{blog: blog} do
      post = blog.get_post!("oldest-post")

      assert post.id == "oldest-post"
    end

    test "raises for unknown id", %{blog: blog} do
      assert_raise Blogex.NotFoundError, fn ->
        blog.get_post!("nope")
      end
    end

    test "returns a future-dated published post by slug", %{blog: blog} do
      post = blog.get_post!("future-post")
      assert post.id == "future-post"
    end

    test "returns a draft post by slug", %{blog: blog} do
      post = blog.get_post!("draft-post")
      assert post.id == "draft-post"
    end
  end

  describe "get_post/1" do
    test "returns nil for unknown id", %{blog: blog} do
      assert blog.get_post("nope") == nil
    end

    test "returns a future-dated post", %{blog: blog} do
      assert %{id: "future-post"} = blog.get_post("future-post")
    end

    test "returns a draft post", %{blog: blog} do
      assert %{id: "draft-post"} = blog.get_post("draft-post")
    end
  end

  describe "paginate/2" do
    setup do
      posts = build_many(25)
      {:ok, _} = FakeBlog.start(posts)
      %{blog: FakeBlog}
    end

    test "returns the correct page size", %{blog: blog} do
      result = blog.paginate(1, 10)

      assert length(result.entries) == 10
    end

    test "calculates total pages", %{blog: blog} do
      result = blog.paginate(1, 10)

      assert result.total_pages == 3
      assert result.total_entries == 25
    end

    test "returns fewer entries on last page", %{blog: blog} do
      result = blog.paginate(3, 10)

      assert length(result.entries) == 5
    end

    test "page 2 does not overlap with page 1", %{blog: blog} do
      page1_ids = blog.paginate(1, 10).entries |> MapSet.new(& &1.id)
      page2_ids = blog.paginate(2, 10).entries |> MapSet.new(& &1.id)

      assert MapSet.disjoint?(page1_ids, page2_ids)
    end

    test "returns empty entries beyond last page", %{blog: blog} do
      result = blog.paginate(99, 10)

      assert result.entries == []
    end
  end
end
