defmodule CubDB.Btree.KeyRangeTest do
  use ExUnit.Case

  alias CubDB.Btree
  alias CubDB.Btree.KeyRange
  alias CubDB.Store

  import TestHelper

  doctest Btree.KeyRange

  test "KeyRange implements Enumerable" do
    Protocol.assert_impl!(Enumerable, KeyRange)
  end

  test "Enumerable.KeyRange.reduce/3 iterates entries with keys between min_key and max_key" do
    entries = [
      foo: 1,
      bar: 2,
      baz: 3,
      qux: 4,
      quux: 5,
      xxx: 6,
      yyy: 7,
      quuux: 8
    ]

    {:ok, store} = Store.TestStore.create()
    btree = make_btree(store, entries, 3)

    for {min, max} <- [
          {nil, nil},
          {:bar, :qux},
          {:ba, :zz},
          {nil, :qux},
          {:yy, :zz},
          {:baz, nil},
          {:c, :a}
        ],
        min_incl <- [true, false],
        max_incl <- [true, false] do
      min_key = if min == nil, do: min, else: {min, min_incl}
      max_key = if max == nil, do: max, else: {max, max_incl}
      key_range = KeyRange.new(btree, min_key, max_key)

      expected_entries =
        entries
        |> Enum.filter(fn {key, _} ->
          (min_key == nil || (min_incl == true && key >= min) || key > min) &&
            (max_key == nil || (max_incl == true && key <= max) || key < max)
        end)
        |> List.keysort(0)

      assert Enum.to_list(key_range) == expected_entries
      assert Enum.count(key_range) == length(expected_entries)
      assert Stream.map(key_range, & &1) |> Enum.to_list() == expected_entries

      assert Stream.zip(key_range, entries) |> Enum.to_list() ==
               Enum.zip(expected_entries, entries)
    end
  end

  test "Enumerable.KeyRange.reduce/3 iterates entries in reverse order if reverse is true" do
    entries = [
      a: 1,
      b: 2,
      c: 3,
      d: 4,
      e: 5,
      f: 6,
      g: 7,
      h: 8
    ]

    {:ok, store} = Store.TestStore.create()
    btree = make_btree(store, entries, 3)

    key_range = KeyRange.new(btree, {:b, true}, {:g, true}, true)

    assert Enum.to_list(key_range) == [g: 7, f: 6, e: 5, d: 4, c: 3, b: 2]
  end

  test "Enum.member/2 returns false if key is outside of range, or not in the btree, and true otherwise" do
    entries = [a: 1, b: 2, c: 3, d: 4]
    {:ok, store} = Store.TestStore.create()
    btree = make_btree(store, entries, 3)

    assert Enum.member?(KeyRange.new(btree, {:b, true}, {:c, true}), {:a, 1}) == false
    assert Enum.member?(KeyRange.new(btree, {:b, true}, {:z, true}), {:e, 1}) == false
    assert Enum.member?(KeyRange.new(btree, {:b, true}, {:z, true}), {:a, 0}) == false
    assert Enum.member?(KeyRange.new(btree, {:a, false}, {:z, true}), {:a, 1}) == false
    assert Enum.member?(KeyRange.new(btree, {:a, true}, {:c, false}), {:c, 3}) == false
    assert Enum.member?(KeyRange.new(btree, nil, nil), 123) == false

    assert Enum.member?(KeyRange.new(btree, {:b, true}, {:z, true}), {:b, 2}) == true
    assert Enum.member?(KeyRange.new(btree, {:b, true}, {:c, true}), {:c, 3}) == true
    assert Enum.member?(KeyRange.new(btree, {:b, false}, {:z, false}), {:c, 3}) == true
    assert Enum.member?(KeyRange.new(btree, {:b, false}, nil), {:d, 4}) == true
    assert Enum.member?(KeyRange.new(btree, nil, {:c, false}), {:b, 2}) == true
  end

  test "Enumerable.Btree.KeyRange.reduce/3 skips nodes marked as deleted" do
    {:ok, store} = Store.TestStore.create()
    tree = make_btree(store, [a: 1, b: 2, c: 3, d: 4], 3) |> Btree.mark_deleted(:b)
    key_range = KeyRange.new(tree, {:a, true}, {:d, false})
    assert Enum.to_list(key_range) == [a: 1, c: 3]
  end
end
