package mtasts

import (
	"reflect"
	"testing"

	"github.com/mjl-/mox/dns"
)

func TestRecord(t *testing.T) {
	good := func(txt string, want Record) {
		t.Helper()
		r, _, err := ParseRecord(txt)
		if err != nil {
			t.Fatalf("parse: %s", err)
		}
		if !reflect.DeepEqual(r, &want) {
			t.Fatalf("want %#v, got %#v", want, *r)
		}
	}

	bad := func(txt string) {
		t.Helper()
		r, _, err := ParseRecord(txt)
		if err == nil {
			t.Fatalf("parse, expected error, got record %v", r)
		}
	}

	good("v=STSv1; id=20160831085700Z;", Record{Version: "STSv1", ID: "20160831085700Z"})
	good("v=STSv1; \t id=20160831085700Z  \t;", Record{Version: "STSv1", ID: "20160831085700Z"})
	good("v=STSv1; id=a", Record{Version: "STSv1", ID: "a"})
	good("v=STSv1; id=a; more=a; ext=2", Record{Version: "STSv1", ID: "a", Extensions: []Pair{{"more", "a"}, {"ext", "2"}}})

	bad("v=STSv0")
	bad("v=STSv10")
	bad("v=STSv2")
	bad("v=STSv1")                                            // missing id
	bad("v=STSv1;")                                           // missing id
	bad("v=STSv1; ext=1")                                     // missing id
	bad("v=STSv1; id=")                                       // empty id
	bad("v=STSv1; id=012345678901234567890123456789012")      // id too long
	bad("v=STSv1; id=test-123")                               // invalid id
	bad("v=STSv1; id=a; more=")                               // empty value in extension
	bad("v=STSv1; id=a; a12345678901234567890123456789012=1") // extension name too long
	bad("v=STSv1; id=a; 1%=a")                                // invalid extension name
	bad("v=STSv1; id=a; test==")                              // invalid extension name
	bad("v=STSv1; id=a;;")                                    // additional semicolon

	const want = `v=STSv1; id=a; more=a; ext=2`
	record := Record{Version: "STSv1", ID: "a", Extensions: []Pair{{"more", "a"}, {"ext", "2"}}}
	got := record.String()
	if got != want {
		t.Fatalf("record string, got %q, want %q", got, want)
	}
}

func TestParsePolicy(t *testing.T) {
	good := func(s string, want Policy) {
		t.Helper()
		p, err := ParsePolicy(s)
		if err != nil {
			t.Fatalf("parse policy: %s", err)
		}
		if !reflect.DeepEqual(p, &want) {
			t.Fatalf("want %v, got %v", want, p)
		}
	}

	good(`version: STSv1
mode: testing
mx: mx1.example.com
mx: mx2.example.com
mx: mx.backup-example.com
max_age: 1296000
`,
		Policy{
			Version: "STSv1",
			Mode:    ModeTesting,
			MX: []STSMX{
				{Domain: dns.Domain{ASCII: "mx1.example.com"}},
				{Domain: dns.Domain{ASCII: "mx2.example.com"}},
				{Domain: dns.Domain{ASCII: "mx.backup-example.com"}},
			},
			MaxAgeSeconds: 1296000,
		},
	)
	good("version: STSv1\nmode: enforce	\nmx: *.example.com \nmax_age: 0 \n",
		Policy{
			Version: "STSv1",
			Mode:    ModeEnforce,
			MX: []STSMX{
				{Wildcard: true, Domain: dns.Domain{ASCII: "example.com"}},
			},
			MaxAgeSeconds: 0,
		},
	)
	good("version:STSv1\r\nmode:\tenforce\r\nmx:  \t\t *.example.com\nmax_age: 1\nmore:ext e  ns ion",
		Policy{
			Version: "STSv1",
			Mode:    ModeEnforce,
			MX: []STSMX{
				{Wildcard: true, Domain: dns.Domain{ASCII: "example.com"}},
			},
			MaxAgeSeconds: 1,
			Extensions:    []Pair{{"more", "ext e  ns ion"}},
		},
	)

	bad := func(s string) {
		t.Helper()
		p, err := ParsePolicy(s)
		if err == nil {
			t.Fatalf("parsing policy did not fail: %v", p)
		}
	}

	bad("")                                                                           // missing version
	bad("version:STSv0\nmode:none\nmax_age:0")                                        // bad version
	bad("version:STSv10\nmode:none\nmax_age:0")                                       // bad version
	bad("version:STSv2\nmode:none\nmax_age:0")                                        // bad version
	bad("version:STSv1\nmax_age:0\nmx:example.com")                                   // missing mode
	bad("version:STSv1\nmode:none")                                                   // missing max_age
	bad("version:STSv1\nmax_age:0\nmode:enforce")                                     // missing mx for mode
	bad("version:STSv1\nmax_age:0\nmode:testing")                                     // missing mx for mode
	bad("max_age:0\nmode:none")                                                       // missing version
	bad("version:STSv1\nmode:none\nmax_age:01234567890")                              // max_age too long
	bad("version:STSv1\nmode:bad\nmax_age:1")                                         // bad mode
	bad("version:STSv1\nmode:none\nmax_age:a")                                        // bad max_age
	bad("version:STSv1\nmode:enforce\nmax_age:0\nmx:")                                // missing value
	bad("version:STSv1\nmode:enforce\nmax_age:0\nmx:*.*.example")                     // bad mx
	bad("version:STSv1\nmode:enforce\nmax_age:0\nmx:**.example")                      // bad mx
	bad("version:STSv1\nmode:enforce\nmax_age:0\nmx:**.example-")                     // bad mx
	bad("version:STSv1\nmode:enforce\nmax_age:0\nmx:test.example-")                   // bad mx
	bad("version:STSv1\nmode:none\nmax_age:0\next:")                                  // empty extension
	bad("version:STSv1\nmode:none\nmax_age:0\na12345678901234567890123456789012:123") // long extension name
	bad("version:STSv1\nmode:none\nmax_age:0\n_bad:test")                             // bad ext name
	bad("version:STSv1\nmode:none\nmax_age:0\nmx: møx.example")                       // invalid u-label in mx

	policy := Policy{
		Version: "STSv1",
		Mode:    ModeTesting,
		MX: []STSMX{
			{Domain: dns.Domain{ASCII: "mx1.example.com"}},
			{Domain: dns.Domain{ASCII: "mx2.example.com"}},
			{Domain: dns.Domain{ASCII: "mx.backup-example.com"}},
		},
		MaxAgeSeconds: 1296000,
	}
	want := `version: STSv1
mode: testing
max_age: 1296000
mx: mx1.example.com
mx: mx2.example.com
mx: mx.backup-example.com
`
	got := policy.String()
	if got != want {
		t.Fatalf("policy string, got %q, want %q", got, want)
	}
}

