/*
Copyright 2019 The Knative Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
	"context"
	"testing"

	"github.com/google/go-cmp/cmp"
	corev1 "k8s.io/api/core/v1"
	"knative.dev/pkg/apis"
)

const (
	kind       = "SomeKind"
	apiVersion = "v1mega1"
	name       = "a-name"
)

var (
	testCert = `-----BEGIN CERTIFICATE-----
MIIDmjCCAoKgAwIBAgIUYzA4bTMXevuk3pl2Mn8hpCYL2C0wDQYJKoZIhvcNAQEL
BQAwLzELMAkGA1UEBhMCVVMxIDAeBgNVBAMMF0tuYXRpdmUtRXhhbXBsZS1Sb290
LUNBMB4XDTIzMDQwNTEzMTUyNFoXDTI2MDEyMzEzMTUyNFowbTELMAkGA1UEBhMC
VVMxEjAQBgNVBAgMCVlvdXJTdGF0ZTERMA8GA1UEBwwIWW91ckNpdHkxHTAbBgNV
BAoMFEV4YW1wbGUtQ2VydGlmaWNhdGVzMRgwFgYDVQQDDA9sb2NhbGhvc3QubG9j
YWwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5teo+En6U5nhqn7Sc
uanqswUmPlgs9j/8l21Rhb4T+ezlYKGQGhbJyFFMuiCE1Rjn8bpCwi7Nnv12Y2nz
FhEv2Jx0yL3Tqx0Q593myqKDq7326EtbO7wmDT0XD03twH5i9XZ0L0ihPWn1mjUy
WxhnHhoFpXrsnQECJorZY6aTrFbGVYelIaj5AriwiqyL0fET8pueI2GwLjgWHFSH
X8XsGAlcLUhkQG0Z+VO9usy4M1Wpt+cL6cnTiQ+sRmZ6uvaj8fKOT1Slk/oUeAi4
WqFkChGzGzLik0QrhKGTdw3uUvI1F2sdQj0GYzXaWqRz+tP9qnXdzk1GrszKKSlm
WBTLAgMBAAGjcDBuMB8GA1UdIwQYMBaAFJJcCftus4vj98N0zQQautsjEu82MAkG
A1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAdBgNV
HQ4EFgQUnu/3vqA3VEzm128x/hLyZzR9JlgwDQYJKoZIhvcNAQELBQADggEBAFc+
1cKt/CNjHXUsirgEhry2Mm96R6Yxuq//mP2+SEjdab+FaXPZkjHx118u3PPX5uTh
gTT7rMfka6J5xzzQNqJbRMgNpdEFH1bbc11aYuhi0khOAe0cpQDtktyuDJQMMv3/
3wu6rLr6fmENo0gdcyUY9EiYrglWGtdXhlo4ySRY8UZkUScG2upvyOhHTxVCAjhP
efbMkNjmDuZOMK+wqanqr5YV6zMPzkQK7DspfRgasMAQmugQu7r2MZpXg8Ilhro1
s/wImGnMVk5RzpBVrq2VB9SkX/ThTVYEC/Sd9BQM364MCR+TA1l8/ptaLFLuwyw8
O2dgzikq8iSy1BlRsVw=
-----END CERTIFICATE-----`

	csr = `-----BEGIN CERTIFICATE REQUEST-----
MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFV0YWgxDzANBgNV
BAcMBkxpbmRvbjEWMBQGA1UECgwNRGlnaUNlcnQgSW5jLjERMA8GA1UECwwIRGln
aUNlcnQxHTAbBgNVBAMMFGV4YW1wbGUuZGlnaWNlcnQuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8+To7d+2kPWeBv/orU3LVbJwDrSQbeKamCmo
wp5bqDxIwV20zqRb7APUOKYoVEFFOEQs6T6gImnIolhbiH6m4zgZ/CPvWBOkZc+c
1Po2EmvBz+AD5sBdT5kzGQA6NbWyZGldxRthNLOs1efOhdnWFuhI162qmcflgpiI
WDuwq4C9f+YkeJhNn9dF5+owm8cOQmDrV8NNdiTqin8q3qYAHHJRW28glJUCZkTZ
wIaSR6crBQ8TbYNE0dc+Caa3DOIkz1EOsHWzTx+n0zKfqcbgXi4DJx+C1bjptYPR
BPZL8DAeWuA8ebudVT44yEp82G96/Ggcf7F33xMxe0yc+Xa6owIDAQABoAAwDQYJ
KoZIhvcNAQEFBQADggEBAB0kcrFccSmFDmxox0Ne01UIqSsDqHgL+XmHTXJwre6D
hJSZwbvEtOK0G3+dr4Fs11WuUNt5qcLsx5a8uk4G6AKHMzuhLsJ7XZjgmQXGECpY
Q4mC3yT3ZoCGpIXbw+iP3lmEEXgaQL0Tx5LFl/okKbKYwIqNiyKWOMj7ZR/wxWg/
ZDGRs55xuoeLDJ/ZRFf9bI+IaCUd1YrfYcHIl3G87Av+r49YVwqRDT0VDV7uLgqn
29XI1PpVUNCPQGn9p/eX6Qo7vpDaPybRtA2R7XLKjQaF9oXWeCUqy1hvJac9QFO2
97Ob1alpHPoZ7mWiEuJwjBPii6a9M9G30nUo39lBi1w=
-----END CERTIFICATE REQUEST-----`

	invaidCert = "certificate"
)

func TestValidateDestination(t *testing.T) {
	ctx := context.Background()

	validRef := corev1.ObjectReference{
		Kind:       kind,
		APIVersion: apiVersion,
		Name:       name,
	}

	validURL := apis.URL{
		Scheme: "http",
		Host:   "host",
	}

	tests := map[string]struct {
		dest *Destination
		want string
	}{
		"nil valid": {
			dest: nil,
			want: "",
		},
		"valid ref": {
			dest: &Destination{
				Ref: &validRef,
			},
			want: "",
		},
		"invalid ref, missing name": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind:       kind,
					APIVersion: apiVersion,
				},
			},
			want: "missing field(s): ref.name",
		},
		"invalid ref, missing api version": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind: kind,
					Name: apiVersion,
				},
			},
			want: "missing field(s): ref.apiVersion",
		},
		"invalid ref, missing kind": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					APIVersion: apiVersion,
					Name:       name,
				},
			},
			want: "missing field(s): ref.kind",
		},
		"valid [apiVersion, kind, name]": {
			dest: &Destination{
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "",
		},
		"invalid [apiVersion, kind, name], missing name": {
			dest: &Destination{
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
			},
			want: "missing field(s): name",
		},
		"invalid [apiVersion, kind, name], missing api version": {
			dest: &Destination{
				DeprecatedKind: kind,
				DeprecatedName: name,
			},
			want: "missing field(s): apiVersion",
		},
		"invalid [apiVersion, kind, name], missing kind": {
			dest: &Destination{
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "missing field(s): kind",
		},
		"valid uri": {
			dest: &Destination{
				URI: &validURL,
			},
		},
		"invalid, uri has no host": {
			dest: &Destination{
				URI: &apis.URL{
					Scheme: "http",
				},
			},
			want: "invalid value: Relative URI is not allowed when Ref and [apiVersion, kind, name] is absent: uri",
		},
		"invalid, uri is not absolute url": {
			dest: &Destination{
				URI: &apis.URL{
					Host: "host",
				},
			},
			want: "invalid value: Relative URI is not allowed when Ref and [apiVersion, kind, name] is absent: uri",
		},
		"invalid, both ref and [apiVersion, kind, name] are present ": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind:       "SomeKind",
					APIVersion: "v1mega1",
					Name:       "a-name",
				},
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "Ref and [apiVersion, kind, name] can't be both present: [apiVersion, kind, name], ref",
		},
		"invalid, both uri and ref, uri is absolute URL": {
			dest: &Destination{
				URI: &validURL,
				Ref: &validRef,
			},
			want: "Absolute URI is not allowed when Ref or [apiVersion, kind, name] is present: [apiVersion, kind, name], ref, uri",
		},
		"invalid, both uri and [apiVersion, kind, name], uri is absolute URL": {
			dest: &Destination{
				URI:                  &validURL,
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "Absolute URI is not allowed when Ref or [apiVersion, kind, name] is present: [apiVersion, kind, name], ref, uri",
		},
		"invalid, both ref, [apiVersion, kind, name] and uri  are nil": {
			dest: &Destination{},
			want: "expected at least one, got none: [apiVersion, kind, name], ref, uri",
		},
		"valid, both uri and ref, uri is not a absolute URL": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				Ref: &validRef,
			},
		},
		"valid, both uri and [apiVersion, kind, name], uri is not a absolute URL": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
		},
		"valid, CACert is valid": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				Ref:     &validRef,
				CACerts: &testCert,
			},
		},
		"invalid,CACert is invalid": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				Ref:     &validRef,
				CACerts: &invaidCert,
			},
			want: "invalid value: CA Cert provided is invalid: caCert",
		},
		"invalid,CSR provided not CA Cert": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				Ref:     &validRef,
				CACerts: &csr,
			},
			want: "invalid value: CA Cert provided is not a certificate: caCert",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			gotErr := tc.dest.Validate(ctx)

			if tc.want != "" {
				if got, want := gotErr.Error(), tc.want; got != want {
					t.Errorf("%s: Error() = %v, wanted %v", name, got, want)
				}
			} else if gotErr != nil {
				t.Errorf("%s: Validate() = %v, wanted nil", name, gotErr)
			}
		})
	}
}

func TestValidateDestinationDisallowDeprecated(t *testing.T) {
	ctx := context.Background()

	validRef := corev1.ObjectReference{
		Kind:       kind,
		APIVersion: apiVersion,
		Name:       name,
	}

	validURL := apis.URL{
		Scheme: "http",
		Host:   "host",
	}

	tests := map[string]struct {
		dest *Destination
		want string
	}{
		"nil valid": {
			dest: nil,
			want: "",
		},
		"valid ref": {
			dest: &Destination{
				Ref: &validRef,
			},
			want: "",
		},
		"invalid ref, missing name": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind:       kind,
					APIVersion: apiVersion,
				},
			},
			want: "missing field(s): ref.name",
		},
		"invalid ref, missing api version": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind: kind,
					Name: apiVersion,
				},
			},
			want: "missing field(s): ref.apiVersion",
		},
		"invalid ref, missing kind": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					APIVersion: apiVersion,
					Name:       name,
				},
			},
			want: "missing field(s): ref.kind",
		},
		"invalid deprecated [apiVersion, kind, name]": {
			dest: &Destination{
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: kind is not allowed here, it's a deprecated value: kind\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
		"invalid deprecated [apiVersion, kind]": {
			dest: &Destination{
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: kind is not allowed here, it's a deprecated value: kind",
		},
		"invalid deprecated [kind, name]": {
			dest: &Destination{
				DeprecatedKind: kind,
				DeprecatedName: name,
			},
			want: "invalid value: kind is not allowed here, it's a deprecated value: kind\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
		"invalid deprecated [apiVersion, name]": {
			dest: &Destination{
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
		"valid uri": {
			dest: &Destination{
				URI: &validURL,
			},
		},
		"invalid, uri has no host": {
			dest: &Destination{
				URI: &apis.URL{
					Scheme: "http",
				},
			},
			want: "invalid value: Relative URI is not allowed when Ref and [apiVersion, kind, name] is absent: uri",
		},
		"invalid, uri is not absolute url": {
			dest: &Destination{
				URI: &apis.URL{
					Host: "host",
				},
			},
			want: "invalid value: Relative URI is not allowed when Ref and [apiVersion, kind, name] is absent: uri",
		},
		"invalid deprecated, both ref and [apiVersion, kind, name] are present ": {
			dest: &Destination{
				Ref: &corev1.ObjectReference{
					Kind:       "SomeKind",
					APIVersion: "v1mega1",
					Name:       "a-name",
				},
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: kind is not allowed here, it's a deprecated value: kind\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
		"invalid, both uri and ref, uri is absolute URL": {
			dest: &Destination{
				URI: &validURL,
				Ref: &validRef,
			},
			want: "Absolute URI is not allowed when Ref or [apiVersion, kind, name] is present: [apiVersion, kind, name], ref, uri",
		},
		"invalid, both uri and [apiVersion, kind, name], uri is absolute URL": {
			dest: &Destination{
				URI:                  &validURL,
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: kind is not allowed here, it's a deprecated value: kind\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
		"invalid, both ref, [apiVersion, kind, name] and uri  are nil": {
			dest: &Destination{},
			want: "expected at least one, got none: [apiVersion, kind, name], ref, uri",
		},
		"valid, both uri and ref, uri is not a absolute URL": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				Ref: &validRef,
			},
		},
		"invalid deprecated, both uri and [apiVersion, kind, name], uri is not a absolute URL": {
			dest: &Destination{
				URI: &apis.URL{
					Path: "/handler",
				},
				DeprecatedKind:       kind,
				DeprecatedAPIVersion: apiVersion,
				DeprecatedName:       name,
			},
			want: "invalid value: apiVersion is not allowed here, it's a deprecated value: apiVersion\ninvalid value: kind is not allowed here, it's a deprecated value: kind\ninvalid value: name is not allowed here, it's a deprecated value: name",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			gotErr := tc.dest.ValidateDisallowDeprecated(ctx)

			if tc.want != "" {
				if got, want := gotErr.Error(), tc.want; got != want {
					t.Errorf("%s: Error() = %v, wanted %v", name, got, want)
				}
			} else if gotErr != nil {
				t.Errorf("%s: Validate() = %v, wanted nil", name, gotErr)
			}
		})
	}
}

func TestDestination_GetRef(t *testing.T) {
	ref := &corev1.ObjectReference{
		APIVersion: apiVersion,
		Kind:       kind,
		Name:       name,
	}
	tests := map[string]struct {
		dest *Destination
		want *corev1.ObjectReference
	}{
		"nil destination": {
			dest: nil,
			want: nil,
		},
		"uri": {
			dest: &Destination{
				URI: &apis.URL{
					Host: "foo",
				},
			},
			want: nil,
		},
		"ref": {
			dest: &Destination{
				Ref: ref,
			},
			want: ref,
		},
		"deprecated ref": {
			dest: &Destination{
				DeprecatedAPIVersion: ref.APIVersion,
				DeprecatedKind:       ref.Kind,
				DeprecatedName:       ref.Name,
			},
			want: ref,
		},
	}

	for n, tc := range tests {
		t.Run(n, func(t *testing.T) {
			got := tc.dest.GetRef()
			if diff := cmp.Diff(tc.want, got); diff != "" {
				t.Error("Unexpected result (-want +got):", diff)
			}
		})
	}
}
