package policy

import (
	"net"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"
)

func Test_normalizeAndValidateCommonName(t *testing.T) {
	tests := []struct {
		name       string
		constraint string
		want       string
		wantErr    bool
	}{
		{
			name:       "fail/empty-constraint",
			constraint: "",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/wildcard",
			constraint: "*",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "ok",
			constraint: "step",
			want:       "step",
			wantErr:    false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeAndValidateCommonName(tt.constraint)
			if (err != nil) != tt.wantErr {
				t.Errorf("normalizeAndValidateCommonName() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("normalizeAndValidateCommonName() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {
	tests := []struct {
		name       string
		constraint string
		want       string
		wantErr    bool
	}{
		{
			name:       "fail/empty-constraint",
			constraint: "",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/wildcard-partial-label",
			constraint: "*xxxx.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/wildcard-in-the-middle",
			constraint: "x.*.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/empty-label",
			constraint: "..local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/empty-reverse",
			constraint: ".",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/no-asterisk",
			constraint: ".example.com",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/idna-internationalized-domain-name-lookup",
			constraint: `\00.local`, // invalid IDNA ASCII character
			want:       "",
			wantErr:    true,
		},
		{
			name:       "ok/wildcard",
			constraint: "*.local",
			want:       ".local",
			wantErr:    false,
		},
		{
			name:       "ok/specific-domain",
			constraint: "example.local",
			want:       "example.local",
			wantErr:    false,
		},
		{
			name:       "ok/idna-internationalized-domain-name-punycode",
			constraint: "*.xn--fsq.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
			want:       ".xn--fsq.jp",
			wantErr:    false,
		},
		{
			name:       "ok/idna-internationalized-domain-name-lookup-transformed",
			constraint: "*.例.jp", // Example value from https://www.w3.org/International/articles/idn-and-iri/
			want:       ".xn--fsq.jp",
			wantErr:    false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeAndValidateDNSDomainConstraint(tt.constraint)
			if (err != nil) != tt.wantErr {
				t.Errorf("normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v", err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("normalizeAndValidateDNSDomainConstraint() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_normalizeAndValidateEmailConstraint(t *testing.T) {
	tests := []struct {
		name       string
		constraint string
		want       string
		wantErr    bool
	}{
		{
			name:       "fail/empty-constraint",
			constraint: "",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/asterisk",
			constraint: "*.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/period",
			constraint: ".local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/@period",
			constraint: "@.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/too-many-@s",
			constraint: "@local@example.com",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/parse-mailbox",
			constraint: "mail@example.com" + string(byte(0)),
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/idna-internationalized-domain",
			constraint: "mail@xn--bla.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/idna-internationalized-domain-name-lookup",
			constraint: `\00local`,
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/parse-domain",
			constraint: "x..example.com",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "ok/wildcard",
			constraint: "@local",
			want:       "local",
			wantErr:    false,
		},
		{
			name:       "ok/specific-mail",
			constraint: "mail@local",
			want:       "mail@local",
			wantErr:    false,
		},
		// TODO(hs): fix the below; doesn't get past parseRFC2821Mailbox; I think it should be allowed.
		// {
		// 	name:       "ok/idna-internationalized-local",
		// 	constraint: `bücher@local`,
		// 	want:       "bücher@local",
		// 	wantErr:    false,
		// },
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeAndValidateEmailConstraint(tt.constraint)
			if (err != nil) != tt.wantErr {
				t.Errorf("normalizeAndValidateEmailConstraint() error = %v, wantErr %v", err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("normalizeAndValidateEmailConstraint() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestNew(t *testing.T) {
	type test struct {
		options []NamePolicyOption
		want    *NamePolicyEngine
		wantErr bool
	}
	var tests = map[string]func(t *testing.T) test{
		"fail/with-permitted-common-name": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedCommonNames("*"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-common-name": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedCommonNames(""),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-dns-domains": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedDNSDomains("**.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-dns-domains": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedDNSDomains("**.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-cidrs": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedCIDRs("127.0.0.1//24"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-cidrs": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedCIDRs("127.0.0.1//24"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedIPsOrCIDRs("127.0.0.1//24"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-ipsOrCIDRs-ip": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedIPsOrCIDRs("127.0.0:1"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedIPsOrCIDRs("127.0.0.1//24"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-ipsOrCIDRs-ip": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedIPsOrCIDRs("127.0.0:1"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-emails": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedEmailAddresses("*.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-emails": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedEmailAddresses("*.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-permitted-uris": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithPermittedURIDomains("**.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"fail/with-excluded-uris": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{
					WithExcludedURIDomains("**.local"),
				},
				want:    nil,
				wantErr: true,
			}
		},
		"ok/default": func(t *testing.T) test {
			return test{
				options: []NamePolicyOption{},
				want:    &NamePolicyEngine{},
				wantErr: false,
			}
		},
		"ok/subject-verification": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithSubjectCommonNameVerification(),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					verifySubjectCommonName: true,
				},
				wantErr: false,
			}
		},
		"ok/literal-wildcards": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithAllowLiteralWildcardNames(),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					allowLiteralWildcardNames: true,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-dns-wildcard-domains": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithPermittedDNSDomains("*.local", "*.example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedDNSDomains:               []string{".local", ".example.com"},
					numberOfDNSDomainConstraints:      2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-dns-domains": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithExcludedDNSDomains("*.local", "*.example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedDNSDomains:               []string{".local", ".example.com"},
					numberOfDNSDomainConstraints:     2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-ip-ranges": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.1/24")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithPermittedIPRanges(nw1, nw2),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedIPRanges: []*net.IPNet{
						nw1, nw2,
					},
					numberOfIPRangeConstraints:        2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-ip-ranges": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.1/24")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithExcludedIPRanges(nw1, nw2),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedIPRanges: []*net.IPNet{
						nw1, nw2,
					},
					numberOfIPRangeConstraints:       2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-cidrs": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.1/24")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithPermittedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedIPRanges: []*net.IPNet{
						nw1, nw2,
					},
					numberOfIPRangeConstraints:        2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-cidrs": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.1/24")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithExcludedCIDRs("127.0.0.1/24", "192.168.0.1/24"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedIPRanges: []*net.IPNet{
						nw1, nw2,
					},
					numberOfIPRangeConstraints:       2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-ipsOrCIDRs-cidr": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.31/32")
			assert.NoError(t, err)
			_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithPermittedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedIPRanges: []*net.IPNet{
						nw1, nw2, nw3,
					},
					numberOfIPRangeConstraints:        3,
					totalNumberOfPermittedConstraints: 3,
					totalNumberOfConstraints:          3,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-ipsOrCIDRs-cidr": func(t *testing.T) test {
			_, nw1, err := net.ParseCIDR("127.0.0.1/24")
			assert.NoError(t, err)
			_, nw2, err := net.ParseCIDR("192.168.0.31/32")
			assert.NoError(t, err)
			_, nw3, err := net.ParseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/128")
			assert.NoError(t, err)
			options := []NamePolicyOption{
				WithExcludedIPsOrCIDRs("127.0.0.1/24", "192.168.0.31", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedIPRanges: []*net.IPNet{
						nw1, nw2, nw3,
					},
					numberOfIPRangeConstraints:       3,
					totalNumberOfExcludedConstraints: 3,
					totalNumberOfConstraints:         3,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-emails": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithPermittedEmailAddresses("mail@local", "@example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedEmailAddresses:           []string{"mail@local", "example.com"},
					numberOfEmailAddressConstraints:   2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-emails": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithExcludedEmailAddresses("mail@local", "@example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedEmailAddresses:           []string{"mail@local", "example.com"},
					numberOfEmailAddressConstraints:  2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-uris": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithPermittedURIDomains("host.local", "*.example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedURIDomains:               []string{"host.local", ".example.com"},
					numberOfURIDomainConstraints:      2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-uris": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithExcludedURIDomains("host.local", "*.example.com"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedURIDomains:               []string{"host.local", ".example.com"},
					numberOfURIDomainConstraints:     2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
		"ok/with-permitted-principals": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithPermittedPrincipals("root", "ops"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					permittedPrincipals:               []string{"root", "ops"},
					numberOfPrincipalConstraints:      2,
					totalNumberOfPermittedConstraints: 2,
					totalNumberOfConstraints:          2,
				},
				wantErr: false,
			}
		},
		"ok/with-excluded-principals": func(t *testing.T) test {
			options := []NamePolicyOption{
				WithExcludedPrincipals("root", "ops"),
			}
			return test{
				options: options,
				want: &NamePolicyEngine{
					excludedPrincipals:               []string{"root", "ops"},
					numberOfPrincipalConstraints:     2,
					totalNumberOfExcludedConstraints: 2,
					totalNumberOfConstraints:         2,
				},
				wantErr: false,
			}
		},
	}
	for name, prep := range tests {
		tc := prep(t)
		t.Run(name, func(t *testing.T) {
			got, err := New(tc.options...)
			if (err != nil) != tc.wantErr {
				t.Errorf("New() error = %v, wantErr %v", err, tc.wantErr)
				return
			}
			if !cmp.Equal(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})) {
				t.Errorf("New() diff =\n %s", cmp.Diff(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})))
			}
		})
	}
}

func Test_normalizeAndValidateURIDomainConstraint(t *testing.T) {
	tests := []struct {
		name       string
		constraint string
		want       string
		wantErr    bool
	}{
		{
			name:       "fail/empty-constraint",
			constraint: "",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/scheme-https",
			constraint: `https://*.local`,
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/too-many-asterisks",
			constraint: "**.local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/empty-label",
			constraint: "..local",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/empty-reverse",
			constraint: ".",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/domain-with-port",
			constraint: "host.local:8443",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/no-asterisk",
			constraint: ".example.com",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/ipv4",
			constraint: "127.0.0.1",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/ipv6-brackets",
			constraint: "[::1]",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/ipv6-no-brackets",
			constraint: "::1",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/ipv6-no-brackets",
			constraint: "[::1",
			want:       "",
			wantErr:    true,
		},
		{
			name:       "fail/idna-internationalized-domain-name-lookup",
			constraint: `\00local`,
			want:       "",
			wantErr:    true,
		},
		{
			name:       "ok/wildcard",
			constraint: "*.local",
			want:       ".local",
			wantErr:    false,
		},
		{
			name:       "ok/specific-domain",
			constraint: "example.local",
			want:       "example.local",
			wantErr:    false,
		},
		{
			name:       "ok/idna-internationalized-domain-name-lookup",
			constraint: `*.bücher.example.com`,
			want:       ".xn--bcher-kva.example.com",
			wantErr:    false,
		},
		{
			// IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results
			// in a difference between Go 1.18 and lower versions. Go 1.18 expects ".xn--fa-hia.de"; not .fass.de.
			name:       "ok/idna-internationalized-domain-name-lookup-deviation",
			constraint: `*.faß.de`,
			want:       ".xn--fa-hia.de",
			wantErr:    false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := normalizeAndValidateURIDomainConstraint(tt.constraint)
			if (err != nil) != tt.wantErr {
				t.Errorf("normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v", err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("normalizeAndValidateURIDomainConstraint() = %v, want %v", got, tt.want)
			}
		})
	}
}