package v1

import (
	"bytes"
	"encoding/json"
	"errors"
	"reflect"
	"regexp"
	"testing"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var parseTimeTests = []struct {
	about string
	in    string
	out   metav1.Time
	err   error
}{
	{"parse common time with minutes", "16:08", mustParseTime("16:08"), nil},
	{"parse time with zeroed minutes", "11:00", mustParseTime("11:00"), nil},
	{"parse corner case last minute of the day", "23:59", mustParseTime("23:59"), nil},

	{"expect error as hour is out of range", "26:09", metav1.Now(), errors.New(`parsing time "26:09": hour out of range`)},
	{"expect error as minute is out of range", "23:69", metav1.Now(), errors.New(`parsing time "23:69": minute out of range`)},
}

func stringToPointer(str string) *string {
	return &str
}

var parseWeekdayTests = []struct {
	about string
	in    string
	out   time.Weekday
	err   error
}{
	{"parse common weekday", "Wed", time.Wednesday, nil},
	{"expect error as weekday is invalid", "Sunday", time.Weekday(0), errors.New("incorrect weekday")},
	{"expect error as weekday is empty", "", time.Weekday(0), errors.New("incorrect weekday")},
}

var clusterNames = []struct {
	about       string
	in          string
	inTeam      string
	clusterName string
	err         error
}{
	{"common team and cluster name", "acid-test", "acid", "test", nil},
	{"cluster name with hyphen", "test-my-name", "test", "my-name", nil},
	{"cluster and team name with hyphen", "my-team-another-test", "my-team", "another-test", nil},
	{"expect error as cluster name is just hyphens", "------strange-team-cluster", "-----", "strange-team-cluster",
		errors.New(`name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)},
	{"expect error as cluster name is too long", "fooobar-fooobarfooobarfooobarfooobarfooobarfooobarfooobarfooobar", "fooobar", "",
		errors.New("name cannot be longer than 58 characters")},
	{"expect error as cluster name does not match {TEAM}-{NAME} format", "acid-test", "test", "", errors.New("name must match {TEAM}-{NAME} format")},
	{"expect error as team and cluster name are empty", "-test", "", "", errors.New("team name is empty")},
	{"expect error as cluster name is empty and team name is a hyphen", "-test", "-", "", errors.New("name must match {TEAM}-{NAME} format")},
	{"expect error as cluster name is empty, team name is a hyphen and cluster name is empty", "", "-", "", errors.New("cluster name must match {TEAM}-{NAME} format. Got cluster name '', team name '-'")},
	{"expect error as cluster and team name are hyphens", "-", "-", "", errors.New("cluster name must match {TEAM}-{NAME} format. Got cluster name '-', team name '-'")},
	// user may specify the team part of the full cluster name differently from the team name returned by the Teams API
	// in the case the actual Teams API name is long enough, this will fail the check
	{"expect error as team name does not match", "foo-bar", "qwerty", "", errors.New("cluster name must match {TEAM}-{NAME} format. Got cluster name 'foo-bar', team name 'qwerty'")},
}

var cloneClusterDescriptions = []struct {
	about string
	in    *CloneDescription
	err   error
}{
	{"cluster name invalid but EndTimeSet is not empty", &CloneDescription{"foo+bar", "", "NotEmpty", "", "", "", "", nil}, nil},
	{"expect error as cluster name does not match DNS-1035", &CloneDescription{"foo+bar", "", "", "", "", "", "", nil},
		errors.New(`clone cluster name must confirm to DNS-1035, regex used for validation is "^[a-z]([-a-z0-9]*[a-z0-9])?$"`)},
	{"expect error as cluster name is too long", &CloneDescription{"foobar123456789012345678901234567890123456789012345678901234567890", "", "", "", "", "", "", nil},
		errors.New("clone cluster name must be no longer than 63 characters")},
	{"common cluster name", &CloneDescription{"foobar", "", "", "", "", "", "", nil}, nil},
}

var maintenanceWindows = []struct {
	about string
	in    []byte
	out   MaintenanceWindow
	err   error
}{{"regular scenario",
	[]byte(`"Tue:10:00-20:00"`),
	MaintenanceWindow{
		Everyday:  false,
		Weekday:   time.Tuesday,
		StartTime: mustParseTime("10:00"),
		EndTime:   mustParseTime("20:00"),
	}, nil},
	{"regular every day scenario",
		[]byte(`"05:00-07:00"`),
		MaintenanceWindow{
			Everyday:  true,
			StartTime: mustParseTime("05:00"),
			EndTime:   mustParseTime("07:00"),
		}, nil},
	{"starts and ends at the same time",
		[]byte(`"Mon:10:00-10:00"`),
		MaintenanceWindow{
			Everyday:  false,
			Weekday:   time.Monday,
			StartTime: mustParseTime("10:00"),
			EndTime:   mustParseTime("10:00"),
		}, nil},
	{"starts and ends 00:00 on sunday",
		[]byte(`"Sun:00:00-00:00"`),
		MaintenanceWindow{
			Everyday:  false,
			Weekday:   time.Sunday,
			StartTime: mustParseTime("00:00"),
			EndTime:   mustParseTime("00:00"),
		}, nil},
	{"without day indication should define to sunday",
		[]byte(`"01:00-10:00"`),
		MaintenanceWindow{
			Everyday:  true,
			Weekday:   time.Sunday,
			StartTime: mustParseTime("01:00"),
			EndTime:   mustParseTime("10:00"),
		}, nil},
	{"expect error as 'From' is later than 'To'", []byte(`"Mon:12:00-11:00"`), MaintenanceWindow{}, errors.New(`'From' time must be prior to the 'To' time`)},
	{"expect error as 'From' is later than 'To' with 00:00 corner case", []byte(`"Mon:10:00-00:00"`), MaintenanceWindow{}, errors.New(`'From' time must be prior to the 'To' time`)},
	{"expect error as 'From' time is not valid", []byte(`"Wed:33:00-00:00"`), MaintenanceWindow{}, errors.New(`could not parse start time: parsing time "33:00": hour out of range`)},
	{"expect error as 'To' time is not valid", []byte(`"Wed:00:00-26:00"`), MaintenanceWindow{}, errors.New(`could not parse end time: parsing time "26:00": hour out of range`)},
	{"expect error as weekday is not valid", []byte(`"Sunday:00:00-00:00"`), MaintenanceWindow{}, errors.New(`could not parse weekday: incorrect weekday`)},
	{"expect error as weekday is empty", []byte(`":00:00-10:00"`), MaintenanceWindow{}, errors.New(`could not parse weekday: incorrect weekday`)},
	{"expect error as maintenance window set seconds", []byte(`"Mon:00:00:00-10:00:00"`), MaintenanceWindow{}, errors.New(`incorrect maintenance window format`)},
	{"expect error as 'To' time set seconds", []byte(`"Mon:00:00-00:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")},
	// ideally, should be implemented
	{"expect error as 'To' has a weekday", []byte(`"Mon:00:00-Fri:00:00"`), MaintenanceWindow{}, errors.New("could not parse end time: incorrect time format")},
	{"expect error as 'To' time is missing", []byte(`"Mon:00:00"`), MaintenanceWindow{}, errors.New("incorrect maintenance window format")}}

var postgresStatus = []struct {
	about string
	in    []byte
	out   PostgresStatus
	err   error
}{
	{"cluster running", []byte(`{"PostgresClusterStatus":"Running"}`),
		PostgresStatus{PostgresClusterStatus: ClusterStatusRunning}, nil},
	{"cluster status undefined", []byte(`{"PostgresClusterStatus":""}`),
		PostgresStatus{PostgresClusterStatus: ClusterStatusUnknown}, nil},
	{"cluster running without full JSON format", []byte(`"Running"`),
		PostgresStatus{PostgresClusterStatus: ClusterStatusRunning}, nil},
	{"cluster status empty", []byte(`""`),
		PostgresStatus{PostgresClusterStatus: ClusterStatusUnknown}, nil}}

var tmp postgresqlCopy
var unmarshalCluster = []struct {
	about   string
	in      []byte
	out     Postgresql
	marshal []byte
	err     error
}{
	{
		about: "example with simple status field",
		in: []byte(`{
	  "kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
	  "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`),
		out: Postgresql{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Postgresql",
				APIVersion: "acid.zalan.do/v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: "acid-testcluster1",
			},
			Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
			// This error message can vary between Go versions, so compute it for the current version.
			Error: json.Unmarshal([]byte(`{
				"kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
				"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
		},
		marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`),
		err:     nil,
	},
	{
		about: "example with /status subresource",
		in: []byte(`{
	  "kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
	  "metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`),
		out: Postgresql{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Postgresql",
				APIVersion: "acid.zalan.do/v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: "acid-testcluster1",
			},
			Status: PostgresStatus{PostgresClusterStatus: ClusterStatusInvalid},
			// This error message can vary between Go versions, so compute it for the current version.
			Error: json.Unmarshal([]byte(`{
				"kind": "Postgresql","apiVersion": "acid.zalan.do/v1",
				"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
		},
		marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
		err:     nil,
	},
	{
		about: "example with clone",
		in:    []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`),
		out: Postgresql{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Postgresql",
				APIVersion: "acid.zalan.do/v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: "acid-testcluster1",
			},
			Spec: PostgresSpec{
				TeamID: "acid",
				Clone: &CloneDescription{
					ClusterName: "team-batman",
				},
			},
			Error: "",
		},
		marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`),
		err:     nil,
	},
	{
		about: "standby example",
		in:    []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "standby": {"s3_wal_path": "s3://custom/path/to/bucket/"}}}`),
		out: Postgresql{
			TypeMeta: metav1.TypeMeta{
				Kind:       "Postgresql",
				APIVersion: "acid.zalan.do/v1",
			},
			ObjectMeta: metav1.ObjectMeta{
				Name: "acid-testcluster1",
			},
			Spec: PostgresSpec{
				TeamID: "acid",
				StandbyCluster: &StandbyDescription{
					S3WalPath: "s3://custom/path/to/bucket/",
				},
			},
			Error: "",
		},
		marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"standby":{"s3_wal_path":"s3://custom/path/to/bucket/"}},"status":{"PostgresClusterStatus":""}}`),
		err:     nil,
	},
	{
		about:   "expect error on malformatted JSON",
		in:      []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`),
		out:     Postgresql{},
		marshal: []byte{},
		err:     errors.New("unexpected end of JSON input")},
	{
		about:   "expect error on JSON with field's value malformatted",
		in:      []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster","creationTimestamp":qaz},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"resources":{"requests":{"cpu":"","memory":""},"limits":{"cpu":"","memory":""}},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
		out:     Postgresql{},
		marshal: []byte{},
		err:     errors.New("invalid character 'q' looking for beginning of value"),
	},
}

