defmodule RemoteIp.BlockTest do
  use ExUnit.Case, async: true
  import Bitwise

  alias RemoteIp.Block

  def octets(n) do
    Stream.repeatedly(fn -> Enum.random(0..255) end) |> Enum.take(n)
  end

  def ipv4(octets) do
    octets |> Enum.join(".")
  end

  def hextets(n) do
    Stream.repeatedly(fn -> Enum.random(0..65_535) end) |> Enum.take(n)
  end

  def ipv6(hextets) do
    hextets |> Enum.map(&Integer.to_string(&1, 16)) |> Enum.join(":")
  end

  test "parse vs parse!" do
    {:ok, success} = Block.parse("127.0.0.1")
    assert Block.parse!("127.0.0.1") == success

    {:error, error} = Block.parse("127001")
    assert_raise ArgumentError, error, fn -> Block.parse!("127001") end
  end

  test "parsing invalid CIDR" do
    assert_raise ArgumentError, ~S'Invalid address "invalid" in CIDR "invalid"', fn ->
      Block.parse!("invalid")
    end
  end

  test "IPv4 block to string" do
    assert "3.14.15.92/32" == Block.parse!("3.14.15.92/32") |> to_string()
    assert "3.14.15.0/24" == Block.parse!("3.14.15.92/24") |> to_string()
    assert "3.14.0.0/16" == Block.parse!("3.14.15.92/16") |> to_string()
    assert "3.0.0.0/8" == Block.parse!("3.14.15.92/8") |> to_string()
    assert "0.0.0.0/0" == Block.parse!("3.14.15.92/0") |> to_string()
  end

  test "IPv6 block to string" do
    assert "123::456/128" == Block.parse!("123::456/128") |> to_string()
    assert "123::/64" == Block.parse!("123::456/64") |> to_string()
    assert "::/0" == Block.parse!("123::456/0") |> to_string()
  end

  describe "parsing IPv4" do
    test "invalid address and prefix" do
      assert_raise ArgumentError, ~S'Invalid address "3.14" in CIDR "3.14/159"', fn ->
        Block.parse!("3.14/159")
      end
    end

    test "invalid address" do
      assert_raise ArgumentError, ~S'Invalid address "3.141.592.6" in CIDR "3.141.592.6/5"', fn ->
        Block.parse!("3.141.592.6/5")
      end
    end

    test "invalid prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix "invalid" in CIDR "0.0.0.0/invalid"', fn ->
        Block.parse!("0.0.0.0/invalid")
      end
    end

    test "negative prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "0.0.0.0/-1"', fn ->
        Block.parse!("0.0.0.0/-1")
      end
    end

    test "oversized prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix 33 in CIDR "0.0.0.0/33"', fn ->
        Block.parse!("0.0.0.0/33")
      end
    end

    test "address sans prefix" do
      ip = ipv4(octets(4))
      assert Block.parse!(ip) == Block.parse!("#{ip}/32")
    end

    test "address with zero prefix" do
      ip = ipv4(octets(4))
      assert %Block{proto: :v4, net: 0} = Block.parse!("#{ip}/0")
    end

    test "addresses with valid prefixes" do
      [a, b, c, d] = octets(4)
      ip = ipv4([a, b, c, d])

      {:v4, net_a} = Block.encode({a, 0, 0, 0})
      {:v4, net_b} = Block.encode({a, b, 0, 0})
      {:v4, net_c} = Block.encode({a, b, c, 0})
      {:v4, net_x} = Block.encode({a, b &&& 0b11110000, 0, 0})

      assert %Block{proto: :v4, net: ^net_a} = Block.parse!("#{ip}/8")
      assert %Block{proto: :v4, net: ^net_b} = Block.parse!("#{ip}/16")
      assert %Block{proto: :v4, net: ^net_c} = Block.parse!("#{ip}/24")
      assert %Block{proto: :v4, net: ^net_x} = Block.parse!("#{ip}/12")
    end

    test "masks" do
      assert %Block{mask: 0b00000000000000000000000000000000} = Block.parse!("0.0.0.0/0")
      assert %Block{mask: 0b10000000000000000000000000000000} = Block.parse!("0.0.0.0/1")
      assert %Block{mask: 0b11000000000000000000000000000000} = Block.parse!("0.0.0.0/2")
      assert %Block{mask: 0b11100000000000000000000000000000} = Block.parse!("0.0.0.0/3")
      assert %Block{mask: 0b11110000000000000000000000000000} = Block.parse!("0.0.0.0/4")
      assert %Block{mask: 0b11111000000000000000000000000000} = Block.parse!("0.0.0.0/5")
      assert %Block{mask: 0b11111100000000000000000000000000} = Block.parse!("0.0.0.0/6")
      assert %Block{mask: 0b11111110000000000000000000000000} = Block.parse!("0.0.0.0/7")
      assert %Block{mask: 0b11111111000000000000000000000000} = Block.parse!("0.0.0.0/8")
      assert %Block{mask: 0b11111111100000000000000000000000} = Block.parse!("0.0.0.0/9")
      assert %Block{mask: 0b11111111110000000000000000000000} = Block.parse!("0.0.0.0/10")
      assert %Block{mask: 0b11111111111000000000000000000000} = Block.parse!("0.0.0.0/11")
      assert %Block{mask: 0b11111111111100000000000000000000} = Block.parse!("0.0.0.0/12")
      assert %Block{mask: 0b11111111111110000000000000000000} = Block.parse!("0.0.0.0/13")
      assert %Block{mask: 0b11111111111111000000000000000000} = Block.parse!("0.0.0.0/14")
      assert %Block{mask: 0b11111111111111100000000000000000} = Block.parse!("0.0.0.0/15")
      assert %Block{mask: 0b11111111111111110000000000000000} = Block.parse!("0.0.0.0/16")
      assert %Block{mask: 0b11111111111111111000000000000000} = Block.parse!("0.0.0.0/17")
      assert %Block{mask: 0b11111111111111111100000000000000} = Block.parse!("0.0.0.0/18")
      assert %Block{mask: 0b11111111111111111110000000000000} = Block.parse!("0.0.0.0/19")
      assert %Block{mask: 0b11111111111111111111000000000000} = Block.parse!("0.0.0.0/20")
      assert %Block{mask: 0b11111111111111111111100000000000} = Block.parse!("0.0.0.0/21")
      assert %Block{mask: 0b11111111111111111111110000000000} = Block.parse!("0.0.0.0/22")
      assert %Block{mask: 0b11111111111111111111111000000000} = Block.parse!("0.0.0.0/23")
      assert %Block{mask: 0b11111111111111111111111100000000} = Block.parse!("0.0.0.0/24")
      assert %Block{mask: 0b11111111111111111111111110000000} = Block.parse!("0.0.0.0/25")
      assert %Block{mask: 0b11111111111111111111111111000000} = Block.parse!("0.0.0.0/26")
      assert %Block{mask: 0b11111111111111111111111111100000} = Block.parse!("0.0.0.0/27")
      assert %Block{mask: 0b11111111111111111111111111110000} = Block.parse!("0.0.0.0/28")
      assert %Block{mask: 0b11111111111111111111111111111000} = Block.parse!("0.0.0.0/29")
      assert %Block{mask: 0b11111111111111111111111111111100} = Block.parse!("0.0.0.0/30")
      assert %Block{mask: 0b11111111111111111111111111111110} = Block.parse!("0.0.0.0/31")
      assert %Block{mask: 0b11111111111111111111111111111111} = Block.parse!("0.0.0.0/32")
    end

    test "reserved ranges" do
      assert Block.parse!("127.0.0.0/8") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<127, 0, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 0, 0, 0>>)
             }

      assert Block.parse!("10.0.0.0/8") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<10, 0, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 0, 0, 0>>)
             }

      assert Block.parse!("172.16.0.0/12") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<172, 16, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 240, 0, 0>>)
             }

      assert Block.parse!("192.168.0.0/16") == %Block{
               proto: :v4,
               net: :binary.decode_unsigned(<<192, 168, 0, 0>>),
               mask: :binary.decode_unsigned(<<255, 255, 0, 0>>)
             }
    end
  end

  describe "parsing IPv6" do
    test "invalid address and prefix" do
      assert_raise ArgumentError, ~S'Invalid address "a:b:c" in CIDR "a:b:c/1/2/3"', fn ->
        Block.parse!("a:b:c/1/2/3")
      end
    end

    test "invalid address" do
      assert_raise ArgumentError, ~S'Invalid address "f7::u" in CIDR "f7::u/12"', fn ->
        Block.parse!("f7::u/12")
      end
    end

    test "invalid prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix "1/2/3" in CIDR "::a:b:c/1/2/3"', fn ->
        Block.parse!("::a:b:c/1/2/3")
      end
    end

    test "negative prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix -1 in CIDR "::/-1"', fn ->
        Block.parse!("::/-1")
      end
    end

    test "oversized prefix" do
      assert_raise ArgumentError, ~S'Invalid prefix 129 in CIDR "::/129"', fn ->
        Block.parse!("::/129")
      end
    end

    test "address sans prefix" do
      ip = ipv6(hextets(8))
      assert Block.parse!(ip) == Block.parse!("#{ip}/128")
    end

    test "address with zero prefix" do
      ip = ipv6(hextets(8))
      assert %Block{proto: :v6, net: 0} = Block.parse!("#{ip}/0")
    end

    test "addresses with valid prefixes" do
      [a, b, c, d, e, f, g, h] = hextets(8)
      ip = ipv6([a, b, c, d, e, f, g, h])

      {:v6, net_a} = Block.encode({a, 0, 0, 0, 0, 0, 0, 0})
      {:v6, net_b} = Block.encode({a, b, 0, 0, 0, 0, 0, 0})
      {:v6, net_c} = Block.encode({a, b, c, 0, 0, 0, 0, 0})
      {:v6, net_d} = Block.encode({a, b, c, d, 0, 0, 0, 0})
      {:v6, net_e} = Block.encode({a, b, c, d, e, 0, 0, 0})
      {:v6, net_f} = Block.encode({a, b, c, d, e, f, 0, 0})
      {:v6, net_g} = Block.encode({a, b, c, d, e, f, g, 0})
      {:v6, net_x} = Block.encode({a, b, c, d, e &&& 0b1000000000000000, 0, 0, 0})

      assert %Block{proto: :v6, net: ^net_a} = Block.parse!("#{ip}/16")
      assert %Block{proto: :v6, net: ^net_b} = Block.parse!("#{ip}/32")
      assert %Block{proto: :v6, net: ^net_c} = Block.parse!("#{ip}/48")
      assert %Block{proto: :v6, net: ^net_d} = Block.parse!("#{ip}/64")
      assert %Block{proto: :v6, net: ^net_e} = Block.parse!("#{ip}/80")
      assert %Block{proto: :v6, net: ^net_f} = Block.parse!("#{ip}/96")
      assert %Block{proto: :v6, net: ^net_g} = Block.parse!("#{ip}/112")
      assert %Block{proto: :v6, net: ^net_x} = Block.parse!("#{ip}/65")
    end

    test "masks" do
      assert %Block{mask: 0b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/0")
      assert %Block{mask: 0b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/1")
      assert %Block{mask: 0b11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/2")
      assert %Block{mask: 0b11100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/3")
      assert %Block{mask: 0b11110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/4")
      assert %Block{mask: 0b11111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/5")
      assert %Block{mask: 0b11111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/6")
      assert %Block{mask: 0b11111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/7")
      assert %Block{mask: 0b11111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/8")
      assert %Block{mask: 0b11111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/9")
      assert %Block{mask: 0b11111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/10")
      assert %Block{mask: 0b11111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/11")
      assert %Block{mask: 0b11111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/12")
      assert %Block{mask: 0b11111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/13")
      assert %Block{mask: 0b11111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/14")
      assert %Block{mask: 0b11111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/15")
      assert %Block{mask: 0b11111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/16")
      assert %Block{mask: 0b11111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/17")
      assert %Block{mask: 0b11111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/18")
      assert %Block{mask: 0b11111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/19")
      assert %Block{mask: 0b11111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/20")
      assert %Block{mask: 0b11111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/21")
      assert %Block{mask: 0b11111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/22")
      assert %Block{mask: 0b11111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/23")
      assert %Block{mask: 0b11111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/24")
      assert %Block{mask: 0b11111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/25")
      assert %Block{mask: 0b11111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/26")
      assert %Block{mask: 0b11111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/27")
      assert %Block{mask: 0b11111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/28")
      assert %Block{mask: 0b11111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/29")
      assert %Block{mask: 0b11111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/30")
      assert %Block{mask: 0b11111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/31")
      assert %Block{mask: 0b11111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/32")
      assert %Block{mask: 0b11111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/33")
      assert %Block{mask: 0b11111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/34")
      assert %Block{mask: 0b11111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/35")
      assert %Block{mask: 0b11111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/36")
      assert %Block{mask: 0b11111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/37")
      assert %Block{mask: 0b11111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/38")
      assert %Block{mask: 0b11111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/39")
      assert %Block{mask: 0b11111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/40")
      assert %Block{mask: 0b11111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/41")
      assert %Block{mask: 0b11111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/42")
      assert %Block{mask: 0b11111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/43")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/44")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/45")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/46")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/47")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/48")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/49")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/50")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/51")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/52")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/53")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/54")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/55")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/56")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/57")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/58")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/59")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/60")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/61")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/62")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/63")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/64")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/65")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/66")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/67")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/68")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/69")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/70")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000} = Block.parse!("::/71")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000} = Block.parse!("::/72")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000} = Block.parse!("::/73")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000} = Block.parse!("::/74")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000} = Block.parse!("::/75")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000} = Block.parse!("::/76")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000} = Block.parse!("::/77")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000} = Block.parse!("::/78")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000} = Block.parse!("::/79")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000} = Block.parse!("::/80")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000} = Block.parse!("::/81")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000} = Block.parse!("::/82")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000} = Block.parse!("::/83")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000} = Block.parse!("::/84")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000} = Block.parse!("::/85")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000} = Block.parse!("::/86")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000} = Block.parse!("::/87")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000} = Block.parse!("::/88")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000} = Block.parse!("::/89")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000} = Block.parse!("::/90")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000} = Block.parse!("::/91")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000} = Block.parse!("::/92")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000} = Block.parse!("::/93")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000} = Block.parse!("::/94")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000} = Block.parse!("::/95")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000} = Block.parse!("::/96")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000} = Block.parse!("::/97")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000} = Block.parse!("::/98")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000} = Block.parse!("::/99")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000} = Block.parse!("::/100")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000} = Block.parse!("::/101")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000} = Block.parse!("::/102")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000} = Block.parse!("::/103")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000} = Block.parse!("::/104")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000} = Block.parse!("::/105")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000} = Block.parse!("::/106")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000} = Block.parse!("::/107")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000} = Block.parse!("::/108")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000} = Block.parse!("::/109")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000} = Block.parse!("::/110")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000} = Block.parse!("::/111")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000} = Block.parse!("::/112")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000} = Block.parse!("::/113")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000} = Block.parse!("::/114")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000} = Block.parse!("::/115")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000} = Block.parse!("::/116")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000} = Block.parse!("::/117")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000} = Block.parse!("::/118")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000} = Block.parse!("::/119")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000} = Block.parse!("::/120")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000} = Block.parse!("::/121")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000} = Block.parse!("::/122")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000} = Block.parse!("::/123")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000} = Block.parse!("::/124")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000} = Block.parse!("::/125")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100} = Block.parse!("::/126")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110} = Block.parse!("::/127")
      assert %Block{mask: 0b11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111} = Block.parse!("::/128")
    end

    test "reserved ranges" do
      assert Block.parse!("::1/128") == %Block{
               proto: :v6,
               net: 0x00000000000000000000000000000001,
               mask: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
             }

      assert Block.parse!("fc00::/7") == %Block{
               proto: :v6,
               net: 0xFC000000000000000000000000000000,
               mask: 0xFE000000000000000000000000000000
             }
    end
  end

  describe "IPv4 membership" do
    test "inside block" do
      block = Block.parse!("192.168.0.0/16")

      for a <- 0..255, b <- 0..255 do
        assert Block.contains?(block, Block.encode({192, 168, a, b}))
      end
    end

    test "outside block" do
      block = Block.parse!("192.168.0.0/16")
      refute Block.contains?(block, Block.encode({192, 167, 255, 255}))
      refute Block.contains?(block, Block.encode({192, 169, 0, 0}))
      refute Block.contains?(block, Block.encode({191, 168, 0, 0}))
      refute Block.contains?(block, Block.encode({191, 255, 255, 255}))
      refute Block.contains?(block, Block.encode({194, 0, 0, 0}))
      refute Block.contains?(block, Block.encode({31, 41, 59, 27}))
    end

    test "with exact match" do
      block = Block.parse!("127.0.0.1/32")
      refute Block.contains?(block, Block.encode({127, 0, 0, 0}))
      assert Block.contains?(block, Block.encode({127, 0, 0, 1}))
      refute Block.contains?(block, Block.encode({127, 0, 0, 2}))
    end

    test "with zero-length prefix" do
      block = Block.parse!("0.0.0.0/0")
      [a, b, c, d] = octets(4)
      assert Block.contains?(block, Block.encode({a, b, c, d}))
    end

    test "with lower bits that are masked off" do
      block = Block.parse!("192.168.100.14/24")
      assert block.net == :binary.decode_unsigned(<<192, 168, 100, 0>>)

      for member <- 0..255 do
        assert Block.contains?(block, Block.encode({192, 168, 100, member}))
      end
    end

    test "against IPv6" do
      block = Block.parse!("0.0.0.0/0")
      [a, b, c, d, e, f, g, h] = hextets(8)
      refute Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h}))
    end
  end

  describe "IPv6 membership" do
    test "inside block" do
      block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120")

      for member <- 0x8800..0x88FF do
        assert Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, member}))
      end
    end

    test "outside block" do
      block = Block.parse!("1111:2222:3333:4444:5555:6666:7777:8800/120")
      refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x87FF}))
      refute Block.contains?(block, Block.encode({0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777, 0x8900}))
    end

    test "with exact match" do
      block = Block.parse!("::1/128")
      refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 0}))
      assert Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 1}))
      refute Block.contains?(block, Block.encode({0, 0, 0, 0, 0, 0, 0, 2}))
    end

    test "with zero-length prefix" do
      block = Block.parse!("::/0")
      [a, b, c, d, e, f, g, h] = hextets(8)
      assert Block.contains?(block, Block.encode({a, b, c, d, e, f, g, h}))
    end

    test "with lower bits that are masked off" do
      block = Block.parse!("a:b:c:d:e:f::/48")
      assert block.net == :binary.decode_unsigned(<<0x000A::16, 0x000B::16, 0x00C::16, 0::16, 0::16, 0::16, 0::16, 0::16>>)
      [d, e, f, g, h] = hextets(5)
      assert Block.contains?(block, Block.encode({0x000A, 0x000B, 0x000C, d, e, f, g, h}))
    end

    test "against IPv4" do
      block = Block.parse!("::/0")
      [a, b, c, d] = octets(4)
      refute Block.contains?(block, Block.encode({a, b, c, d}))
    end
  end
end
