// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package net

import (
	"cmp"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"internal/testenv"
	"os/exec"
	"reflect"
	"regexp"
	"slices"
	"strings"
	"syscall"
	"testing"
)

var nslookupTestServers = []string{"mail.golang.com", "gmail.com"}
var lookupTestIPs = []string{"8.8.8.8", "1.1.1.1"}

func toJson(v any) string {
	data, _ := json.Marshal(v)
	return string(data)
}

func testLookup(t *testing.T, fn func(*testing.T, *Resolver, string)) {
	for _, def := range []bool{true, false} {
		def := def
		for _, server := range nslookupTestServers {
			server := server
			var name string
			if def {
				name = "default/"
			} else {
				name = "go/"
			}
			t.Run(name+server, func(t *testing.T) {
				t.Parallel()
				r := DefaultResolver
				if !def {
					r = &Resolver{PreferGo: true}
				}
				fn(t, r, server)
			})
		}
	}
}

func TestNSLookupMX(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	testLookup(t, func(t *testing.T, r *Resolver, server string) {
		mx, err := r.LookupMX(context.Background(), server)
		if err != nil {
			t.Fatal(err)
		}
		if len(mx) == 0 {
			t.Fatal("no results")
		}
		expected, err := nslookupMX(server)
		if err != nil {
			t.Skipf("skipping failed nslookup %s test: %s", server, err)
		}
		byPrefAndHost := func(a, b *MX) int {
			if r := cmp.Compare(a.Pref, b.Pref); r != 0 {
				return r
			}
			return strings.Compare(a.Host, b.Host)
		}
		slices.SortFunc(expected, byPrefAndHost)
		slices.SortFunc(mx, byPrefAndHost)
		if !reflect.DeepEqual(expected, mx) {
			t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(mx))
		}
	})
}

func TestNSLookupCNAME(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	testLookup(t, func(t *testing.T, r *Resolver, server string) {
		cname, err := r.LookupCNAME(context.Background(), server)
		if err != nil {
			t.Fatalf("failed %s: %s", server, err)
		}
		if cname == "" {
			t.Fatalf("no result %s", server)
		}
		expected, err := nslookupCNAME(server)
		if err != nil {
			t.Skipf("skipping failed nslookup %s test: %s", server, err)
		}
		if expected != cname {
			t.Errorf("different results %s:\texp:%v\tgot:%v", server, expected, cname)
		}
	})
}

func TestNSLookupNS(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	testLookup(t, func(t *testing.T, r *Resolver, server string) {
		ns, err := r.LookupNS(context.Background(), server)
		if err != nil {
			t.Fatalf("failed %s: %s", server, err)
		}
		if len(ns) == 0 {
			t.Fatal("no results")
		}
		expected, err := nslookupNS(server)
		if err != nil {
			t.Skipf("skipping failed nslookup %s test: %s", server, err)
		}
		byHost := func(a, b *NS) int {
			return strings.Compare(a.Host, b.Host)
		}
		slices.SortFunc(expected, byHost)
		slices.SortFunc(ns, byHost)
		if !reflect.DeepEqual(expected, ns) {
			t.Errorf("different results %s:\texp:%v\tgot:%v", toJson(server), toJson(expected), ns)
		}
	})
}

func TestNSLookupTXT(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	testLookup(t, func(t *testing.T, r *Resolver, server string) {
		txt, err := r.LookupTXT(context.Background(), server)
		if err != nil {
			t.Fatalf("failed %s: %s", server, err)
		}
		if len(txt) == 0 {
			t.Fatalf("no results")
		}
		expected, err := nslookupTXT(server)
		if err != nil {
			t.Skipf("skipping failed nslookup %s test: %s", server, err)
		}
		slices.Sort(expected)
		slices.Sort(txt)
		if !slices.Equal(expected, txt) {
			t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(txt))
		}
	})
}

func TestLookupLocalPTR(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	addr, err := localIP()
	if err != nil {
		t.Errorf("failed to get local ip: %s", err)
	}
	names, err := LookupAddr(addr.String())
	if err != nil {
		t.Errorf("failed %s: %s", addr, err)
	}
	if len(names) == 0 {
		t.Errorf("no results")
	}
	expected, err := lookupPTR(addr.String())
	if err != nil {
		t.Skipf("skipping failed lookup %s test: %s", addr.String(), err)
	}
	slices.Sort(expected)
	slices.Sort(names)
	if !slices.Equal(expected, names) {
		t.Errorf("different results %s:\texp:%v\tgot:%v", addr, toJson(expected), toJson(names))
	}
}