var postgresqlList = []struct {
	about string
	in    []byte
	out   PostgresqlList
	err   error
}{
	{"expect success", []byte(`{"apiVersion":"v1","items":[{"apiVersion":"acid.zalan.do/v1","kind":"Postgresql","metadata":{"labels":{"team":"acid"},"name":"acid-testcluster42","namespace":"default","resourceVersion":"30446957","selfLink":"/apis/acid.zalan.do/v1/namespaces/default/postgresqls/acid-testcluster42","uid":"857cd208-33dc-11e7-b20a-0699041e4b03"},"spec":{"allowedSourceRanges":["185.85.220.0/22"],"numberOfInstances":1,"postgresql":{"version":"18"},"teamId":"acid","volume":{"size":"10Gi"}},"status":{"PostgresClusterStatus":"Running"}}],"kind":"List","metadata":{},"resourceVersion":"","selfLink":""}`),
		PostgresqlList{
			TypeMeta: metav1.TypeMeta{
				Kind:       "List",
				APIVersion: "v1",
			},
			Items: []Postgresql{{
				TypeMeta: metav1.TypeMeta{
					Kind:       "Postgresql",
					APIVersion: "acid.zalan.do/v1",
				},
				ObjectMeta: metav1.ObjectMeta{
					Name:            "acid-testcluster42",
					Namespace:       "default",
					Labels:          map[string]string{"team": "acid"},
					ResourceVersion: "30446957",
					SelfLink:        "/apis/acid.zalan.do/v1/namespaces/default/postgresqls/acid-testcluster42",
					UID:             "857cd208-33dc-11e7-b20a-0699041e4b03",
				},
				Spec: PostgresSpec{
					ClusterName:         "testcluster42",
					PostgresqlParam:     PostgresqlParam{PgVersion: "18"},
					Volume:              Volume{Size: "10Gi"},
					TeamID:              "acid",
					AllowedSourceRanges: []string{"185.85.220.0/22"},
					NumberOfInstances:   1,
				},
				Status: PostgresStatus{
					PostgresClusterStatus: ClusterStatusRunning,
				},
				Error: "",
			}},
		},
		nil},
	{"expect error on malformatted JSON", []byte(`{"apiVersion":"v1","items":[{"apiVersion":"acid.zalan.do/v1","kind":"Postgresql","metadata":{"labels":{"team":"acid"},"name":"acid-testcluster42","namespace"`),
		PostgresqlList{},
		errors.New("unexpected end of JSON input")}}

