-- | The CIDR modules contains most of the functions used for working
--   with the CIDR type.
module Cidr
( Cidr(..),
  cidr_properties,
  cidr_tests,
  combine_all,
  contains,
  contains_proper,
  enumerate,
  max_octet1,
  max_octet2,
  max_octet3,
  max_octet4,
  min_octet1,
  min_octet2,
  min_octet3,
  min_octet4,
  normalize
) where

import Data.List (nub)
import Data.List.Split (splitOneOf)
import Data.Maybe (catMaybes, mapMaybe)

import Test.Tasty ( TestTree, localOption, testGroup )
import Test.Tasty.HUnit ( (@?=), testCase )
import Test.Tasty.QuickCheck (
  Arbitrary( arbitrary ),
  Gen,
  Property,
  QuickCheckTests( QuickCheckTests ),
  (==>),
  testProperty )
import Text.Read (readMaybe)

import qualified Bit as B (Bit(..))
import IPv4Address (
  IPv4Address( IPv4Address, octet1, octet2, octet3, octet4 ),
  most_sig_bit_different )
import Maskable (Maskable(apply_mask))
import Maskbits ( Maskbits(Zero) )
import Octet (Octet())


data Cidr = Cidr { ipv4address :: IPv4Address,
                   maskbits :: Maskbits }


instance Show Cidr where
    show cidr = (show (ipv4address cidr)) ++ "/" ++ (show (maskbits cidr))


instance Arbitrary Cidr where
    arbitrary = do
      ipv4 <- arbitrary :: Gen IPv4Address
      mask <- arbitrary :: Gen Maskbits
      return (Cidr ipv4 mask)


instance Eq Cidr where
  -- | Two CIDRs are equal if they have the same network bits and if
  --   their masks are the same. In other words, if they are the same
  --   after normalization.
  cidr1 == cidr2 = (cidr1 <= cidr2) && (cidr2 <= cidr1)

instance Ord Cidr where
  -- | The CIDR order is simply numeric, with the IPv4Address being
  --   considered first, before the mask. There was an arbitrary
  --   choice that had to be made here: which CIDR is smaller,
  --   127.0.0.1/8, or 127.0.0.1/32?
  --
  --   The arguments for 127.0.0.1/8 <= 127.0.0.1/32 are that it
  --   agrees with the numeric sort order on masks, and that it's
  --   generally nicer to see the big networks before the small ones.
  --
  --   On the other hand, this order disagrees with the containment
  --   partial order, since 127.0.0.1/32 is contained properly in
  --   127.0.0.1/8.
  --
  cidr1 <= cidr2 = if addr1 == addr2 then mask1 <= mask2 else addr1 <= addr2
    where
      Cidr addr1 mask1 = normalize cidr1
      Cidr addr2 mask2 = normalize cidr2

-- | Returns the mask portion of a CIDR address. That is, everything
--   after the trailing slash.
maskbits_from_cidr_string :: String -> Maybe Maskbits
maskbits_from_cidr_string s
  | length partlist == 2 = readMaybe (partlist !! 1)
  | otherwise = Nothing
    where
      partlist = splitOneOf "/" s


-- | Takes an IP address String in CIDR notation, and returns a list
--   of its octets (as Ints).
octets_from_cidr_string :: String -> [Octet]
octets_from_cidr_string s =
  case parts of
    (p1:p2:p3:p4:_) -> mapMaybe readMaybe [p1,p2,p3,p4]
    _ -> []
  where
    parts = splitOneOf "./" s

instance Read Cidr where
  -- | Parse everything or nothing.
  readsPrec _ s =
    case (octets_from_cidr_string s) of
      [oct1, oct2, oct3, oct4] ->
        case (maskbits_from_cidr_string s) of
          Just mbits ->
            [(Cidr (IPv4Address oct1 oct2 oct3 oct4) mbits, "")]
          _ -> []
      _ -> []


-- | Given a CIDR, return the minimum valid IPv4 address contained
--   within it.
min_host :: Cidr -> IPv4Address
min_host (Cidr addr mask) = apply_mask addr mask B.Zero

-- | Given a CIDR, return the maximum valid IPv4 address contained
--   within it.
max_host :: Cidr -> IPv4Address
max_host (Cidr addr mask) = apply_mask addr mask B.One

