package functions

import (
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"gopkg.in/yaml.v2"
	fnlabels "knative.dev/func/pkg/k8s/labels"

	. "knative.dev/func/pkg/testing"
)

// TestFunction_Validate ensures that we are permissive on what we accept and
// strict on what we emit.  This takes the form of not validating a function
// on instantiation, but rather on write.  A function is expected to be in a
// partial, even invalid state on disk; mostly due to the possibility of manual
// editing of the func.yaml.  Writing, however, should always to write a
// function in a known valid state.
func TestFunction_Validate(t *testing.T) {
	root, cleanup := Mktemp(t)
	t.Cleanup(cleanup)

	var f Function
	var err error

	// Loading a nonexistent (new) function should not fail
	// I.e. it will not run .Validate, or it would error that the function at
	// root has no language or name.
	if f, err = NewFunction(root); err != nil {
		t.Fatal(err)
	}

	// Attempting to write the function will fail as being invalid
	invalidEnv := "*invalid"
	f.Build.BuildEnvs = []Env{{Name: &invalidEnv}}
	if err = f.Write(); err == nil {
		t.Fatalf("expected error writing an incomplete (invalid) function")
	}

	// Write the invalid Function
	//
	// Write this intentionally invalid function to disk.
	// NOTE: this depends on an implementation detail of the package: the yaml
	// serialization of the Function struct to a known filename. This is why this
	// test belongs here in the same package as the implementation rather than in
	// package functions_test which treats the function package as an opaque-box.
	path := filepath.Join(root, FunctionFile)
	bb, err := yaml.Marshal(&f)
	if err != nil {
		t.Fatal(err)
	}
	if err = os.WriteFile(path, bb, os.ModePerm); err != nil {
		t.Fatal(err)
	}

	// Loading the invalid function should not fail, but validation should.
	if f, err = NewFunction(root); err != nil {
		t.Fatal(err)
	}
	if err = f.Validate(); err == nil { // axiom check; not strictly part of this test
		t.Fatal("did not receive an error validating a known-invlaid (incomplete) function")
	}

	// Remove the invalid structures... write should complete without error.
	f.Build.BuildEnvs = []Env{}
	if err = f.Write(); err != nil {
		t.Fatal(err)
	}
	if f, err = NewFunction(root); err != nil {
		t.Fatal(err)
	}
	if err = f.Validate(); err != nil {
		t.Fatal(err)
	}

}

func TestFunction_ImageWithDigest(t *testing.T) {
	type fields struct {
		Image       string
		ImageDigest string
	}
	tests := []struct {
		name   string
		fields fields
		want   string
	}{
		{
			name:   "Full path with port",
			fields: fields{Image: "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar", ImageDigest: "42"},
			want:   "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar@42",
		},
		{
			name:   "Path with namespace",
			fields: fields{Image: "johndoe/bar", ImageDigest: "42"},
			want:   "johndoe/bar@42",
		},
		{
			name:   "Just image name",
			fields: fields{Image: "bar:latest", ImageDigest: "42"},
			want:   "bar@42",
		},
		{
			name:   "Full path with port and SHA256 Digest",
			fields: fields{Image: "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar@sha256:42", ImageDigest: "sha256:42"},
			want:   "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar@sha256:42",
		},
		{
			name:   "Full path with port and SHA256 Digest with empty ImageDigest",
			fields: fields{Image: "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar@sha256:42", ImageDigest: ""},
			want:   "image-registry.openshift-image-registry.svc.cluster.local:50000/default/bar@sha256:42",
		},
	}
	//TODO: gauron99 - this is gonna need to be changed (probably) because:
	// 1: imageDigest now doesnt have a dedicated structure member (resolved?)
	// 2: is still fetched after pushing the Function (which is a temporary fix -- it really should be during build)
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := Function{
				Build: BuildSpec{
					Image: tt.fields.Image,
				},
			}
			if got := f.ImageNameWithDigest(tt.fields.ImageDigest); got != tt.want {
				t.Errorf("ImageNameWithDigest(tt.fields.ImageDigest) = %v, want %v", got, tt.want)
			}
		})
	}
}