var podAnnotations = []struct {
	about       string
	in          []byte
	annotations map[string]string
	err         error
}{{
	about: "common annotations",
	in: []byte(`{
		"kind": "Postgresql",
		"apiVersion": "acid.zalan.do/v1",
		"metadata": {
			"name": "acid-testcluster1"
		},
		"spec": {
			"podAnnotations": {
				"foo": "bar"
			},
			"teamId": "acid",
			"clone": {
				"cluster": "team-batman"
			}
		}
	}`),
	annotations: map[string]string{"foo": "bar"},
	err:         nil},
}

var serviceAnnotations = []struct {
	about       string
	in          []byte
	annotations map[string]string
	err         error
}{
	{
		about: "common single annotation",
		in: []byte(`{
			"kind": "Postgresql",
			"apiVersion": "acid.zalan.do/v1",
			"metadata": {
				"name": "acid-testcluster1"
			},
			"spec": {
				"serviceAnnotations": {
					"foo": "bar"
				},
				"teamId": "acid",
				"clone": {
					"cluster": "team-batman"
				}
			}
		}`),
		annotations: map[string]string{"foo": "bar"},
		err:         nil,
	},
	{
		about: "common two annotations",
		in: []byte(`{
			"kind": "Postgresql",
			"apiVersion": "acid.zalan.do/v1",
			"metadata": {
				"name": "acid-testcluster1"
			},
			"spec": {
				"serviceAnnotations": {
					"foo": "bar",
					"post": "gres"
				},
				"teamId": "acid",
				"clone": {
					"cluster": "team-batman"
				}
			}
		}`),
		annotations: map[string]string{"foo": "bar", "post": "gres"},
		err:         nil,
	},
}