-- | Given a CIDR, return the first octet of the minimum valid IPv4
--   address contained within it.
min_octet1 :: Cidr -> Octet
min_octet1 cidr = octet1 (min_host cidr)

-- | Given a CIDR, return the second octet of the minimum valid IPv4
--   address contained within it.
min_octet2 :: Cidr -> Octet
min_octet2 cidr = octet2 (min_host cidr)

-- | Given a CIDR, return the third octet of the minimum valid IPv4
--   address contained within it.
min_octet3 :: Cidr -> Octet
min_octet3 cidr = octet3 (min_host cidr)

-- | Given a CIDR, return the fourth octet of the minimum valid IPv4
--   address contained within it.
min_octet4 :: Cidr -> Octet
min_octet4 cidr = octet4 (min_host cidr)

-- | Given a CIDR, return the first octet of the maximum valid IPv4
--   address contained within it.
max_octet1 :: Cidr -> Octet
max_octet1 cidr = octet1 (max_host cidr)

-- | Given a CIDR, return the second octet of the maximum valid IPv4
--   address contained within it.
max_octet2 :: Cidr -> Octet
max_octet2 cidr = octet2 (max_host cidr)

-- | Given a CIDR, return the third octet of the maximum valid IPv4
--   address contained within it.
max_octet3 :: Cidr -> Octet
max_octet3 cidr = octet3 (max_host cidr)

-- | Given a CIDR, return the fourth octet of the maximum valid IPv4
--   address contained within it.
max_octet4 :: Cidr -> Octet
max_octet4 cidr = octet4 (max_host cidr)



-- | Return true if the first argument (a CIDR range) contains the
--   second (another CIDR range). There are a lot of ways we can be
--   fed junk here. For lack of a better alternative, just return
--   False when we are given nonsense.
--
--   If the number of bits in the network part of the first address is
--   larger than the number of bits in the second, there is no way
--   that the first range can contain the second. For, if the number
--   of network bits is larger, then the number of host bits must be
--   smaller, and if cidr1 has fewer hosts than cidr2, cidr1 most
--   certainly does not contain cidr2.
--
--   On the other hand, if the first argument (cidr1) has fewer (or
--   the same number of) network bits as the second, it can contain
--   the second. In this case, we need to check that every host in
--   cidr2 is contained in cidr1. If a host in cidr2 is contained in
--   cidr1, then at least mbits1 of an address in cidr2 will match
--   cidr1. For example,
--
--   cidr1 = 192.168.1.0\/23, cidr2 = 192.168.1.100\/24
--
--   Here, cidr2 contains all of 192.168.1.0 through
--   192.168.1.255. However, cidr1 contains BOTH 192.168.0.0 through
--   192.168.0.255 and 192.168.1.0 through 192.168.1.255. In essence,
--   what we want to check is that cidr2 "begins with" something that
--   cidr1 CAN begin with. Since cidr1 can begin with 192.168.1, and
--   cidr2 DOES, cidr1 contains cidr2..
--
--   The way that we check this is to apply cidr1's mask to cidr2's
--   address and see if the result is the same as cidr1's mask applied
--   to cidr1's address.
--
contains :: Cidr -> Cidr -> Bool
contains (Cidr addr1 mbits1) (Cidr addr2 mbits2)
  | mbits1 > mbits2 = False
  | otherwise = addr1masked == addr2masked
  where
    addr1masked = apply_mask addr1 mbits1 B.Zero
    addr2masked = apply_mask addr2 mbits1 B.Zero


-- | Contains but is not equal to.
contains_proper :: Cidr -> Cidr -> Bool
contains_proper cidr1 cidr2 =
    (cidr1 `contains` cidr2) && (not (cidr2 `contains` cidr1))


-- | A CIDR range is redundant (with respect to the given list) if
--   another CIDR range in that list properly contains it.
redundant :: [Cidr] -> Cidr -> Bool
redundant cidrlist cidr = any ((flip contains_proper) cidr) cidrlist


