package bind

import (
	"bytes"
	"fmt"
	"log"
	"math/rand"
	"testing"

	"github.com/miekg/dns"
	"github.com/miekg/dns/dnsutil"
)

func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
	// Take a zonefile, parse it, then generate a zone. We should
	// get back the same string.
	// This is used after any WriteZoneFile test as an extra verification step.

	// Parse the output:
	var parsed []dns.RR
	for x := range dns.ParseZone(buf, "bosun.org", "bosun.org.zone") {
		if x.Error != nil {
			log.Fatalf("Error in zonefile: %v", x.Error)
		} else {
			parsed = append(parsed, x.RR)
		}
	}
	// Generate it back:
	buf2 := &bytes.Buffer{}
	WriteZoneFile(buf2, parsed, "bosun.org.")

	// Compare:
	if buf2.String() != expected {
		t.Fatalf("Regenerated zonefile does not match: got=(\n%v\n)\nexpected=(\n%v\n)\n", buf2.String(), expected)
	}
}

func TestMostCommonTtl(t *testing.T) {
	var records []dns.RR
	var g, e uint32
	r1, _ := dns.NewRR("bosun.org. 100 IN A 1.1.1.1")
	r2, _ := dns.NewRR("bosun.org. 200 IN A 1.1.1.1")
	r3, _ := dns.NewRR("bosun.org. 300 IN A 1.1.1.1")
	r4, _ := dns.NewRR("bosun.org. 400 IN NS foo.bosun.org.")
	r5, _ := dns.NewRR("bosun.org. 400 IN NS bar.bosun.org.")

	// All records are TTL=100
	records = nil
	records, e = append(records, r1, r1, r1), 100
	g = mostCommonTtl(records)
	if e != g {
		t.Fatalf("expected %d; got %d\n", e, g)
	}

	// Mixture of TTLs with an obvious winner.
	records = nil
	records, e = append(records, r1, r2, r2), 200
	g = mostCommonTtl(records)
	if e != g {
		t.Fatalf("expected %d; got %d\n", e, g)
	}

	// 3-way tie. Largest TTL should be used.
	records = nil
	records, e = append(records, r1, r2, r3), 300
	g = mostCommonTtl(records)
	if e != g {
		t.Fatalf("expected %d; got %d\n", e, g)
	}

	// NS records are ignored.
	records = nil
	records, e = append(records, r1, r4, r5), 100
	g = mostCommonTtl(records)
	if e != g {
		t.Fatalf("expected %d; got %d\n", e, g)
	}

}

// func WriteZoneFile

func TestWriteZoneFileSimple(t *testing.T) {
	r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
	r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
	r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3}, "bosun.org.")
	expected := `$TTL 300
@                IN A     192.30.252.153
                 IN A     192.30.252.154
www              IN CNAME bosun.org.
`
	if buf.String() != expected {
		t.Log(buf.String())
		t.Log(expected)
		t.Fatalf("Zone file does not match.")
	}

	parseAndRegen(t, buf, expected)
}

func TestWriteZoneFileSimpleTtl(t *testing.T) {
	r1, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.153")
	r2, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.154")
	r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
	r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org.")
	expected := `$TTL 100
@                IN A     192.30.252.153
                 IN A     192.30.252.154
                 IN A     192.30.252.155
www        300   IN CNAME bosun.org.
`
	if buf.String() != expected {
		t.Log(buf.String())
		t.Log(expected)
		t.Fatalf("Zone file does not match.")
	}

	parseAndRegen(t, buf, expected)
}

func TestWriteZoneFileMx(t *testing.T) {
	//exhibits explicit ttls and long name
	r1, _ := dns.NewRR(`bosun.org. 300 IN TXT "aaa"`)
	r2, _ := dns.NewRR(`bosun.org. 300 IN TXT "bbb"`)
	r2.(*dns.TXT).Txt[0] = `b"bb`
	r3, _ := dns.NewRR("bosun.org. 300 IN MX 1 ASPMX.L.GOOGLE.COM.")
	r4, _ := dns.NewRR("bosun.org. 300 IN MX 5 ALT1.ASPMX.L.GOOGLE.COM.")
	r5, _ := dns.NewRR("bosun.org. 300 IN MX 10 ASPMX3.GOOGLEMAIL.COM.")
	r6, _ := dns.NewRR("bosun.org. 300 IN A 198.252.206.16")
	r7, _ := dns.NewRR("*.bosun.org. 600 IN A 198.252.206.16")
	r8, _ := dns.NewRR(`_domainkey.bosun.org. 300 IN TXT "vvvv"`)
	r9, _ := dns.NewRR(`google._domainkey.bosun.org. 300 IN TXT "\"foo\""`)
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org")
	if buf.String() != testdataZFMX {
		t.Log(buf.String())
		t.Log(testdataZFMX)
		t.Fatalf("Zone file does not match.")
	}
	parseAndRegen(t, buf, testdataZFMX)
}