func mustParseTime(s string) metav1.Time {
	v, err := time.Parse("15:04", s)
	if err != nil {
		panic(err)
	}

	return metav1.Time{Time: v.UTC()}
}

func TestParseTime(t *testing.T) {
	for _, tt := range parseTimeTests {
		t.Run(tt.about, func(t *testing.T) {
			aTime, err := parseTime(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("ParseTime expected error: %v, got: %v", tt.err, err)
				}
				return
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}

			if aTime != tt.out {
				t.Errorf("Expected time: %v, got: %v", tt.out, aTime)
			}
		})
	}
}

func TestWeekdayTime(t *testing.T) {
	for _, tt := range parseWeekdayTests {
		t.Run(tt.about, func(t *testing.T) {
			aTime, err := parseWeekday(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("ParseWeekday expected error: %v, got: %v", tt.err, err)
				}
				return
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}

			if aTime != tt.out {
				t.Errorf("Expected weekday: %v, got: %v", tt.out, aTime)
			}
		})
	}
}

func TestPodAnnotations(t *testing.T) {
	for _, tt := range podAnnotations {
		t.Run(tt.about, func(t *testing.T) {
			var cluster Postgresql
			err := cluster.UnmarshalJSON(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("Unable to marshal cluster with podAnnotations: expected %v got %v", tt.err, err)
				}
				return
			}
			for k, v := range cluster.Spec.PodAnnotations {
				found, expected := v, tt.annotations[k]
				if found != expected {
					t.Errorf("Didn't find correct value for key %v in for podAnnotations: Expected %v found %v", k, expected, found)
				}
			}
		})
	}
}

