//go:build integration
// +build integration

package functions_test

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/volume"
	"github.com/docker/docker/client"

	"knative.dev/func/pkg/builders/s2i"
	"knative.dev/func/pkg/docker"
	fn "knative.dev/func/pkg/functions"
	"knative.dev/func/pkg/knative"
	"knative.dev/func/pkg/oci"
	. "knative.dev/func/pkg/testing"
	"knative.dev/pkg/ptr"
)

// # Integration Tests
//
// go test -tags integration ./...
//
// ## Requirements
//
// A cluster is required. See .github/workflows for more. For example:
//
//   ./hack/install-binaries.sh && ./hack/allocate.sh && ./hack/registry.sh
//
// Binaries are required:  go for compiling functions and git for
// repository-related tests.
//
// ## Cluster Cleanup
//
// The test cluster and most resources can be removed with:
//   ./hack/delete.sh
//
// ## Configuration
//
// Use the FUNC_INT_* environment variables to alter behavior, binaries to
// use, etc.
//
// NOTE: Downloaded images are not removed.
//

const (
	DefaultIntTestHome       = "./testdata/default_home"
	DefaultIntTestKubeconfig = "../../hack/bin/kubeconfig.yaml"
	DefaultIntTestRegistry   = "localhost:50000/func"
	DefaultIntTestNamespace  = "default"
	DefaultIntTestVerbose    = false
)

var (
	Go         = getEnvAsBin("FUNC_INT_GO", "go")
	Git        = getEnvAsBin("FUNC_INT_GIT", "git")
	Kubeconfig = getEnvAsPath("FUNC_INT_KUBECONFIG", DefaultIntTestKubeconfig)
	Verbose    = getEnvAsBool("FUNC_INT_VERBOSE", DefaultIntTestVerbose)
	Registry   = getEnv("FUNC_INT_REGISTRY", DefaultIntTestRegistry)
	Home, _    = filepath.Abs(DefaultIntTestHome)
)

// containsInstance checks if the list includes the given instance.
func containsInstance(list []fn.ListItem, name, namespace string) bool {
	for _, v := range list {
		if v.Name == name && v.Namespace == namespace {
			return true
		}
	}
	return false
	// Note that client.List is tested implicitly via its use in TestInt_New
	// and TestInt_Delete.
}

// TestInt_New creates
func TestInt_New(t *testing.T) {
	resetEnv()
	// Assemble
	root, cleanup := Mktemp(t)
	defer cleanup()
	verbose := true
	name := "test-new"
	client := newClient(verbose)

	// Act
	if _, _, err := client.New(context.Background(), fn.Function{Name: name, Namespace: DefaultIntTestNamespace, Root: root, Runtime: "go"}); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, name, DefaultIntTestNamespace)

	// Assert
	list, err := client.List(context.Background(), DefaultIntTestNamespace)
	if err != nil {
		t.Fatal(err)
	}

	if !containsInstance(list, name, DefaultIntTestNamespace) {
		t.Log(list)
		t.Fatalf("deployed instance list does not contain function %q", name)
	}
}

// TestInt_Deploy_Defaults deployes using client methods from New but manually
func TestInt_Deploy_Defaults(t *testing.T) {
	resetEnv()
	_, cleanup := Mktemp(t)
	defer cleanup()
	verbose := true

	client := newClient(verbose)
	f := fn.Function{Name: "deploy", Namespace: DefaultIntTestNamespace, Runtime: "go"}
	var err error

	if f, err = client.Init(f); err != nil {
		t.Fatal(err)
	}
	if f, err = client.Build(context.Background(), f); err != nil {
		t.Fatal(err)
	}
	if f, _, err = client.Push(context.Background(), f); err != nil {
		t.Fatal(err)
	}

	defer del(t, client, "deploy", DefaultIntTestNamespace)
	// TODO: gauron99 -- remove this when you set full image name after build instead
	// of push -- this has to be here because of a workaround
	f.Deploy.Image = f.Build.Image

	if f, err = client.Deploy(context.Background(), f); err != nil {
		t.Fatal(err)
	}
}