func FuzzParseRecord(f *testing.F) {
	f.Add("v=STSv1; id=20160831085700Z;")
	f.Add("v=STSv1; \t id=20160831085700Z  \t;")
	f.Add("v=STSv1; id=a")
	f.Add("v=STSv1; id=a; more=a; ext=2")

	f.Add("v=STSv0")
	f.Add("v=STSv10")
	f.Add("v=STSv2")
	f.Add("v=STSv1")                                            // missing id
	f.Add("v=STSv1;")                                           // missing id
	f.Add("v=STSv1; ext=1")                                     // missing id
	f.Add("v=STSv1; id=")                                       // empty id
	f.Add("v=STSv1; id=012345678901234567890123456789012")      // id too long
	f.Add("v=STSv1; id=test-123")                               // invalid id
	f.Add("v=STSv1; id=a; more=")                               // empty value in extension
	f.Add("v=STSv1; id=a; a12345678901234567890123456789012=1") // extension name too long
	f.Add("v=STSv1; id=a; 1%=a")                                // invalid extension name
	f.Add("v=STSv1; id=a; test==")                              // invalid extension name
	f.Add("v=STSv1; id=a;;")                                    // additional semicolon

	f.Fuzz(func(t *testing.T, s string) {
		r, _, err := ParseRecord(s)
		if err == nil {
			_ = r.String()
		}
	})
}

func FuzzParsePolicy(f *testing.F) {
	f.Add(`version: STSv1
mode: testing
mx: mx1.example.com
mx: mx2.example.com
mx: mx.backup-example.com
max_age: 1296000
`)
	f.Add(`version: STSv1
mode: enforce
mx: *.example.com
max_age: 0
`)
	f.Add("version:STSv1\r\nmode:\tenforce\r\nmx:  \t\t *.example.com\nmax_age: 1\nmore:ext e  ns ion")

	f.Add("")                                                                           // missing version
	f.Add("version:STSv0\nmode:none\nmax_age:0")                                        // bad version
	f.Add("version:STSv10\nmode:none\nmax_age:0")                                       // bad version
	f.Add("version:STSv2\nmode:none\nmax_age:0")                                        // bad version
	f.Add("version:STSv1\nmax_age:0\nmx:example.com")                                   // missing mode
	f.Add("version:STSv1\nmode:none")                                                   // missing max_age
	f.Add("version:STSv1\nmax_age:0\nmode:enforce")                                     // missing mx for mode
	f.Add("version:STSv1\nmax_age:0\nmode:testing")                                     // missing mx for mode
	f.Add("max_age:0\nmode:none")                                                       // missing version
	f.Add("version:STSv1\nmode:none\nmax_age:0 ")                                       // trailing whitespace
	f.Add("version:STSv1\nmode:none\nmax_age:01234567890")                              // max_age too long
	f.Add("version:STSv1\nmode:bad\nmax_age:1")                                         // bad mode
	f.Add("version:STSv1\nmode:none\nmax_age:a")                                        // bad max_age
	f.Add("version:STSv1\nmode:enforce\nmax_age:0\nmx:")                                // missing value
	f.Add("version:STSv1\nmode:enforce\nmax_age:0\nmx:*.*.example")                     // bad mx
	f.Add("version:STSv1\nmode:enforce\nmax_age:0\nmx:**.example")                      // bad mx
	f.Add("version:STSv1\nmode:enforce\nmax_age:0\nmx:**.example-")                     // bad mx
	f.Add("version:STSv1\nmode:enforce\nmax_age:0\nmx:test.example-")                   // bad mx
	f.Add("version:STSv1\nmode:none\nmax_age:0\next:")                                  // empty extension
	f.Add("version:STSv1\nmode:none\nmax_age:0\next:abc ")                              // trailing space
	f.Add("version:STSv1\nmode:none\nmax_age:0\next:a\t")                               // invalid char
	f.Add("version:STSv1\nmode:none\nmax_age:0\na12345678901234567890123456789012:123") // long extension name
	f.Add("version:STSv1\nmode:none\nmax_age:0\n_bad:test")                             // bad ext name

	f.Fuzz(func(t *testing.T, s string) {
		r, err := ParsePolicy(s)
		if err == nil {
			_ = r.String()
		}
	})
}