func TestServiceAnnotations(t *testing.T) {
	for _, tt := range serviceAnnotations {
		t.Run(tt.about, func(t *testing.T) {
			var cluster Postgresql
			err := cluster.UnmarshalJSON(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("Unable to marshal cluster with serviceAnnotations: expected %v got %v", tt.err, err)
				}
				return
			}
			for k, v := range cluster.Spec.ServiceAnnotations {
				found, expected := v, tt.annotations[k]
				if found != expected {
					t.Errorf("Didn't find correct value for key %v in for serviceAnnotations: Expected %v found %v", k, expected, found)
				}
			}
		})
	}
}

func TestClusterName(t *testing.T) {
	for _, tt := range clusterNames {
		t.Run(tt.about, func(t *testing.T) {
			name, err := ExtractClusterName(tt.in, tt.inTeam)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("ExtractClusterName expected error: %v, got: %v", tt.err, err)
				}
				return
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}
			if name != tt.clusterName {
				t.Errorf("Expected cluserName: %q, got: %q", tt.clusterName, name)
			}
		})
	}
}

func TestCloneClusterDescription(t *testing.T) {
	for _, tt := range cloneClusterDescriptions {
		t.Run(tt.about, func(t *testing.T) {
			if err := validateCloneClusterDescription(tt.in); err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("testCloneClusterDescription expected error: %v, got: %v", tt.err, err)
				}
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}
		})
	}
}

func TestUnmarshalMaintenanceWindow(t *testing.T) {
	for _, tt := range maintenanceWindows {
		t.Run(tt.about, func(t *testing.T) {
			var m MaintenanceWindow
			err := m.UnmarshalJSON(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("MaintenanceWindow unmarshal expected error: %v, got %v", tt.err, err)
				}
				return
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}

			if !reflect.DeepEqual(m, tt.out) {
				t.Errorf("Expected maintenance window: %#v, got: %#v", tt.out, m)
			}
		})
	}
}

func TestMarshalMaintenanceWindow(t *testing.T) {
	for _, tt := range maintenanceWindows {
		t.Run(tt.about, func(t *testing.T) {
			if tt.err != nil {
				return
			}

			s, err := tt.out.MarshalJSON()
			if err != nil {
				t.Errorf("Marshal Error: %v", err)
			}

			if !bytes.Equal(s, tt.in) {
				t.Errorf("Expected Marshal: %q, got: %q", string(tt.in), string(s))
			}
		})
	}
}

func TestUnmarshalPostgresStatus(t *testing.T) {
	for _, tt := range postgresStatus {
		t.Run(tt.about, func(t *testing.T) {

			var ps PostgresStatus
			err := ps.UnmarshalJSON(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("CR status unmarshal expected error: %v, got %v", tt.err, err)
				}
				return
			}

			if !reflect.DeepEqual(ps, tt.out) {
				t.Errorf("Expected status: %#v, got: %#v", tt.out, ps)
			}
		})
	}
}

func TestPostgresUnmarshal(t *testing.T) {
	for _, tt := range unmarshalCluster {
		t.Run(tt.about, func(t *testing.T) {
			var cluster Postgresql
			err := cluster.UnmarshalJSON(tt.in)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("Unmarshal expected error: %v, got: %v", tt.err, err)
				}
				return
			} else if tt.err != nil {
				t.Errorf("Expected error: %v", tt.err)
			}

			if !reflect.DeepEqual(cluster, tt.out) {
				t.Errorf("Expected Postgresql: %#v, got %#v", tt.out, cluster)
			}
		})
	}
}