// TestInt_Deploy_WithOptions deploys function with all options explicitly set
func TestInt_Deploy_WithOptions(t *testing.T) {
	resetEnv()
	root, cleanup := Mktemp(t)
	defer cleanup()
	verbose := false

	f := fn.Function{Runtime: "go", Name: "test-deploy-with-options", Root: root, Namespace: DefaultIntTestNamespace}
	f.Deploy = fn.DeploySpec{
		Options: fn.Options{
			Scale: &fn.ScaleOptions{
				Min:         ptr.Int64(1),
				Max:         ptr.Int64(10),
				Metric:      ptr.String("concurrency"),
				Target:      ptr.Float64(5),
				Utilization: ptr.Float64(5),
			},
			Resources: &fn.ResourcesOptions{
				Requests: &fn.ResourcesRequestsOptions{
					CPU:    ptr.String("10m"),
					Memory: ptr.String("100m"),
				},
				Limits: &fn.ResourcesLimitsOptions{
					CPU:         ptr.String("1000m"),
					Memory:      ptr.String("1000M"),
					Concurrency: ptr.Int64(10),
				},
			},
		},
	}

	client := newClient(verbose)
	if _, _, err := client.New(context.Background(), f); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "test-deploy-with-options", DefaultIntTestNamespace)
}

func TestInt_Deploy_WithTriggers(t *testing.T) {
	resetEnv()
	root, cleanup := Mktemp(t)
	defer cleanup()
	verbose := true

	f := fn.Function{Runtime: "go", Name: "test-deploy-with-triggers", Root: root, Namespace: DefaultIntTestNamespace}
	f.Deploy = fn.DeploySpec{
		Subscriptions: []fn.KnativeSubscription{
			{
				Source: "default",
				Filters: map[string]string{
					"key": "value",
					"foo": "bar",
				},
			},
		},
	}

	client := newClient(verbose)
	if _, _, err := client.New(context.Background(), f); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "test-deploy-with-triggers", DefaultIntTestNamespace)
}

func TestInt_Update_WithAnnotationsAndLabels(t *testing.T) {
	resetEnv()
	_, cleanup := Mktemp(t)
	defer cleanup()
	functionName := "updateannlab"
	verbose := false

	servingClient, err := knative.NewServingClient(DefaultIntTestNamespace)
	if err != nil {
		t.Fatal(err)
	}

	// Deploy a function without any annotations or labels
	client := newClient(verbose)
	f := fn.Function{Name: functionName, Runtime: "go", Namespace: DefaultIntTestNamespace}

	if _, f, err = client.New(context.Background(), f); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, functionName, DefaultIntTestNamespace)

	// Updated function with a new set of annotations and labels
	// deploy and check that deployed kcsv contains correct annotations and labels

	annotations := map[string]string{"ann1": "val1", "ann2": "val2"}
	labels := []fn.Label{
		{Key: ptr.String("lab1"), Value: ptr.String("v1")},
		{Key: ptr.String("lab2"), Value: ptr.String("v2")},
	}
	f.Deploy.Annotations = annotations
	f.Deploy.Labels = labels

	if f, err = client.Deploy(context.Background(), f, fn.WithDeploySkipBuildCheck(true)); err != nil {
		t.Fatal(err)
	}

	ksvc, err := servingClient.GetService(context.Background(), functionName)
	if err != nil {
		t.Fatal(err)
	}

	for k, v := range annotations {
		if val, ok := ksvc.Annotations[k]; ok {
			if v != val {
				t.Fatal(fmt.Errorf("expected annotation %q to have value %q, but the annotation in the deployed service has value %q", k, v, val))
			}
		} else {
			t.Fatal(fmt.Errorf("annotation %q not found in the deployed service", k))
		}
	}
	for _, l := range labels {
		if val, ok := ksvc.Labels[*l.Key]; ok {
			if *l.Value != val {
				t.Fatal(fmt.Errorf("expected label %q to have value %q, but the label in the deployed service has value %q", *l.Key, *l.Value, val))
			}
		} else {
			t.Fatal(fmt.Errorf("label %q not found in the deployed service", *l.Key))
		}
	}

	// Remove some annotations and labels
	// deploy and check that deployed kcsv contains correct annotations and labels

	annotations = map[string]string{"ann1": "val1"}
	labels = []fn.Label{{Key: ptr.String("lab1"), Value: ptr.String("v1")}}
	f.Deploy.Annotations = annotations
	f.Deploy.Labels = labels

	if f, err = client.Deploy(context.Background(), f, fn.WithDeploySkipBuildCheck(true)); err != nil {
		t.Fatal(err)
	}

	ksvc, err = servingClient.GetService(context.Background(), functionName)
	if err != nil {
		t.Fatal(err)
	}

	for k, v := range annotations {
		if val, ok := ksvc.Annotations[k]; ok {
			if v != val {
				t.Fatal(fmt.Errorf("expected annotation %q to have value %q, but the annotation in the deployed service has value %q", k, v, val))
			}
		} else {
			t.Fatal(fmt.Errorf("annotation %q not found in the deployed service", k))
		}
	}
	for _, l := range labels {
		if val, ok := ksvc.Labels[*l.Key]; ok {
			if *l.Value != val {
				t.Fatal(fmt.Errorf("expected label %q to have value %q, but the label in the deployed service has value %q", *l.Key, *l.Value, val))
			}
		} else {
			t.Fatal(fmt.Errorf("label %q not found in the deployed service", *l.Key))
		}
	}
}