// TestFunction_ImageName ensures that the full image name is
// returned for a Function, based on the Function's Registry and Name,
// including utilizing the DefaultRegistry if the Function's defined
// registry is a single token (just the namespace).
func TestFunction_ImageName(t *testing.T) {
	var (
		f   Function
		got string
		err error
	)
	tests := []struct {
		name          string
		registry      string
		funcName      string
		expectedImage string
		expectError   bool
	}{
		{"short-name", "alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
		{"short-name-trailing-slash", "alice/", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
		{"full-name-quay-io", "quay.io/alice", "myfunc", "quay.io/alice/myfunc:latest", false},
		{"full-name-docker-io", "docker.io/alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
		{"full-name-with-sub-path", "docker.io/alice/sub", "myfunc", DefaultRegistry + "/alice/sub/myfunc:latest", false},
		{"localhost-direct", "localhost:5000", "myfunc", "localhost:5000/myfunc:latest", false},
		{"full-name-with-sub-sub-path", "us-central1-docker.pkg.dev/my-gcpproject/team/user", "myfunc", "us-central1-docker.pkg.dev/my-gcpproject/team/user/myfunc:latest", false},
		{"missing-func-name", "alice", "", "", true},
		{"missing-registry", "", "myfunc", "", true},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			f = Function{Registry: test.registry, Name: test.funcName}
			got, err = f.ImageName()
			if test.expectError && err == nil {
				t.Errorf("registry '%v' and name '%v' did not yield the expected error",
					test.registry, test.funcName)
			}
			if !test.expectError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if got != test.expectedImage {
				t.Errorf("expected registry '%v' name '%v' to yield image '%v', got '%v'",
					test.registry, test.funcName, test.expectedImage, got)
			}
		})
	}
}

func Test_LabelsMap(t *testing.T) {
	key1 := "key1"
	key2 := "key2"
	value1 := "value1"
	value2 := "value2"

	t.Setenv("BAD_EXAMPLE", ":invalid")
	valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}"

	t.Setenv("GOOD_EXAMPLE", "valid")
	valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}"

	tests := []struct {
		name        string
		labels      []Label
		expectErr   bool
		expectedMap map[string]string
	}{
		{
			name: "invalid Labels should return err",
			labels: []Label{
				{
					Value: &value1,
				},
			},
			expectErr: true,
		},
		{
			name: "with valid env var",
			labels: []Label{
				{
					Key:   &key1,
					Value: &valueLocalEnv4,
				},
			},
			expectErr: false,
			expectedMap: map[string]string{
				key1: "valid",
			},
		},
		{
			name: "with invalid env var",
			labels: []Label{
				{
					Key:   &key1,
					Value: &valueLocalEnvIncorrect4,
				},
			},
			expectErr: true,
		},
		{
			name: "empty labels allowed. returns default labels",
			labels: []Label{
				{
					Key: &key1,
				},
			},
			expectErr: false,
			expectedMap: map[string]string{
				key1: "",
			},
		},
		{
			name: "full set of labels",
			labels: []Label{
				{
					Key:   &key1,
					Value: &value1,
				},
				{
					Key:   &key2,
					Value: &value2,
				},
			},
			expectErr: false,
			expectedMap: map[string]string{
				key1: value1,
				key2: value2,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			f := Function{
				Name:    "some-function",
				Runtime: "golang",
				Deploy:  DeploySpec{Labels: tt.labels},
			}
			got, err := f.LabelsMap()

			if tt.expectErr {
				if err == nil {
					t.Error("expected error but didn't get an error from LabelsMap")
				}
			} else {
				if err != nil {
					t.Errorf("got unexpected err: %s", err)
				}
			}
			if err == nil {
				defaultLabels := expectedDefaultLabels(f)
				for k, v := range defaultLabels {
					tt.expectedMap[k] = v
				}
				if res := reflect.DeepEqual(got, tt.expectedMap); !res {
					t.Errorf("mismatch in actual and expected labels return. actual: %#v, expected: %#v", got, tt.expectedMap)
				}
			}
		})
	}
}

func expectedDefaultLabels(f Function) map[string]string {
	return map[string]string{
		fnlabels.FunctionNameKey:    f.Name,
		fnlabels.FunctionRuntimeKey: f.Runtime,
	}
}
