open Dns

let ip = Ipaddr.V4.of_string_exn
let name = Domain_name.of_string_exn

let invalid_soa name =
  let p pre =
    Domain_name.(prepend_label_exn name "invalid" |> fun n -> prepend_label_exn n pre)
  in
  {
    Soa.nameserver = p "ns" ; hostmaster = p "hostmaster" ;
    serial = 1l ; refresh = 16384l ; retry = 2048l ;
    expiry = 1048576l ; minimum = 300l
  }

let cached_err =
  let module M = struct
    type t = [ `Cache_miss | `Cache_drop ]
    let pp ppf = function
      | `Cache_miss -> Fmt.string ppf "cache miss"
      | `Cache_drop -> Fmt.string ppf "cache drop"
    let equal a b = match a, b with
      | `Cache_miss, `Cache_miss -> true
      | `Cache_drop, `Cache_drop -> true
      | _ -> false
  end in
  (module M: Alcotest.TESTABLE with type t = M.t)

let entry_eq t a b =
  match a, b with
  | `Entry b, `Entry b' -> Rr_map.equal_rr t b b'
  | `No_data (name, soa), `No_data (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
  | `No_domain (name, soa), `No_domain (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
  | `Serv_fail (name, soa), `Serv_fail (name', soa') -> Domain_name.equal name name' && Dns.Soa.compare soa soa' = 0
  | _, _ -> false

let cached_ok (t : 'a Rr_map.key) =
  let pp ppf res = Dns_cache.pp_entry t ppf (fst res)
  and equal (r, rank) (r', rank') =
    entry_eq t r r' && Dns_cache.compare_rank rank rank' = 0
  in
  Alcotest.testable pp equal

let cached_r t = Alcotest.(result (cached_ok t) cached_err)

let empty_cache () =
  let cache = Dns_cache.empty 100 in
  Alcotest.check (cached_r Rr_map.A) "empty cache results in Cache_miss"
    (Error `Cache_miss)
    (snd (Dns_cache.get cache 0L (name "an-actual-website.com") A))

let cache_a () =
  let cache = Dns_cache.empty 100 in
  let name = name "an-actual-website.com" in
  let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
  let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
  Alcotest.check (cached_r Rr_map.A) "cache with A results in res"
    (Ok (`Entry a, AuthoritativeAnswer None))
    (snd (Dns_cache.get cache 0L name A)) ;
  Alcotest.check (cached_r Rr_map.Cname) "cache with A results in CacheMiss"
    (Error `Cache_miss) (snd (Dns_cache.get cache 0L name Cname))

let cache_nodata () =
  let cache = Dns_cache.empty 100 in
  let name = name "an-alias.com"
  and subname = name "another-domain.an-alias.com"
  in
  let soa = invalid_soa name in
  let nodata = `No_data (subname, soa) in
  let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
  let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
  let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) nodata in
  Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodata"
    (Ok (nodata, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A)) ;
  Alcotest.check (cached_r Rr_map.Ns) "cache with A nodata results in cache miss for NS"
    (Error `Cache_miss) (snd (Dns_cache.get cache 0L subname Ns)) ;
  Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in a record"
    (Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L name A)) ;
  Alcotest.check (cached_r Rr_map.Ns) "cache with A nodata results in cache miss for NS'"
    (Error `Cache_miss) (snd (Dns_cache.get cache 0L name Ns)) ;
  let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) (`Entry a) in
  Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodata"
    (Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A))

let cache_nodom () =
  let cache = Dns_cache.empty 100 in
  let name = name "an-alias.com"
  and subname = name "another-domain.an-alias.com"
  and subsubname = name "fo.another-domain.an-alias.com"
  in
  let soa = invalid_soa name in
  let nodom = `No_domain (subname, soa) in
  let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
  let cache = Dns_cache.set cache 0L name A (AuthoritativeAnswer None) (`Entry a) in
  let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) nodom in
  Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in nodom"
    (Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A)) ;
  Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in nodom for NS"
    (Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname Ns)) ;
  Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in nodom for Ns and subsub"
    (Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subsubname Ns)) ;
  Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in nodom for A and subsub"
    (Ok (nodom, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subsubname A)) ;
  Alcotest.check (cached_r Rr_map.A) "cache with A nodom results in a record"
    (Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L name A)) ;
  Alcotest.check (cached_r Rr_map.Ns) "cache with A nodom results in cache miss for NS'"
    (Error `Cache_miss) (snd (Dns_cache.get cache 0L name Ns)) ;
  let cache = Dns_cache.set cache 0L subname A (AuthoritativeAnswer None) (`Entry a) in
  Alcotest.check (cached_r Rr_map.A) "cache with A nodata results in nodom"
    (Ok (`Entry a, AuthoritativeAnswer None)) (snd (Dns_cache.get cache 0L subname A))

let cache_tests = [
  "empty cache", `Quick, empty_cache ;
  "cache with A", `Quick, cache_a ;
  "cache nodata", `Quick, cache_nodata ;
  "cache nodom", `Quick, cache_nodom ;
]

let entry_or_cname t a b = match a, b with
  | (`Alias (ttl, name), r),
    (`Alias (ttl', name'), r') ->
    ttl = ttl' && Domain_name.equal name name' && Dns_cache.compare_rank r r' = 0
 | (#Dns_cache.entry as e1, r1), (#Dns_cache.entry as e2, r2) ->
   entry_eq t e1 e2 && Dns_cache.compare_rank r1 r2 = 0
 | _ -> false

let pp_or_cname t ppf = function
 | `Alias (ttl, name), _ -> Fmt.pf ppf "alias %lu %a" ttl Domain_name.pp name
 | #Dns_cache.entry as e, _ -> Dns_cache.pp_entry t ppf e

let cname_or_cached t =
  Alcotest.testable (pp_or_cname t) (entry_or_cname t)

let cached_cname_r t = Alcotest.result (cname_or_cached t) cached_err

let empty = Dns_cache.empty 100

let cname_empty_cache () =
  Alcotest.check (cached_cname_r Rr_map.A) "empty cache results in Cache_miss"
    (Error `Cache_miss)
    (snd (Dns_cache.get_or_cname empty 0L (name "foo.com") A))

let cname_cache_a () =
  let name = name "foo.com" in
  let a = 250l, Ipaddr.V4.Set.singleton (ip "1.2.3.4") in
  let cache = Dns_cache.set empty 0L name A (AuthoritativeAnswer None) (`Entry a) in
  Alcotest.check (cached_cname_r Rr_map.A) "cache with A results in res"
    (Ok (`Entry a, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name A)) ;
  Alcotest.check (cached_cname_r Rr_map.Cname) "cache with A results in CacheMiss"
    (Error `Cache_miss)
    (snd (Dns_cache.get_or_cname cache 0L name Cname))

let cname_cache_cname () =
  let rel = name "bar.com" in
  let name = name "foo.com" in
  let cname = 250l, rel in
  let cache = Dns_cache.set empty 0L name Cname (AuthoritativeAnswer None) (`Entry cname) in
  Alcotest.check (cached_cname_r Rr_map.Cname) "cache with CNAME results in res"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name Cname)) ;
  Alcotest.check (cached_cname_r Rr_map.A) "cache with CNAME results in res for A"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name A)) ;
  Alcotest.check (cached_cname_r Rr_map.Ns) "cache with CNAME results in res for NS"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name Ns))