// TestInt_Remove ensures removal of a function instance.
func TestInt_Remove(t *testing.T) {
	resetEnv()
	_, cleanup := Mktemp(t)
	defer cleanup()
	verbose := true
	name := "remove"

	client := newClient(verbose)
	f := fn.Function{Name: name, Namespace: DefaultIntTestNamespace, Runtime: "go"}
	var err error
	if _, _, err = client.New(context.Background(), f); err != nil {
		t.Fatal(err)
	}

	del(t, client, "remove", DefaultIntTestNamespace)

	list, err := client.List(context.Background(), DefaultIntTestNamespace)
	if err != nil {
		t.Fatal(err)
	}
	if containsInstance(list, name, DefaultIntTestNamespace) {
		t.Log(list)
		t.Fatalf("deployed instance list still contains function %q", name)
	}
}

// TestInt_RemoteRepositories ensures that initializing a function
// defined in a remote repository finds the template, writes
// the expected files, and retains the expected modes.
// NOTE: this test only succeeds due to an override in
// templates' copyNode which forces mode 755 for directories.
// See https://github.com/go-git/go-git/issues/364
func TestInt_RemoteRepositories(t *testing.T) {
	resetEnv()
	_, cleanup := Mktemp(t)
	defer cleanup()

	// Write the test template from the remote onto root
	client := fn.New(
		fn.WithRegistry(DefaultIntTestRegistry),
		fn.WithRepository("https://github.com/boson-project/test-templates"),
	)
	_, err := client.Init(fn.Function{
		Root:     ".",
		Runtime:  "runtime",
		Template: "template",
	})
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		Path string
		Perm uint32
		Dir  bool
	}{
		{Path: "file", Perm: 0644},
		{Path: "dir-a/file", Perm: 0644},
		{Path: "dir-b/file", Perm: 0644},
		{Path: "dir-b/executable", Perm: 0755},
		{Path: "dir-b", Perm: 0755},
		{Path: "dir-a", Perm: 0755},
	}

	// Note that .Perm() are used to only consider the least-significant 9 and
	// thus not have to consider the directory bit.
	for _, test := range tests {
		file, err := os.Stat(test.Path)
		if err != nil {
			t.Fatal(err)
		}
		t.Logf("%04o repository/%v", file.Mode().Perm(), test.Path)
		if file.Mode().Perm() != os.FileMode(test.Perm) {
			t.Fatalf("expected 'repository/%v' to have mode %04o, got %04o", test.Path, test.Perm, file.Mode().Perm())
		}
	}
}

