// +build integration

package function_test

import (
	"context"
	"os"
	"reflect"
	"testing"
	"time"

	boson "github.com/boson-project/func"
	"github.com/boson-project/func/buildpacks"
	"github.com/boson-project/func/docker"
	"github.com/boson-project/func/knative"
)

/*
 NOTE:  Running integration tests locally requires a configured test cluster.
        Test failures may require manual removal of dangling resources.

 ## Integration Cluster

 These integration tests require a properly configured cluster,
 such as that which is setup and configured in CI (see .github/workflows).
 A local KinD cluster can be started via:
   ./hack/allocate.sh && ./hack/configure.sh

 ## Integration Testing

 These tests can be run via the make target:
   make test-integration
  or manually by specifying the tag
   go test -v -tags integration ./...

 ## Teardown and Cleanup

 Tests should clean up after themselves.  In the event of failures, one may
 need to manually remove files:
   rm -rf ./testdata/example.com
 The test cluster is not automatically removed, as it can be reused.  To remove:
   ./hack/delete.sh
*/

const (
	// DefaultRegistry must contain both the registry host and
	// registry namespace at this time.  This will likely be
	// split and defaulted to the forthcoming in-cluster registry.
	DefaultRegistry = "localhost:5000/func"

	// DefaultNamespace for the underlying deployments.  Must be the same
	// as is set up and configured (see hack/configure.sh)
	DefaultNamespace = "func"
)

func TestList(t *testing.T) {
	verbose := true

	// Assemble
	lister, err := knative.NewLister(DefaultNamespace)
	if err != nil {
		t.Fatal(err)
	}
	client := boson.New(
		boson.WithLister(lister),
		boson.WithVerbose(verbose))

	// Act
	names, err := client.List(context.Background())
	if err != nil {
		t.Fatal(err)
	}

	// Assert
	if len(names) != 0 {
		t.Fatalf("Expected no Functions, got %v", names)
	}
}

// TestNew creates
func TestNew(t *testing.T) {
	defer within(t, "testdata/example.com/testnew")()
	verbose := true

	client := newClient(verbose)

	// Act
	if err := client.New(context.Background(), boson.Function{Name: "testnew", Root: ".", Runtime: "go"}); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "testnew")

	// Assert
	items, err := client.List(context.Background())
	names := []string{}
	for _, item := range items {
		names = append(names, item.Name)
	}
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(names, []string{"testnew"}) {
		t.Fatalf("Expected function list ['testnew'], got %v", names)
	}
}

// TestDeploy updates
func TestDeploy(t *testing.T) {
	defer within(t, "testdata/example.com/deploy")()
	verbose := true

	client := newClient(verbose)

	if err := client.New(context.Background(), boson.Function{Name: "deploy", Root: ".", Runtime: "go"}); err != nil {
		t.Fatal(err)
	}
	defer del(t, client, "deploy")

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

// TestRemove deletes
func TestRemove(t *testing.T) {
	defer within(t, "testdata/example.com/remove")()
	verbose := true

	client := newClient(verbose)

	if err := client.New(context.Background(), boson.Function{Name: "remove", Root: ".", Runtime: "go"}); err != nil {
		t.Fatal(err)
	}
	waitFor(t, client, "remove")

	if err := client.Remove(context.Background(), boson.Function{Name: "remove"}); err != nil {
		t.Fatal(err)
	}

	names, err := client.List(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(names) != 0 {
		t.Fatalf("Expected empty Functions list, got %v", names)
	}
}

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

// newClient creates an instance of the func client whose concrete impls
// match those created by the kn func plugin CLI.
func newClient(verbose bool) *boson.Client {
	// TODO: Forcing verbose to false in order to pass integration tests
	verbose = false
	builder := buildpacks.NewBuilder()
	builder.Verbose = verbose

	pusher, err := docker.NewPusher()
	if err != nil {
		panic(err)
	}
	pusher.Verbose = verbose

	deployer, err := knative.NewDeployer(DefaultNamespace)
	if err != nil {
		panic(err) // TODO: remove error from deployer constructor
	}
	deployer.Verbose = verbose

	remover, err := knative.NewRemover(DefaultNamespace)
	if err != nil {
		panic(err) // TODO: remove error from remover constructor
	}
	remover.Verbose = verbose

	lister, err := knative.NewLister(DefaultNamespace)
	if err != nil {
		panic(err) // TODO: remove error from lister constructor
	}
	lister.Verbose = verbose

	return boson.New(
		boson.WithRegistry(DefaultRegistry),
		boson.WithVerbose(verbose),
		boson.WithBuilder(builder),
		boson.WithPusher(pusher),
		boson.WithDeployer(deployer),
		boson.WithRemover(remover),
		boson.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 *boson.Client, name string) {
	t.Helper()
	waitFor(t, c, name)
	if err := c.Remove(context.Background(), boson.Function{Name: name}); 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 Boson provider's Creaet would be made syncrhonous.
func waitFor(t *testing.T, c *boson.Client, name string) {
	t.Helper()
	var pollInterval = 2 * time.Second

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

// Create the given directory, CD to it, and return a function which can be
// run in a defer statement to return to the original directory and cleanup.
// Note must be executed, not deferred itself
// NO:  defer within(t, "somedir")
// YES: defer within(t, "somedir")()
func within(t *testing.T, root string) func() {
	t.Helper()
	cwd := pwd(t)
	mkdir(t, root)
	cd(t, root)
	return func() {
		cd(t, cwd)
		rm(t, root)
	}
}

func pwd(t *testing.T) string {
	t.Helper()
	dir, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	return dir
}

func mkdir(t *testing.T, dir string) {
	t.Helper()
	if err := os.MkdirAll(dir, 0700); err != nil {
		t.Fatal(err)
	}
}

func cd(t *testing.T, dir string) {
	t.Helper()
	if err := os.Chdir(dir); err != nil {
		t.Fatal(err)
	}
}

func rm(t *testing.T, dir string) {
	t.Helper()
	if err := os.RemoveAll(dir); err != nil {
		t.Fatal(err)
	}
}

func touch(file string) {
	_, err := os.Stat(file)
	if os.IsNotExist(err) {
		f, err := os.Create(file)
		if err != nil {
			panic(err)
		}
		defer f.Close()
	}
	t := time.Now().Local()
	if err := os.Chtimes(file, t, t); err != nil {
		panic(err)
	}
}