var testdataZFMX = `$TTL 300
@                IN A     198.252.206.16
                 IN MX    1 ASPMX.L.GOOGLE.COM.
                 IN MX    5 ALT1.ASPMX.L.GOOGLE.COM.
                 IN MX    10 ASPMX3.GOOGLEMAIL.COM.
                 IN TXT   "aaa"
                 IN TXT   "b\"bb"
*          600   IN A     198.252.206.16
_domainkey       IN TXT   "vvvv"
google._domainkey IN TXT  "\"foo\""
`

func TestWriteZoneFileSrv(t *testing.T) {
	//exhibits explicit ttls and long name
	r1, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`)
	r2, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 20 5050 foo.com.`)
	r3, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
	r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
	r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org")
	if buf.String() != testdataZFSRV {
		t.Log(buf.String())
		t.Log(testdataZFSRV)
		t.Fatalf("Zone file does not match.")
	}
	parseAndRegen(t, buf, testdataZFSRV)
}

var testdataZFSRV = `$TTL 300
@                IN SRV   10 10 5050 foo.com.
                 IN SRV   10 10 5050 foo.com.
                 IN SRV   10 20 5050 foo.com.
                 IN SRV   20 10 5050 foo.com.
                 IN SRV   10 10 9999 foo.com.
`

func TestWriteZoneFilePtr(t *testing.T) {
	//exhibits explicit ttls and long name
	r1, _ := dns.NewRR(`bosun.org. 300 IN PTR chell.bosun.org`)
	r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
	r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3}, "bosun.org")
	if buf.String() != testdataZFPTR {
		t.Log(buf.String())
		t.Log(testdataZFPTR)
		t.Fatalf("Zone file does not match.")
	}
	parseAndRegen(t, buf, testdataZFPTR)
}

var testdataZFPTR = `$TTL 300
@                IN PTR   alex.bosun.org.
                 IN PTR   barney.bosun.org.
                 IN PTR   chell.bosun.org.
`

func TestWriteZoneFileCaa(t *testing.T) {
	//exhibits explicit ttls and long name
	r1, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issuewild ";"`)
	r2, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`)
	r3, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "http://example.com"`)
	r4, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.com"`)
	r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
	r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
	buf := &bytes.Buffer{}
	WriteZoneFile(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org")
	if buf.String() != testdataZFCAA {
		t.Log(buf.String())
		t.Log(testdataZFCAA)
		t.Fatalf("Zone file does not match.")
	}
	parseAndRegen(t, buf, testdataZFCAA)
}

var testdataZFCAA = `$TTL 300
@                IN CAA   1 iodef "http://example.com"
                 IN CAA   1 iodef "mailto:example.com"
                 IN CAA   0 iodef "https://example.com"
                 IN CAA   0 iodef "https://example.net"
                 IN CAA   0 issue "letsencrypt.org"
                 IN CAA   0 issuewild ";"
`

func TestWriteZoneFileOrder(t *testing.T) {
	var records []dns.RR
	for i, td := range []string{
		"@",
		"@",
		"@",
		"stackoverflow.com.",
		"*",
		"foo",
		"bar.foo",
		"hip.foo",
		"mup",
		"a.mup",
		"bzt.mup",
		"aaa.bzt.mup",
		"zzz.bzt.mup",
		"nnn.mup",
		"zt.mup",
		"zap",
	} {
		name := dnsutil.AddOrigin(td, "stackoverflow.com.")
		r, _ := dns.NewRR(fmt.Sprintf("%s 300 IN A 1.2.3.%d", name, i))
		records = append(records, r)
	}

	buf := &bytes.Buffer{}
	WriteZoneFile(buf, records, "stackoverflow.com.")
	// Compare
	if buf.String() != testdataOrder {
		t.Log("Found:")
		t.Log(buf.String())
		t.Log("Expected:")
		t.Log(testdataOrder)
		t.Fatalf("Zone file does not match.")
	}
	parseAndRegen(t, buf, testdataOrder)

	// Now shuffle the list many times and make sure it still works:
	for iteration := 5; iteration > 0; iteration-- {
		// Randomize the list:
		perm := rand.Perm(len(records))
		for i, v := range perm {
			records[i], records[v] = records[v], records[i]
			//fmt.Println(i, v)
		}
		// Generate
		buf := &bytes.Buffer{}
		WriteZoneFile(buf, records, "stackoverflow.com.")
		// Compare
		if buf.String() != testdataOrder {
			t.Log(buf.String())
			t.Log(testdataOrder)
			t.Fatalf("Zone file does not match.")
		}
		parseAndRegen(t, buf, testdataOrder)
	}
}

