{.used.}

# Nim-Libp2p
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
#  * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

import std/[strutils, sequtils, tables]
import chronos
import ../libp2p/[stream/connection,
                  transports/tcptransport,
                  upgrademngrs/upgrade,
                  multiaddress,
                  nameresolving/nameresolver,
                  nameresolving/dnsresolver,
                  nameresolving/mockresolver]

import ./helpers
#
#Cloudflare
const fallbackDnsServers = @[
  initTAddress("1.1.1.1:53"),
  initTAddress("1.0.0.1:53"),
  initTAddress("[2606:4700:4700::1111]:53")
]

const unixPlatform = defined(linux) or defined(solaris) or
                     defined(macosx) or defined(freebsd) or
                     defined(netbsd) or defined(openbsd) or
                     defined(dragonfly)


proc guessOsNameServers(): seq[TransportAddress] =
  when unixPlatform:
    var resultSeq = newSeqOfCap[TransportAddress](3)
    try:
      for l in lines("/etc/resolv.conf"):
        let lineParsed = l.strip().split(seps = Whitespace + {'%'}, maxsplit = 2)
        if lineParsed.len < 2: continue
        if lineParsed[0].startsWith('#'): continue

        if lineParsed[0] == "nameserver":
          resultSeq.add(initTAddress(lineParsed[1], Port(53)))

          if resultSeq.len > 2: break #3 nameserver max on linux
    except CatchableError as err:
      echo "Failed to get unix nameservers ", err.msg
    finally:
      if resultSeq.len > 0:
        return resultSeq
      return fallbackDnsServers
  elif defined(windows):
    #TODO
    return fallbackDnsServers
  else:
    return fallbackDnsServers