func TestMarshal(t *testing.T) {
	for _, tt := range unmarshalCluster {
		t.Run(tt.about, func(t *testing.T) {

			if tt.err != nil {
				return
			}

			// Unmarshal and marshal example to capture api changes
			var cluster Postgresql
			err := cluster.UnmarshalJSON(tt.marshal)
			if err != nil {
				if tt.err == nil || err.Error() != tt.err.Error() {
					t.Errorf("Backwards compatibility unmarshal expected error: %v, got: %v", tt.err, err)
				}
				return
			}
			expected, err := json.Marshal(cluster)
			if err != nil {
				t.Errorf("Backwards compatibility marshal error: %v", err)
			}

			m, err := json.Marshal(tt.out)
			if err != nil {
				t.Errorf("Marshal error: %v", err)
			}
			if !bytes.Equal(m, expected) {
				t.Errorf("Marshal Postgresql \nexpected: %q, \ngot:      %q", string(expected), string(m))
			}
		})
	}
}

func TestPostgresMeta(t *testing.T) {
	for _, tt := range unmarshalCluster {
		t.Run(tt.about, func(t *testing.T) {

			if a := tt.out.GetObjectKind(); a != &tt.out.TypeMeta {
				t.Errorf("GetObjectKindMeta \nexpected: %v, \ngot:       %v", tt.out.TypeMeta, a)
			}

			if a := tt.out.GetObjectMeta(); reflect.DeepEqual(a, tt.out.ObjectMeta) {
				t.Errorf("GetObjectMeta \nexpected: %v, \ngot:       %v", tt.out.ObjectMeta, a)
			}
		})
	}
}

func TestPostgresListMeta(t *testing.T) {
	for _, tt := range postgresqlList {
		t.Run(tt.about, func(t *testing.T) {
			if tt.err != nil {
				return
			}

			if a := tt.out.GetObjectKind(); a != &tt.out.TypeMeta {
				t.Errorf("GetObjectKindMeta expected: %v, got: %v", tt.out.TypeMeta, a)
			}

			if a := tt.out.GetListMeta(); reflect.DeepEqual(a, tt.out.ListMeta) {
				t.Errorf("GetObjectMeta expected: %v, got: %v", tt.out.ListMeta, a)
			}
		})
	}
}

func TestPostgresqlClone(t *testing.T) {
	for _, tt := range unmarshalCluster {
		t.Run(tt.about, func(t *testing.T) {
			cp := &tt.out
			cp.Error = ""
			clone := cp.Clone()
			if !reflect.DeepEqual(clone, cp) {
				t.Errorf("TestPostgresqlClone expected: \n%#v\n, got \n%#v", cp, clone)
			}
		})
	}
}

func TestAllowedSourceRangesPattern(t *testing.T) {
	// pattern used in CRD validation for allowedSourceRanges
	pattern := `^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$`
	re := regexp.MustCompile(pattern)

	valid := []string{
		// IPv4
		"192.168.1.0/24",
		"0.0.0.0/0",
		"127.0.0.1/32",
		"10.0.0.0/8",
		"185.85.220.0/22",
		// IPv6
		"fd01::/48",
		"::1/128",
		"::/0",
		"2001:db8::/32",
		"fe80::1/64",
		"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128",
	}

	invalid := []string{
		"999.999.999.999/24",
		"192.168.1.0/33",
		"192.168.1.0",
		"not-an-ip",
		"fd01::/129",
		"::gggg/64",
		"",
	}

	for _, cidr := range valid {
		if !re.MatchString(cidr) {
			t.Errorf("expected %q to match allowedSourceRanges pattern", cidr)
		}
	}

	for _, cidr := range invalid {
		if re.MatchString(cidr) {
			t.Errorf("expected %q NOT to match allowedSourceRanges pattern", cidr)
		}
	}
}