var testdataOrder = `$TTL 300
@                IN A     1.2.3.0
                 IN A     1.2.3.1
                 IN A     1.2.3.2
                 IN A     1.2.3.3
*                IN A     1.2.3.4
foo              IN A     1.2.3.5
bar.foo          IN A     1.2.3.6
hip.foo          IN A     1.2.3.7
mup              IN A     1.2.3.8
a.mup            IN A     1.2.3.9
bzt.mup          IN A     1.2.3.10
aaa.bzt.mup      IN A     1.2.3.11
zzz.bzt.mup      IN A     1.2.3.12
nnn.mup          IN A     1.2.3.13
zt.mup           IN A     1.2.3.14
zap              IN A     1.2.3.15
`

// func formatLine

func TestFormatLine(t *testing.T) {
	tests := []struct {
		lengths  []int
		fields   []string
		expected string
	}{
		{[]int{2, 2, 0}, []string{"a", "b", "c"}, "a  b  c"},
		{[]int{2, 2, 0}, []string{"aaaaa", "b", "c"}, "aaaaa b c"},
	}
	for _, ts := range tests {
		actual := formatLine(ts.lengths, ts.fields)
		if actual != ts.expected {
			t.Errorf("\"%s\" != \"%s\"", actual, ts.expected)
		}
	}
}

// func zoneLabelLess

func TestZoneLabelLess(t *testing.T) {
	/*
			The zone should sort in prefix traversal order:

		  @
		  *
		  foo
		  bar.foo
		  hip.foo
		  mup
		  a.mup
		  bzt.mup
		  *.bzt.mup
		  1.bzt.mup
		  2.bzt.mup
		  10.bzt.mup
		  aaa.bzt.mup
		  zzz.bzt.mup
		  nnn.mup
		  zt.mup
		  zap
	*/

	var tests = []struct {
		e1, e2   string
		expected bool
	}{
		{"@", "@", false},
		{"@", "*", true},
		{"@", "b", true},
		{"*", "@", false},
		{"*", "*", false},
		{"*", "b", true},
		{"foo", "foo", false},
		{"foo", "bar", false},
		{"bar", "foo", true},
		{"a.mup", "mup", false},
		{"mup", "a.mup", true},
		{"a.mup", "a.mup", false},
		{"a.mup", "bzt.mup", true},
		{"a.mup", "aa.mup", true},
		{"zt.mup", "aaa.bzt.mup", false},
		{"aaa.bzt.mup", "mup", false},
		{"*.bzt.mup", "aaa.bzt.mup", true},
		{"1.bzt.mup", "aaa.bzt.mup", true},
		{"1.bzt.mup", "2.bzt.mup", true},
		{"10.bzt.mup", "2.bzt.mup", false},
		{"nnn.mup", "aaa.bzt.mup", false},
		{`www\.miek.nl`, `www.miek.nl`, false},
	}

	for _, test := range tests {
		actual := zoneLabelLess(test.e1, test.e2)
		if test.expected != actual {
			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
		}
		actual = zoneLabelLess(test.e2, test.e1)
		// The reverse should work too:
		var expected bool
		if test.e1 == test.e2 {
			expected = false
		} else {
			expected = !test.expected
		}
		if expected != actual {
			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
		}
	}
}

func TestZoneRrtypeLess(t *testing.T) {
	/*
		In zonefiles we want to list SOAs, then NSs, then all others.
	*/

	var tests = []struct {
		e1, e2   uint16
		expected bool
	}{
		{dns.TypeSOA, dns.TypeSOA, false},
		{dns.TypeSOA, dns.TypeA, true},
		{dns.TypeSOA, dns.TypeTXT, true},
		{dns.TypeSOA, dns.TypeNS, true},
		{dns.TypeNS, dns.TypeSOA, false},
		{dns.TypeNS, dns.TypeA, true},
		{dns.TypeNS, dns.TypeTXT, true},
		{dns.TypeNS, dns.TypeNS, false},
		{dns.TypeA, dns.TypeSOA, false},
		{dns.TypeA, dns.TypeA, false},
		{dns.TypeA, dns.TypeTXT, true},
		{dns.TypeA, dns.TypeNS, false},
		{dns.TypeMX, dns.TypeSOA, false},
		{dns.TypeMX, dns.TypeA, false},
		{dns.TypeMX, dns.TypeTXT, true},
		{dns.TypeMX, dns.TypeNS, false},
	}

	for _, test := range tests {
		actual := zoneRrtypeLess(test.e1, test.e2)
		if test.expected != actual {
			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
		}
		actual = zoneRrtypeLess(test.e2, test.e1)
		// The reverse should work too:
		var expected bool
		if test.e1 == test.e2 {
			expected = false
		} else {
			expected = !test.expected
		}
		if expected != actual {
			t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
		}
	}
}