-- | First, we look at all possible pairs of cidrs, and combine the
--   adjacent ones in to a new list. Then, we concatenate that list
--   with the original one, and filter out all of the redundancies. If
--   two adjacent Cidrs are combined into a larger one, they will be
--   removed in the second step since the larger Cidr must contain the
--   smaller two.
--
--   Once this is done, we see whether or not the result is different
--   than the argument that was passed in. If nothing changed, we're
--   done and return the list that was passed to us. However, if
--   something changed, we recurse and try to combine the list again.
combine_all :: [Cidr] -> [Cidr]
combine_all cidrs
  | cidrs == (combine_contained unique_cidrs) = cidrs
  | otherwise = combine_all (combine_contained unique_cidrs)
    where
      unique_cidrs = nub cidr_combinations
      cidr_combinations =
        cidrs ++ (catMaybes [ (combine_adjacent x y) | x <- cidrs, y <- cidrs ])


-- | Take a list of CIDR ranges and filter out all of the ones that
--   are contained entirelt within some other range in the list.
combine_contained :: [Cidr] -> [Cidr]
combine_contained cidrs =
  filter (not . (redundant cidrs)) cidrs


-- | If the two Cidrs are not adjacent, return Cidr.None. Otherwise,
--   decrement the maskbits of cidr1 and return that; it will contain
--   both cidr1 and cidr2.
combine_adjacent :: Cidr -> Cidr -> Maybe Cidr
combine_adjacent cidr1 cidr2
  | not (adjacent cidr1 cidr2) = Nothing
  | (maskbits cidr1 == Zero) = Nothing
  | otherwise = Just $ cidr1 { maskbits = pred (maskbits cidr1) }



-- | Determine whether or not two CIDR ranges are adjacent. If two
--   ranges lie consecutively within the IP space, they can be
--   combined. For example, 10.1.0.0/24 and 10.0.1.0/24 are adjacent,
--   and can be combined in to 10.1.0.0/23.
adjacent :: Cidr -> Cidr -> Bool
adjacent cidr1 cidr2
  | mbits1 /= mbits2 = False
  | mbits1 == Maskbits.Zero = False -- They're equal.
  | otherwise = (mbits1 == (most_sig_bit_different addr1 addr2))
  where
    addr1 = ipv4address cidr1
    addr2 = ipv4address cidr2
    mbits1 = maskbits cidr1
    mbits2 = maskbits cidr2


enumerate :: Cidr -> [IPv4Address]
enumerate cidr = [(min_host cidr)..(max_host cidr)]


-- | Replace any masked bits in this CIDR's IPv4Address with zeros.
normalize :: Cidr -> Cidr
normalize (Cidr addr mask) =
  Cidr nrml_addr mask
  where
    nrml_addr = apply_mask addr mask B.Zero

-- Test lists.
cidr_tests :: TestTree
cidr_tests =
  testGroup "CIDR Tests" [
    test_enumerate,
    test_min_host1,
    test_max_host1,
    test_equality1,
    test_contains1,
    test_contains2,
    test_contains_proper1,
    test_contains_proper2,
    test_adjacent1,
    test_adjacent2,
    test_adjacent3,
    test_adjacent4,
    test_combine_contained1,
    test_combine_contained2,
    test_combine_all1,
    test_combine_all2,
    test_combine_all3,
    test_normalize1,
    test_normalize2,
    test_normalize3,
    test_big_networks_come_first ]

cidr_properties :: TestTree
cidr_properties =
  testGroup "CIDR Properties" [
      prop_all_cidrs_contain_themselves,
      prop_contains_proper_antisymmetric,
      prop_normalize_idempotent,
      prop_normalize_preserves_equality,
      prop_ord_instance_antisymmetric,
      prop_ord_instance_reflexive,
      prop_ord_instance_transitive,
      prop_ord_uses_addr_when_masks_equal,
      prop_ord_uses_mask_when_addrs_equal,
      prop_ord_and_contains_disagree,
      prop_ord_minimum,
      prop_ord_maximum ]


-- HUnit Tests
test_enumerate :: TestTree
test_enumerate =
  testCase desc $ actual @?= expected
  where
    desc = "192.168.0.240/30 is enumerated correctly"
    oct1 = toEnum 192 :: Octet
    oct2 = toEnum 168 :: Octet
    oct3 = minBound :: Octet
    mk_ip = IPv4Address oct1 oct2 oct3
    addr1 = mk_ip $ toEnum 240
    addr2 = mk_ip $ toEnum 241
    addr3 = mk_ip $ toEnum 242
    addr4 = mk_ip $ toEnum 243
    expected = [addr1, addr2, addr3, addr4]
    actual = enumerate (read "192.168.0.240/30" :: Cidr)