// TestInt_Invoke_ClientToService ensures that the client can invoke a remotely
// deployed service, both by the route returned directly as well as using
// the invocation helper client.Invoke.
func TestInt_Invoke_ClientToService(t *testing.T) {
	resetEnv()
	root, cleanup := Mktemp(t)
	defer cleanup()
	var (
		verbose = true
		ctx     = context.Background()
		client  = newClient(verbose)
		route   string
		err     error
	)

	// Create a function
	f := fn.Function{Name: "f", Runtime: "go", Namespace: DefaultIntTestNamespace}
	f, err = client.Init(f)
	if err != nil {
		t.Fatal(err)
	}
	source := `
package function

import "net/http"

func Handle(res http.ResponseWriter, req *http.Request) {
  res.Write([]byte("TestInvoke_ClientToService OK"))
}
`
	err = os.WriteFile(filepath.Join(root, "handle.go"), []byte(source), os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}

	if route, f, err = client.Apply(ctx, f); err != nil {
		t.Fatal(err)
	}
	if err := f.Write(); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "f", DefaultIntTestNamespace)

	// Invoke via the route
	resp, err := http.Get(route)
	if err != nil {
		t.Fatal(err)
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if string(b) != "TestInvoke_ClientToService OK" {
		t.Fatalf("unexpected response from HTTP GET: %v", b)
	}

	// Invoke using the helper
	_, body, err := client.Invoke(ctx, root, "", fn.NewInvokeMessage())
	if err != nil {
		t.Fatal(err)
	}

	if body != "TestInvoke_ClientToService OK" {
		t.Fatalf("unexpected response from client.Invoke: %v", b)
	}
}