suite "Name resolving":
  suite "Generic Resolving":
    var resolver {.threadvar.}: MockResolver

    proc testOne(input: string, output: seq[MultiAddress]): bool =
      let resolved = waitFor resolver.resolveMAddress(MultiAddress.init(input).tryGet())
      if resolved != output:
        echo "Expected ", output
        echo "Got ", resolved
        return false
      return true

    proc testOne(input: string, output: seq[string]): bool =
      testOne(input, output.mapIt(MultiAddress.init(it).tryGet()))

    proc testOne(input, output: string): bool =
      testOne(input, @[MultiAddress.init(output).tryGet()])

    asyncSetup:
      resolver = MockResolver.new()

    asyncTest "test multi address dns resolve":
      resolver.ipResponses[("localhost", false)] = @["127.0.0.1"]
      resolver.ipResponses[("localhost", true)] = @["::1"]

      check testOne("/dns/localhost/udp/0", @["/ip4/127.0.0.1/udp/0", "/ip6/::1/udp/0"])
      check testOne("/dns4/localhost/tcp/0", "/ip4/127.0.0.1/tcp/0")
      check testOne("/dns6/localhost/tcp/0", "/ip6/::1/tcp/0")
      check testOne("/dns6/localhost/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "/ip6/::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN")

    asyncTest "test non dns resolve":
      resolver.ipResponses[("localhost", false)] = @["127.0.0.1"]
      resolver.ipResponses[("localhost", true)] = @["::1"]

      check testOne("/ip6/::1/tcp/0", "/ip6/::1/tcp/0")

    asyncTest "dnsaddr recursive test":
      resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @[
        "dnsaddr=/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"
      ]

      resolver.txtResponses["_dnsaddr.sjc-1.bootstrap.libp2p.io"] = @[
        "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN"
      ]

      resolver.txtResponses["_dnsaddr.ams-2.bootstrap.libp2p.io"] = @[
        "dnsaddr=/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
        "dnsaddr=/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb"
      ]

      check testOne("/dnsaddr/bootstrap.libp2p.io/", @[
        "/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "/ip4/147.75.83.83/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
        "/ip6/2604:1380:2000:7a00::1/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
      ])

    asyncTest "dnsaddr suffix matching test":
      resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @[
        "dnsaddr=/dnsaddr/ams-2.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
        "dnsaddr=/dnsaddr/sjc-1.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "dnsaddr=/dnsaddr/nrt-1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
        "dnsaddr=/dnsaddr/ewr-1.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
      ]

      resolver.txtResponses["_dnsaddr.sjc-1.bootstrap.libp2p.io"] = @[
        "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
      ]

      resolver.txtResponses["_dnsaddr.ams-1.bootstrap.libp2p.io"] = @[
        "dnsaddr=/ip4/147.75.69.143/tcp/4001/p2p/shouldbefiltered",
        "dnsaddr=/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/shouldbefiltered",
      ]

      check testOne("/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", @[
        "/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
        "/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
      ])

    asyncTest "dnsaddr infinite recursion":
      resolver.txtResponses["_dnsaddr.bootstrap.libp2p.io"] = @["dnsaddr=/dnsaddr/bootstrap.libp2p.io"]

      check testOne("/dnsaddr/bootstrap.libp2p.io/", newSeq[string]())

    test "getHostname":
      check:
        MultiAddress.init("/dnsaddr/bootstrap.libp2p.io/").tryGet().getHostname == "bootstrap.libp2p.io"
        MultiAddress.init("/ip4/147.75.69.143/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN").tryGet().getHostname == "147.75.69.143"
        MultiAddress.init("/ip6/2604:1380:1000:6000::1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN").tryGet().getHostname == "2604:1380:1000:6000::1"
        MultiAddress.init("/dns/localhost/udp/0").tryGet().getHostname == "localhost"
        MultiAddress.init("/dns4/hello.com/udp/0").tryGet().getHostname == "hello.com"
        MultiAddress.init("/dns6/hello.com/udp/0").tryGet().getHostname == "hello.com"
        MultiAddress.init("/wss/").tryGet().getHostname == ""

  suite "DNS Resolving":
    teardown:
      checkTrackers()

    asyncTest "test manual dns ip resolve":
      ## DNS mock server
      proc clientMark1(transp: DatagramTransport,
                       raddr: TransportAddress): Future[void] {.async.} =
        var msg = transp.getMessage()
        let
          resp = if msg[24] == 1: #AAAA or A
              "\xae\xbf\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" &
              "\x74\x75\x73\x02\x69\x6d\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x18\xb5\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\xac\x43\x0a\xa1\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x19\xb5"
            else:
              "\xe8\xc5\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" &
              "\x74\x75\x73\x02\x69\x6d\x00\x00\x1c\x00\x01\xc0\x0c\x00\x1c\x00" &
              "\x01\x00\x00\x00\x4f\x00\x10\x26\x06\x47\x00\x00\x10\x00\x00\x00" &
              "\x00\x00\x00\x68\x16\x19\xb5\xc0\x0c\x00\x1c\x00\x01\x00\x00\x00" &
              "\x4f\x00\x10\x26\x06\x47\x00\x00\x10\x00\x00\x00\x00\x00\x00\x68" &
              "\x16\x18\xb5\xc0\x0c\x00\x1c\x00\x01\x00\x00\x00\x4f\x00\x10\x26" &
              "\x06\x47\x00\x00\x10\x00\x00\x00\x00\x00\x00\xac\x43\x0a\xa1"
        await transp.sendTo(raddr, resp)

      let server = newDatagramTransport(clientMark1)

      # The test
      var dnsresolver = DnsResolver.new(@[server.localAddress])

      check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_UNSPEC)) ==
        mapIt(
          @["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0",
          "[2606:4700:10::6816:19b5]:0", "[2606:4700:10::6816:18b5]:0", "[2606:4700:10::ac43:aa1]:0"
        ], initTAddress(it))
      check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) ==
        mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it))
      check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET6)) ==
        mapIt(@["[2606:4700:10::6816:19b5]:0", "[2606:4700:10::6816:18b5]:0", "[2606:4700:10::ac43:aa1]:0"], initTAddress(it))

      await server.closeWait()

    asyncTest "test unresponsive dns server":
      var unresponsiveTentatives = 0
      ## DNS mock server
      proc clientMark1(transp: DatagramTransport,
                       raddr: TransportAddress): Future[void] {.async.} =
        unresponsiveTentatives.inc()
        check unresponsiveTentatives == 1

      proc clientMark2(transp: DatagramTransport,
                       raddr: TransportAddress): Future[void] {.async.} =
        var msg = transp.getMessage()
        let resp =
              "\xae\xbf\x81\x80\x00\x01\x00\x03\x00\x00\x00\x00\x06\x73\x74\x61" &
              "\x74\x75\x73\x02\x69\x6d\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x18\xb5\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\xac\x43\x0a\xa1\xc0\x0c\x00\x01\x00" &
              "\x01\x00\x00\x00\x4f\x00\x04\x68\x16\x19\xb5"
        await transp.sendTo(raddr, resp)

      let
        unresponsiveServer = newDatagramTransport(clientMark1)
        server = newDatagramTransport(clientMark2)

      # The test
      var dnsresolver = DnsResolver.new(@[unresponsiveServer.localAddress, server.localAddress])

      check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) ==
        mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it))

      check await(dnsresolver.resolveIp("status.im", 0.Port, Domain.AF_INET)) ==
        mapIt(@["104.22.24.181:0", "172.67.10.161:0", "104.22.25.181:0"], initTAddress(it))

      await server.closeWait()
      await unresponsiveServer.closeWait()

    asyncTest "inexisting domain resolving":
      var dnsresolver = DnsResolver.new(guessOsNameServers())
      let invalid = await dnsresolver.resolveIp("thisdomain.doesnot.exist", 0.Port)
      check invalid.len == 0

    asyncTest "wrong domain resolving":
      var dnsresolver = DnsResolver.new(guessOsNameServers())
      let invalid = await dnsresolver.resolveIp("", 0.Port)
      check invalid.len == 0

    asyncTest "unreachable dns server":
      var dnsresolver = DnsResolver.new(@[initTAddress("172.67.10.161:53")])
      let invalid = await dnsresolver.resolveIp("google.fr", 0.Port)
      check invalid.len == 0