test_min_host1 :: TestTree
test_min_host1 =
  testCase desc $ actual @?= expected
  where
    desc = "The minimum host in 10.0.0.0/24 is 10.0.0.0"
    actual = show $ min_host (read "10.0.0.0/24" :: Cidr)
    expected = "10.0.0.0"


test_max_host1 :: TestTree
test_max_host1 =
  testCase desc $ actual @?= expected
  where
    desc = "The maximum host in 10.0.0.0/24 is 10.0.0.255"
    actual = show $ max_host (read "10.0.0.0/24" :: Cidr)
    expected = "10.0.0.255"


test_equality1 :: TestTree
test_equality1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/23 equals itself"
    actual = read "10.1.1.0/23" :: Cidr
    expected = read "10.1.1.0/23" :: Cidr


test_contains1 :: TestTree
test_contains1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/23 contains 10.1.1.0/24"
    cidr1 = read "10.1.1.0/23" :: Cidr
    cidr2 = read "10.1.1.0/24" :: Cidr
    expected = True
    actual = cidr1 `contains` cidr2


test_contains2 :: TestTree
test_contains2 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/23 contains itself"
    cidr1 = read "10.1.1.0/23" :: Cidr
    expected = True
    actual = cidr1 `contains` cidr1


test_contains_proper1 :: TestTree
test_contains_proper1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/23 contains 10.1.1.0/24 properly"
    cidr1 = read "10.1.1.0/23" :: Cidr
    cidr2 = read "10.1.1.0/24" :: Cidr
    expected = True
    actual = cidr1 `contains_proper` cidr2


test_contains_proper2 :: TestTree
test_contains_proper2 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/23 does not contain itself properly"
    cidr1 = read "10.1.1.0/23" :: Cidr
    expected = False
    actual = cidr1 `contains_proper` cidr1


test_adjacent1 :: TestTree
test_adjacent1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.0.0/24 is adjacent to 10.1.1.0/24"
    cidr1 = read "10.1.0.0/24" :: Cidr
    cidr2 = read "10.1.1.0/24" :: Cidr
    expected = True
    actual = cidr1 `adjacent` cidr2


test_adjacent2 :: TestTree
test_adjacent2 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.0.0/23 is not adjacent to 10.1.0.0/24"
    cidr1 = read "10.1.0.0/23" :: Cidr
    cidr2 = read "10.1.0.0/24" :: Cidr
    expected = False
    actual = cidr1 `adjacent` cidr2


test_adjacent3 :: TestTree
test_adjacent3 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.0.0/24 is not adjacent to 10.2.5.0/24"
    cidr1 = read "10.1.0.0/24" :: Cidr
    cidr2 = read "10.2.5.0/24" :: Cidr
    expected = False
    actual = cidr1 `adjacent` cidr2


test_adjacent4 :: TestTree
test_adjacent4 =
  testCase desc $ actual @?= expected
  where
    desc = "10.1.1.0/24 is not adjacent to 10.1.2.0/24"
    cidr1 = read "10.1.1.0/24" :: Cidr
    cidr2 = read "10.1.2.0/24" :: Cidr
    expected = False
    actual = cidr1 `adjacent` cidr2

test_combine_contained1 :: TestTree
test_combine_contained1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.0.0.0/8, 10.1.0.0/16, and 10.1.1.0/24 combine to 10.0.0.0/8"
    cidr1 = read "10.0.0.0/8" :: Cidr
    cidr2 = read "10.1.0.0/16" :: Cidr
    cidr3 = read "10.1.1.0/24" :: Cidr
    test_cidrs = [cidr1, cidr2, cidr3]
    expected = [cidr1]
    actual = combine_contained test_cidrs

test_combine_contained2 :: TestTree
test_combine_contained2 =
  testCase desc $ actual @?= expected
  where
    desc = "192.168.3.0/23 does not contain 192.168.1.0/24"
    cidr1 = read "192.168.3.0/23" :: Cidr
    cidr2 = read "192.168.1.0/24" :: Cidr
    expected = [cidr1, cidr2]
    actual = combine_contained [cidr1, cidr2]