let cname_cache_cname_nodata () =
  let rel = name "bar.com" in
  let name = name "foo.com" in
  let cname = 250l, rel in
  let bad_soa = invalid_soa name in
  let cache =
    Dns_cache.set
      (Dns_cache.set empty 0L name Cname (AuthoritativeAnswer None) (`Entry cname))
      0L name Ns (AuthoritativeAnswer None) (`No_data (name, bad_soa))
  in
  Alcotest.check (cached_cname_r Rr_map.Cname) "cache with CNAME results in res"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name Cname)) ;
  Alcotest.check (cached_cname_r Rr_map.Ns) "cache with CNAME results in res for NS"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name Ns)) ;
  Alcotest.check (cached_cname_r Rr_map.A) "cache with CNAME results in res for A"
    (Ok (`Alias cname, AuthoritativeAnswer None))
    (snd (Dns_cache.get_or_cname cache 0L name A))

let cname_cache_tests = [
  "empty cache", `Quick, cname_empty_cache ;
  "cache with A", `Quick, cname_cache_a ;
  "cache with CNAME", `Quick, cname_cache_cname ;
  "cache with another cname", `Quick, cname_cache_cname_nodata ;
]

let tests = [
  "cache tests", cache_tests;
  "cname cache tests", cname_cache_tests;
]

let () = Alcotest.run "DNS cache tests" tests