// TestInt_Invoke_ServiceToService ensures that a Function can invoke another
// service via localhost service discovery api provided by the Dapr sidecar.
func TestInt_Invoke_ServiceToService(t *testing.T) {
	resetEnv()
	var (
		verbose = true
		ctx     = context.Background()
		client  = newClient(verbose)
		err     error
		source  string
		route   string
	)

	// Create function A
	// A function which responds to GET requests with a static value.
	root, done := Mktemp(t)
	defer done()
	f := fn.Function{Name: "a", Runtime: "go", Namespace: DefaultIntTestNamespace}
	f, err = client.Init(f)
	if err != nil {
		t.Fatal(err)
	}
	source = `
package function

import "net/http"

func Handle(res http.ResponseWriter, req *http.Request) {
  res.Write([]byte("TestInvoke_ServiceToService OK"))
}
`
	err = os.WriteFile(filepath.Join(root, "handle.go"), []byte(source), os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err = client.Apply(ctx, f); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "a", DefaultIntTestNamespace)

	// Create Function B
	// which responds with the response from an invocation of 'a' via the
	// localhost service discovery and invocation API.
	client2 := newClient(verbose)
	root, done = Mktemp(t)
	defer done()
	f = fn.Function{Name: "b", Runtime: "go", Namespace: DefaultIntTestNamespace}
	f, err = client2.Init(f)
	if err != nil {
		t.Fatal(err)
	}

	source = `
package function

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

func Handle(w http.ResponseWriter, req *http.Request) {
	var e error
	var r *http.Response
	var buff bytes.Buffer
	var out io.Writer = io.MultiWriter(os.Stderr, &buff)
	for i := 0; i < 10; i++ {
		r, e = http.Get("http://localhost:3500/v1.0/invoke/a/method/")
		if e != nil {
			_, _ = fmt.Fprintf(out, "unable to invoke function a: %v\n", e)
			time.Sleep(time.Second*3)
			continue
		}
		defer r.Body.Close()
		if r.StatusCode != 200 {
			_, _ = fmt.Fprintf(out, "bad http status code when invoking a: %d\n", r.StatusCode)
			time.Sleep(time.Second*3)
			continue
		}
		w.WriteHeader(200)
		_, _ = io.Copy(w, r.Body)
		return
	}
	http.Error(w, fmt.Sprintf("unable to invoke function a:\n%s", buff.String()), http.StatusServiceUnavailable)
	return
}
`
	err = os.WriteFile(filepath.Join(root, "handle.go"), []byte(source), os.ModePerm)
	if err != nil {
		t.Fatal(err)
	}
	if route, f, err = client2.Apply(ctx, f); err != nil {
		t.Fatal(err)
	}
	defer client2.Remove(ctx, "", "", f, true)

	resp, err := http.Get(route)
	if err != nil {
		t.Fatal(err)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if string(body) != "TestInvoke_ServiceToService OK" {
		t.Fatalf("Unexpected response from Function B: %v", string(body))
	}
}

// TestDeployWithoutHome ensures that running client.New works without
// home
func TestDeployWithoutHome(t *testing.T) {
	root, cleanup := Mktemp(t)
	defer cleanup()

	t.Setenv("HOME", "")
	t.Setenv("XDG_CONFIG_HOME", "")
	verbose := false
	name := "test-deploy-no-home"

	f := fn.Function{Runtime: "node", Name: name, Root: root, Namespace: DefaultIntTestNamespace}

	// client with s2i builder because pack needs HOME
	client := newClientWithS2i(verbose)

	// expect to succeed
	_, _, err := client.New(context.Background(), f)
	if err != nil {
		t.Fatalf("expected no errors but got %v", err)
	}

	defer del(t, client, name, DefaultIntTestNamespace)
}

// ***********
//	Helpers
// ***********

func getEnvAsPath(env, dflt string) (val string) {
	val = getEnv(env, dflt)
	if !filepath.IsAbs(val) { // convert to abs
		var err error
		if val, err = filepath.Abs(val); err != nil {
			panic(fmt.Sprintf("error converting path to absolute. %v", err))
		}
	}
	return
}

func getEnvAsBool(env string, dfltBool bool) bool {
	dflt := fmt.Sprintf("%t", dfltBool)
	val, err := strconv.ParseBool(getEnv(env, dflt))
	if err != nil {
		panic(fmt.Sprintf("value for %v expected to be boolean. %v", env, err))
	}
	return val
}

func getEnvAsBin(env, dflt string) string {
	val, err := exec.LookPath(getEnv(env, dflt))
	if err != nil {
		fmt.Fprintf(os.Stderr, "error locating command %q. %v", val, err)
	}
	return val
}

func getEnv(env, dflt string) (val string) {
	if v := os.Getenv(env); v != "" {
		val = v
	}
	if val == "" {
		val = dflt
	}
	return
}

func resetEnv() {
	os.Clearenv()
	os.Setenv("HOME", Home)
	os.Setenv("KUBECONFIG", Kubeconfig)
	os.Setenv("FUNC_GO", Go)
	os.Setenv("FUNC_GIT", Git)
	os.Setenv("FUNC_VERBOSE", fmt.Sprintf("%t", Verbose))

	// The Registry will be set either during first-time setup using the
	// global config, or already defaulted by the user via environment variable.
	os.Setenv("FUNC_REGISTRY", Registry)

	// The following host-builder related settings will become the defaults
	// once the host builder supports the core runtimes.  Setting them here in
	// order to futureproof individual tests.
	os.Setenv("FUNC_ENABLE_HOST_BUILDER", "true") // Enable the host builder
	os.Setenv("FUNC_BUILDER", "host")             // default to host builder
	os.Setenv("FUNC_CONTAINER", "false")          // "run" uses host builder

}

// newClient creates an instance of the func client with concrete impls
// sufficient for running integration tests.
func newClient(verbose bool) *fn.Client {
	return fn.New(
		fn.WithRegistry(DefaultIntTestRegistry),
		fn.WithBuilder(oci.NewBuilder("", verbose)),
		fn.WithPusher(oci.NewPusher(true, true, verbose)),
		fn.WithDeployer(knative.NewDeployer(knative.WithDeployerVerbose(verbose))),
		fn.WithDescriber(knative.NewDescriber(verbose)),
		fn.WithRemover(knative.NewRemover(verbose)),
		fn.WithLister(knative.NewLister(verbose)),
		fn.WithVerbose(verbose),
	)
}

// copy of newClient just builder is s2i instead of buildpacks
func newClientWithS2i(verbose bool) *fn.Client {
	builder := s2i.NewBuilder(s2i.WithVerbose(verbose))
	pusher := docker.NewPusher(docker.WithVerbose(verbose))
	deployer := knative.NewDeployer(knative.WithDeployerVerbose(verbose))
	describer := knative.NewDescriber(verbose)
	remover := knative.NewRemover(verbose)
	lister := knative.NewLister(verbose)

	return fn.New(
		fn.WithRegistry(DefaultIntTestRegistry),
		fn.WithVerbose(verbose),
		fn.WithBuilder(builder),
		fn.WithPusher(pusher),
		fn.WithDeployer(deployer),
		fn.WithDescriber(describer),
		fn.WithRemover(remover),
		fn.WithLister(lister),
	)
}

// Del cleans up after a test by removing a function by name.
// (test fails if the named function does not exist)
//
// Intended to be run in a defer statement immediately after creation, del
// works around the asynchronicity of the underlying platform's creation
// step by polling the provider until the names function becomes available
// (or the test times out), before firing off a deletion request.
// Of course, ideally this would be replaced by the use of a synchronous
// method, or at a minimum a way to register a callback/listener for the
// creation event.  This is what we have for now, and the show must go on.
func del(t *testing.T, c *fn.Client, name, namespace string) {
	t.Helper()
	waitFor(t, c, name, namespace)
	f := fn.Function{Name: name, Deploy: fn.DeploySpec{Namespace: DefaultIntTestNamespace}}
	if err := c.Remove(context.Background(), "", "", f, false); err != nil {
		t.Fatal(err)
	}

	// TODO(lkingland):  The below breaks things
	// The following should not exist, nor its imports.
	//
	// If there is ever a problem with dangling volumes, that is
	// something which should be fixed in the _remover_, and tested there.
	//
	// ... check with Jefferson what caused such a state, open an
	// issue to alter remover if necessary, and then delete all of this.
	if true {
		return
	}

	cli, _, err := docker.NewClient(client.DefaultDockerHost)
	if err != nil {
		t.Fatal(err)
	}
	defer cli.Close()
	opts := volume.ListOptions{
		Filters: filters.NewArgs(
			filters.Arg("name", fmt.Sprintf("pack-cache-func_%s_*", name)),
		),
	}
	resp, err := cli.VolumeList(context.Background(), opts)
	if err != nil {
		t.Fatal(err)
	}
	for _, vol := range resp.Volumes {
		t.Log("deleting volume:", vol.Name)
		err = cli.VolumeRemove(context.Background(), vol.Name, true)
		if err != nil {
			t.Fatal(err)
		}
	}
}

// waitFor the named function to become available in List output.
// TODO: the API should be synchronous, but that depends first on
// Create returning the derived name such that we can bake polling in.
// Ideally the provider's Create would be made syncrhonous.
func waitFor(t *testing.T, c *fn.Client, name, namespace string) {
	t.Helper()
	var pollInterval = 2 * time.Second

	for { // ever (i.e. defer to global test timeout)
		nn, err := c.List(context.Background(), namespace)
		if err != nil {
			t.Fatal(err)
		}
		for _, n := range nn {
			if n.Name == name {
				return
			}
		}
		time.Sleep(pollInterval)
	}
}