test_combine_all1 :: TestTree
test_combine_all1 =
  testCase desc $ actual @?= expected
  where
    desc = "10.0.0.0/24 is adjacent to 10.0.1.0/24 "
           ++ "and 10.0.3.0/23 contains 10.0.2.0/24"
    cidr1 = read "10.0.0.0/24" :: Cidr
    cidr2 = read "10.0.1.0/24" :: Cidr
    cidr3 = read "10.0.2.0/24" :: Cidr
    cidr4 = read "10.0.3.0/23" :: Cidr
    cidr5 = read "10.0.0.0/23" :: Cidr
    test_cidrs = [cidr1, cidr2, cidr3, cidr4, cidr5]
    expected = [read "10.0.0.0/22" :: Cidr]
    actual = combine_all test_cidrs


test_combine_all2 :: TestTree
test_combine_all2 =
  testCase desc $ actual @?= expected
  where
    desc = "127.0.0.1/32 combines with itself recursively"
    cidr1 = read "127.0.0.1/32" :: Cidr
    test_cidrs = [cidr1, cidr1, cidr1, cidr1, cidr1]
    expected = [cidr1]
    actual = combine_all test_cidrs


test_combine_all3 :: TestTree
test_combine_all3 =
  testCase desc $ actual @?= expected
  where
    desc = "10.0.0.16, 10.0.0.17, 10.0.0.18, and "
           ++ "10.0.0.19 get combined into 10.0.0.16/30"
    cidr1 = read "10.0.0.16/32" :: Cidr
    cidr2 = read "10.0.0.17/32" :: Cidr
    cidr3 = read "10.0.0.18/32" :: Cidr
    cidr4 = read "10.0.0.19/32" :: Cidr
    test_cidrs = [cidr1, cidr2, cidr3, cidr4]
    expected = [read "10.0.0.16/30" :: Cidr]
    actual = combine_all test_cidrs

test_normalize1 :: TestTree
test_normalize1 =
  testCase desc $ actual @?= expected
  where
    desc = "127.0.0.1/8 normalized is 127.0.0.0/8"
    expected = read "127.0.0.0/8" :: Cidr
    actual = normalize (read "127.0.0.1/8" :: Cidr)


test_normalize2 :: TestTree
test_normalize2 =
  testCase desc $ actual @?= expected
  where
    desc = "192.168.1.101/24 normalized is 192.168.1.0/24"
    expected = read "192.168.1.0/24" :: Cidr
    actual = normalize (read "192.168.1.101/24" :: Cidr)

test_normalize3 :: TestTree
test_normalize3 =
  testCase desc $ actual @?= expected
  where
    desc = "10.10.10.10/22 normalized is 10.10.8.0/22"
    expected = read "10.10.8.0/22" :: Cidr
    actual = normalize (read "10.10.10.10/22" :: Cidr)

-- | Test a stated property of the Ord instance, namely that the big
--   network 127.0.0.1/8 comes before the small network 127.0.0.1/32.
test_big_networks_come_first :: TestTree
test_big_networks_come_first =
  testCase desc $ actual @?= expected
  where
    desc = "127.0.0.1/8 comes before 127.0.0.1/32"
    big = read "127.0.0.1/8" :: Cidr
    small = read "127.0.0.1/32" :: Cidr
    expected = True
    actual = big <= small -- not a typo

-- QuickCheck Tests
prop_all_cidrs_contain_themselves :: TestTree
prop_all_cidrs_contain_themselves =
  testProperty "All CIDRs contain themselves" prop
  where
    prop :: Cidr -> Bool
    prop cidr1 = cidr1 `contains` cidr1


-- If cidr1 properly contains cidr2, then by definition cidr2
-- does not properly contain cidr1.
prop_contains_proper_antisymmetric :: TestTree
prop_contains_proper_antisymmetric =
  testProperty "CIDR proper containment is an antisymmetric relation" prop
  where
    prop :: Cidr -> Cidr -> Property
    prop cidr1 cidr2 =
      (cidr1 `contains_proper` cidr2) ==>
        (not (cidr2 `contains_proper` cidr1))