func TestLookupPTR(t *testing.T) {
	testenv.MustHaveExternalNetwork(t)

	for _, addr := range lookupTestIPs {
		names, err := LookupAddr(addr)
		if err != nil {
			// The DNSError type stores the error as a string, so it cannot wrap the
			// original error code and we cannot check for it here. However, we can at
			// least use its error string to identify the correct localized text for
			// the error to skip.
			var DNS_ERROR_RCODE_SERVER_FAILURE syscall.Errno = 9002
			if strings.HasSuffix(err.Error(), DNS_ERROR_RCODE_SERVER_FAILURE.Error()) {
				testenv.SkipFlaky(t, 38111)
			}
			t.Errorf("failed %s: %s", addr, err)
		}
		if len(names) == 0 {
			t.Errorf("no results")
		}
		expected, err := lookupPTR(addr)
		if err != nil {
			t.Logf("skipping failed lookup %s test: %s", addr, err)
			continue
		}
		slices.Sort(expected)
		slices.Sort(names)
		if !slices.Equal(expected, names) {
			t.Errorf("different results %s:\texp:%v\tgot:%v", addr, toJson(expected), toJson(names))
		}
	}
}

func nslookup(qtype, name string) (string, error) {
	var out strings.Builder
	var err strings.Builder
	cmd := exec.Command("nslookup", "-querytype="+qtype, name)
	cmd.Stdout = &out
	cmd.Stderr = &err
	if err := cmd.Run(); err != nil {
		return "", err
	}
	r := strings.ReplaceAll(out.String(), "\r\n", "\n")
	// nslookup stderr output contains also debug information such as
	// "Non-authoritative answer" and it doesn't return the correct errcode
	if strings.Contains(err.String(), "can't find") {
		return r, errors.New(err.String())
	}
	return r, nil
}

func nslookupMX(name string) (mx []*MX, err error) {
	var r string
	if r, err = nslookup("mx", name); err != nil {
		return
	}
	mx = make([]*MX, 0, 10)
	// linux nslookup syntax
	// golang.org      mail exchanger = 2 alt1.aspmx.l.google.com.
	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+mail exchanger\s*=\s*([0-9]+)\s*([a-z0-9.\-]+)$`)
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		pref, _, _ := dtoi(ans[2])
		mx = append(mx, &MX{absDomainName(ans[3]), uint16(pref)})
	}
	// windows nslookup syntax
	// gmail.com       MX preference = 30, mail exchanger = alt3.gmail-smtp-in.l.google.com
	rx = regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+MX preference\s*=\s*([0-9]+)\s*,\s*mail exchanger\s*=\s*([a-z0-9.\-]+)$`)
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		pref, _, _ := dtoi(ans[2])
		mx = append(mx, &MX{absDomainName(ans[3]), uint16(pref)})
	}
	return
}

func nslookupNS(name string) (ns []*NS, err error) {
	var r string
	if r, err = nslookup("ns", name); err != nil {
		return
	}
	ns = make([]*NS, 0, 10)
	// golang.org      nameserver = ns1.google.com.
	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+nameserver\s*=\s*([a-z0-9.\-]+)$`)
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		ns = append(ns, &NS{absDomainName(ans[2])})
	}
	return
}

func nslookupCNAME(name string) (cname string, err error) {
	var r string
	if r, err = nslookup("cname", name); err != nil {
		return
	}
	// mail.golang.com canonical name = golang.org.
	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+canonical name\s*=\s*([a-z0-9.\-]+)$`)
	// assumes the last CNAME is the correct one
	last := name
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		last = ans[2]
	}
	return absDomainName(last), nil
}

func nslookupTXT(name string) (txt []string, err error) {
	var r string
	if r, err = nslookup("txt", name); err != nil {
		return
	}
	txt = make([]string, 0, 10)
	// linux
	// golang.org      text = "v=spf1 redirect=_spf.google.com"

	// windows
	// golang.org      text =
	//
	//    "v=spf1 redirect=_spf.google.com"
	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+text\s*=\s*"(.*)"$`)
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		txt = append(txt, ans[2])
	}
	return
}

func ping(name string) (string, error) {
	cmd := exec.Command("ping", "-n", "1", "-a", name)
	stdoutStderr, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("%v: %v", err, string(stdoutStderr))
	}
	r := strings.ReplaceAll(string(stdoutStderr), "\r\n", "\n")
	return r, nil
}

func lookupPTR(name string) (ptr []string, err error) {
	var r string
	if r, err = ping(name); err != nil {
		return
	}
	ptr = make([]string, 0, 10)
	rx := regexp.MustCompile(`(?m)^Pinging\s+([a-zA-Z0-9.\-]+)\s+\[.*$`)
	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
		ptr = append(ptr, absDomainName(ans[1]))
	}
	return
}

func localIP() (ip IP, err error) {
	conn, err := Dial("udp", "golang.org:80")
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	localAddr := conn.LocalAddr().(*UDPAddr)

	return localAddr.IP, nil
}