-- Running "normalize" a second time shouldn't do anything.
prop_normalize_idempotent :: TestTree
prop_normalize_idempotent =
  testProperty "The CIDR \"normalize\" function is idempotent" prop
  where
    prop :: Cidr -> Bool
    prop cidr = (normalize cidr) == (normalize (normalize cidr))

-- Normalization should not affect equality of two CIDRs.
prop_normalize_preserves_equality :: TestTree
prop_normalize_preserves_equality =
  testProperty "The CIDR \"normalize\" function preserves equality" prop
  where
    prop :: Cidr -> Cidr -> Bool
    prop cidr1 cidr2 = (cidr1 == cidr2) == (normalize cidr1 == normalize cidr2)


prop_ord_instance_reflexive :: TestTree
prop_ord_instance_reflexive =
  testProperty "The CIDR order is reflexive" prop
  where
    prop :: Cidr -> Bool
    prop cidr = cidr <= cidr


prop_ord_instance_transitive :: TestTree
prop_ord_instance_transitive =
  testProperty "The CIDR order is transitive" prop
  where
    prop :: Cidr -> Cidr -> Cidr -> Property
    prop cidr1 cidr2 cidr3 =
      (cidr1 <= cidr2 && cidr2 <= cidr3) ==> cidr1 <= cidr3

-- This is how Eq is currently implemented, but it is useful to have
-- around in case that changes. Try fewer instances of this than usual
-- because it's a rare condition.
prop_ord_instance_antisymmetric :: TestTree
prop_ord_instance_antisymmetric =
  localOption (QuickCheckTests 500) $
    testProperty "The CIDR order is antisymmetric" prop
  where
    prop :: Cidr -> Cidr -> Property
    prop cidr1 cidr2 =
      (cidr1 <= cidr2 && cidr2 <= cidr1) ==> cidr1 == cidr2


-- When comparing two CIDRs with the same mask, the comparison
-- should be numeric (i.e. whatever the IPv4Address does).
-- Of course, we have to normalize first.
prop_ord_uses_addr_when_masks_equal :: TestTree
prop_ord_uses_addr_when_masks_equal =
  testProperty "The CIDR order is the IPv4Address order for equal masks" prop
  where
    prop :: Cidr -> Cidr -> Property
    prop cidr1 cidr2 =
      (mask1 == mask2) ==> (cidr1 <= cidr2) == (addr1 <= addr2)
      where
        (Cidr addr1 mask1) = normalize cidr1
        (Cidr addr2 mask2) = normalize cidr2


-- If we have two CIDRs whose normalized addresses agree, then we want
-- to use the mask order, i.e. that big networks come before small
-- networks. This disagrees with containment order.
prop_ord_uses_mask_when_addrs_equal :: TestTree
prop_ord_uses_mask_when_addrs_equal =
  localOption (QuickCheckTests 500) $
    testProperty "The CIDR order is by mask when the addresses agree" prop
  where
    prop :: Cidr -> Cidr -> Property
    prop cidr1 cidr2 =
      (addr1 == addr2) ==> (cidr1 <= cidr2) == (mask1 <= mask2)
      where
        (Cidr addr1 mask1) = normalize cidr1
        (Cidr addr2 mask2) = normalize cidr2


-- Big networks come first.
prop_ord_and_contains_disagree :: TestTree
prop_ord_and_contains_disagree =
  testProperty "The CIDR order disagrees with containment" prop
  where
    prop :: Cidr -> Cidr -> Property
    prop cidr1 cidr2 = (cidr1 `contains` cidr2) ==> (cidr1 <= cidr2)


-- The biggest network always comes first.
prop_ord_minimum :: TestTree
prop_ord_minimum =
  testProperty "The CIDR order has 0.0.0.0/0 as a minimum" prop
  where
    min_cidr = read "0.0.0.0/0" :: Cidr
    prop :: Cidr -> Bool
    prop cidr = min_cidr <= cidr


-- The CIDR order also has a maximum.
prop_ord_maximum :: TestTree
prop_ord_maximum =
  testProperty "The CIDR order has 255.255.255.255/32 as a maximum" prop
  where
    max_cidr = read "255.255.255.255/32" :: Cidr
    prop :: Cidr -> Bool
    prop cidr = max_cidr >= cidr
