// +build !integration

package function_test

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	fn "github.com/boson-project/func"
	"github.com/boson-project/func/mock"
)

const (
	// TestRegistry for calculating destination image during tests.
	// Will be optional once we support in-cluster container registries
	// by default.  See TestRegistryRequired for details.
	TestRegistry = "quay.io/alice"

	// TestRuntime consists of a specially designed templates directory
	// used exclusively for embedded template write tests.
	TestRuntime = "test"
)

// TestNew Function completes without error using defaults and zero values.
// New is the superset of creating a new fully deployed Function, and
// thus implicitly tests Create, Build and Deploy, which are exposed
// by the client API for those who prefer manual transmissions.
func TestNew(t *testing.T) {
	root := "testdata/example.com/testCreate" // Root from which to run the test
	defer using(t, root)()

	// New Client
	client := fn.New(fn.WithRegistry(TestRegistry))

	// New Function using Client
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}
}

// TestTemplateWrites ensures a template is written.
func TestTemplateWrites(t *testing.T) {
	root := "testdata/example.com/testCreateWrites"
	defer using(t, root)()

	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.Create(fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Assert file was written
	if _, err := os.Stat(filepath.Join(root, fn.ConfigFile)); os.IsNotExist(err) {
		t.Fatalf("Initialize did not result in '%v' being written to '%v'", fn.ConfigFile, root)
	}
}

// TestExtantAborts ensures that a directory which contains an extant
// Function does not reinitialize
func TestExtantAborts(t *testing.T) {
	root := "testdata/example.com/testCreateInitializedAborts"
	defer using(t, root)()

	// New once
	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// New again should fail as already initialized
	if err := client.New(context.Background(), fn.Function{Root: root}); err == nil {
		t.Fatal("error expected initilizing a path already containing an initialized Function")
	}
}

// TestNonemptyDirectoryAborts ensures that a directory which contains any
// visible files aborts.
func TestNonemptyDirectoryAborts(t *testing.T) {
	root := "testdata/example.com/testCreateNonemptyDirectoryAborts"
	defer using(t, root)()

	// An unexpected, non-hidden file.
	visibleFile := filepath.Join(root, "file.txt")
	if err := ioutil.WriteFile(visibleFile, []byte{}, 0644); err != nil {
		t.Fatal(err)
	}

	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.New(context.Background(), fn.Function{Root: root}); err == nil {
		t.Fatal("error expected initilizing a Function in a nonempty directory")
	}
}

// TestHiddenFilesIgnored ensures that initializing in a directory that
// only contains hidden files does not error, protecting against the naieve
// implementation of aborting initialization if any files exist, which would
// break functions tracked in source control (.git), or when used in
// conjunction with other tools (.envrc, etc)
func TestHiddenFilesIgnored(t *testing.T) {
	// Create a directory for the Function
	root := "testdata/example.com/testCreateHiddenFilesIgnored"
	defer using(t, root)()

	// Create a hidden file that should be ignored.
	hiddenFile := filepath.Join(root, ".envrc")
	if err := ioutil.WriteFile(hiddenFile, []byte{}, 0644); err != nil {
		t.Fatal(err)
	}

	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}
}

// TestDefaultRuntime ensures that the default runtime is applied to new
// Functions and persisted.
func TestDefaultRuntime(t *testing.T) {
	// Create a root for the new Function
	root := "testdata/example.com/testCreateDefaultRuntime"
	defer using(t, root)()

	// Create a new function at root with all defaults.
	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Load the function
	f, err := fn.NewFunction(root)
	if err != nil {
		t.Fatal(err)
	}

	// Ensure it has defaulted runtime
	if f.Runtime != fn.DefaultRuntime {
		t.Fatal("The default runtime was not applied or persisted.")
	}
}

// TestExtensibleRepositories ensures that templates are extensible
// using a custom path to template repositories on disk. The custom repositories
// location is not defined herein but expected to be provided because, for
// example, a CLI may want to use XDG_CONFIG_HOME.  Assuming a repository path
// $FUNC_REPOSITORIES, a Go template named 'json' which is provided in the
// repository 'boson', would be expected to be in the location:
// $FUNC_REPOSITORIES/boson/go/json
// See the CLI for full details, but a standard default location is
// $HOME/.config/func/repositories/boson/go/json
func TestExtensibleRepositories(t *testing.T) {
	// Create a directory for the new Function
	root := "testdata/example.com/testExtensibleRepositories"
	defer using(t, root)()

	// Create a new client with a path to the extensible templates
	client := fn.New(
		fn.WithRepositories("testdata/repositories"),
		fn.WithRegistry(TestRegistry))

	// Create a Function specifying a template which only exists in the extensible set
	if err := client.New(context.Background(), fn.Function{Root: root, Runtime: "test", Template: "customProvider/tplc"}); err != nil {
		t.Fatal(err)
	}

	// Ensure that a file from that only exists in that template set was actually written 'json.js'
	if _, err := os.Stat(filepath.Join(root, "customtpl.txt")); os.IsNotExist(err) {
		t.Fatalf("Initializing a custom did not result in customtpl.txt being written to '%v'", root)
	} else if err != nil {
		t.Fatal(err)
	}
}

// TestRuntimeNotFound generates an error (embedded default repository).
func TestRuntimeNotFound(t *testing.T) {
	// Create a directory for the Function
	root := "testdata/example.com/testRuntimeNotFound"
	defer using(t, root)()

	client := fn.New(fn.WithRegistry(TestRegistry))

	// creating a Function with an unsupported runtime should bubble
	// the error generated by the underlying template initializer.
	f := fn.Function{Root: root, Runtime: "invalid"}
	err := client.New(context.Background(), f)
	if !errors.Is(err, fn.ErrRuntimeNotFound) {
		t.Fatalf("Expected ErrRuntimeNotFound, got %T", err)
	}
}

// TestRuntimeNotFoundCustom ensures that the correct error is returned
// when the requested runtime is not found in a given custom repository
func TestRuntimeNotFoundCustom(t *testing.T) {
	root := "testdata/example.com/testRuntimeNotFoundCustom"
	defer using(t, root)()

	// Create a new client with path to the extensible templates
	client := fn.New(
		fn.WithRepositories("testdata/repositories"),
		fn.WithRegistry(TestRegistry))

	// Create a Function specifying a runtime, 'python' that does not exist
	// in the custom (testdata) repository but does in the embedded.
	f := fn.Function{Root: root, Runtime: "python", Template: "customProvider/event"}

	// creating should error as runtime not found
	err := client.New(context.Background(), f)
	if !errors.Is(err, fn.ErrRuntimeNotFound) {
		t.Fatalf("Expected ErrRuntimeNotFound, got %v", err)
	}
}

// TestTemplateNotFound generates an error (embedded default repository).
func TestTemplateNotFound(t *testing.T) {
	root := "testdata/example.com/testTemplateNotFound"
	defer using(t, root)()

	client := fn.New(fn.WithRegistry(TestRegistry))

	// creating a Function with an unsupported runtime should bubble
	// the error generated by the unsderlying template initializer.
	f := fn.Function{Root: root, Runtime: "go", Template: "invalid"}
	err := client.New(context.Background(), f)
	if !errors.Is(err, fn.ErrTemplateNotFound) {
		t.Fatalf("Expected ErrTemplateNotFound, got %v", err)
	}
}

// TestTemplateNotFoundCustom ensures that the correct error is returned
// when the requested template is not found in the given custom repository.
func TestTemplateNotFoundCustom(t *testing.T) {
	root := "testdata/example.com/testTemplateNotFoundCustom"
	defer using(t, root)()

	// Create a new client with path to extensible templates
	client := fn.New(
		fn.WithRepositories("testdata/repositories"),
		fn.WithRegistry(TestRegistry))

	// An invalid template, but a valid custom provider
	f := fn.Function{Root: root, Runtime: "test", Template: "customProvider/invalid"}

	// Creation should generate the correct error of template not being found.
	err := client.New(context.Background(), f)
	if !errors.Is(err, fn.ErrTemplateNotFound) {
		t.Fatalf("Expected ErrTemplateNotFound, got %v", err)
	}
}

// TestNamed ensures that an explicitly passed name is used in leau of the
// path derived name when provided, and persists through instantiations.
func TestNamed(t *testing.T) {
	// Explicit name to use
	name := "service.example.com"

	// Path which would derive to testWithHame.example.com were it not for the
	// explicitly provided name.
	root := "testdata/example.com/testWithName"
	defer using(t, root)()

	client := fn.New(fn.WithRegistry(TestRegistry))

	if err := client.New(context.Background(), fn.Function{Root: root, Name: name}); err != nil {
		t.Fatal(err)
	}

	f, err := fn.NewFunction(root)
	if err != nil {
		t.Fatal(err)
	}

	if f.Name != name {
		t.Fatalf("expected name '%v' got '%v", name, f.Name)
	}
}

// TestRegistryRequired ensures that a registry is required, and is
// prepended with the DefaultRegistry if a single token.
// Registry is the namespace at the container image registry.
// If not prepended with the registry, it will be defaulted:
// Examples:  "docker.io/alice"
//            "quay.io/bob"
//            "charlie" (becomes [DefaultRegistry]/charlie
// At this time a registry namespace is required as we rely on a third-party
// registry in all cases.  When we support in-cluster container registries,
// this configuration parameter will become optional.
func TestRegistryRequired(t *testing.T) {
	// Create a root for the Function
	root := "testdata/example.com/testRegistry"
	defer using(t, root)()

	client := fn.New()
	var err error
	if err = client.New(context.Background(), fn.Function{Root: root}); err == nil {
		t.Fatal("did not receive expected error creating a Function without specifying Registry")
	}
	fmt.Println(err)
}

// TestDeriveImage ensures that the full image (tag) of the resultant OCI
// container is populated based of a derivation using configured registry
// plus the service name.
func TestDeriveImage(t *testing.T) {
	// Create the root Function directory
	root := "testdata/example.com/testDeriveImage"
	defer using(t, root)()

	// Create the function which calculates fields such as name and image.
	client := fn.New(fn.WithRegistry(TestRegistry))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Load the function with the now-populated fields.
	f, err := fn.NewFunction(root)
	if err != nil {
		t.Fatal(err)
	}

	// In form: [Default Registry]/[Registry Namespace]/[Service Name]:latest
	expected := TestRegistry + "/" + f.Name + ":latest"
	if f.Image != expected {
		t.Fatalf("expected image '%v' got '%v'", expected, f.Image)
	}
}

// TestDeriveImageDefaultRegistry ensures that a Registry which does not have
// a registry prefix has the DefaultRegistry prepended.
// For example "alice" becomes "docker.io/alice"
func TestDeriveImageDefaultRegistry(t *testing.T) {
	// Create the root Function directory
	root := "testdata/example.com/testDeriveImageDefaultRegistry"
	defer using(t, root)()

	// Create the function which calculates fields such as name and image.
	// Rather than use TestRegistry, use a single-token name and expect
	// the DefaultRegistry to be prepended.
	client := fn.New(fn.WithRegistry("alice"))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Load the function with the now-populated fields.
	f, err := fn.NewFunction(root)
	if err != nil {
		t.Fatal(err)
	}

	// Expected image is [DefaultRegistry]/[namespace]/[servicename]:latest
	expected := fn.DefaultRegistry + "/alice/" + f.Name + ":latest"
	if f.Image != expected {
		t.Fatalf("expected image '%v' got '%v'", expected, f.Image)
	}
}

// TestDelegation ensures that Create invokes each of the individual
// subcomponents via delegation through Build, Push and
// Deploy (and confirms expected fields calculated).
func TestNewDelegates(t *testing.T) {
	var (
		root          = "testdata/example.com/testCreateDelegates" // .. in which to initialize
		expectedName  = "testCreateDelegates"                      // expected to be derived
		expectedImage = "quay.io/alice/testCreateDelegates:latest"
		builder       = mock.NewBuilder()
		pusher        = mock.NewPusher()
		deployer      = mock.NewDeployer()
	)

	// Create a directory for the test
	defer using(t, root)()

	// Create a client with mocks for each of the subcomponents.
	client := fn.New(
		fn.WithRegistry(TestRegistry),
		fn.WithBuilder(builder),   // builds an image
		fn.WithPusher(pusher),     // pushes images to a registry
		fn.WithDeployer(deployer), // deploys images as a running service
	)

	// Register Function delegates on the mocks which validate assertions
	// -------------

	// The builder should be invoked with a path to a Function project's source
	// An example image name is returned.
	builder.BuildFn = func(f fn.Function) error {
		expectedPath, err := filepath.Abs(root)
		if err != nil {
			t.Fatal(err)
		}
		if expectedPath != f.Root {
			t.Fatalf("builder expected path %v, got '%v'", expectedPath, f.Root)
		}
		return nil
	}

	pusher.PushFn = func(f fn.Function) (string, error) {
		if f.Image != expectedImage {
			t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Image)
		}
		return "", nil
	}

	deployer.DeployFn = func(f fn.Function) error {
		if f.Name != expectedName {
			t.Fatalf("deployer expected name '%v', got '%v'", expectedName, f.Name)
		}
		if f.Image != expectedImage {
			t.Fatalf("deployer expected image '%v', got '%v'", expectedImage, f.Image)
		}
		return nil
	}

	// Invocation
	// -------------

	// Invoke the creation, triggering the Function delegates, and
	// perform follow-up assertions that the Functions were indeed invoked.
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Confirm that each delegate was invoked.
	if !builder.BuildInvoked {
		t.Fatal("builder was not invoked")
	}
	if !pusher.PushInvoked {
		t.Fatal("pusher was not invoked")
	}
	if !deployer.DeployInvoked {
		t.Fatal("deployer was not invoked")
	}
}

// TestRun ensures that the runner is invoked with the absolute path requested.
func TestRun(t *testing.T) {
	// Create the root Function directory
	root := "testdata/example.com/testRun"
	defer using(t, root)()

	// Create a client with the mock runner and the new test Function
	runner := mock.NewRunner()
	client := fn.New(fn.WithRegistry(TestRegistry), fn.WithRunner(runner))
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Run the newly created function
	if err := client.Run(context.Background(), root); err != nil {
		t.Fatal(err)
	}

	// Assert the runner was invoked, and with the expected root.
	if !runner.RunInvoked {
		t.Fatal("run did not invoke the runner")
	}
	absRoot, err := filepath.Abs(root)
	if err != nil {
		t.Fatal(err)
	}
	if runner.RootRequested != absRoot {
		t.Fatalf("expected path '%v', got '%v'", absRoot, runner.RootRequested)
	}
}

// TestUpdate ensures that the deployer properly invokes the build/push/deploy
// process, erroring if run on a directory uncreated.
func TestUpdate(t *testing.T) {
	var (
		root          = "testdata/example.com/testUpdate"
		expectedName  = "testUpdate"
		expectedImage = "quay.io/alice/testUpdate:latest"
		builder       = mock.NewBuilder()
		pusher        = mock.NewPusher()
		deployer      = mock.NewDeployer()
	)

	// Create the root Function directory
	defer using(t, root)()

	// A client with mocks whose implementaton will validate input.
	client := fn.New(
		fn.WithRegistry(TestRegistry),
		fn.WithBuilder(builder),
		fn.WithPusher(pusher),
		fn.WithDeployer(deployer))

	// create the new Function which will be updated
	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Builder whose implementation verifies the expected root
	builder.BuildFn = func(f fn.Function) error {
		rootPath, err := filepath.Abs(root)
		if err != nil {
			t.Fatal(err)
		}
		if f.Root != rootPath {
			t.Fatalf("builder expected path %v, got '%v'", rootPath, f.Root)
		}
		return nil
	}

	// Pusher whose implementaiton verifies the expected image
	pusher.PushFn = func(f fn.Function) (string, error) {
		if f.Image != expectedImage {
			t.Fatalf("pusher expected image '%v', got '%v'", expectedImage, f.Image)
		}
		// image of given name wouold be pushed to the configured registry.
		return "", nil
	}

	// Update whose implementaiton verifed the expected name and image
	deployer.DeployFn = func(f fn.Function) error {
		if f.Name != expectedName {
			t.Fatalf("updater expected name '%v', got '%v'", expectedName, f.Name)
		}
		if f.Image != expectedImage {
			t.Fatalf("updater expected image '%v', got '%v'", expectedImage, f.Image)
		}
		return nil
	}

	// Invoke the creation, triggering the Function delegates, and
	// perform follow-up assertions that the Functions were indeed invoked.
	if err := client.Deploy(context.Background(), root); err != nil {
		t.Fatal(err)
	}

	if !builder.BuildInvoked {
		t.Fatal("builder was not invoked")
	}
	if !pusher.PushInvoked {
		t.Fatal("pusher was not invoked")
	}
	if !deployer.DeployInvoked {
		t.Fatal("deployer was not invoked")
	}
}

// TestRemoveByPath ensures that the remover is invoked to remove
// the Function with the name of the function at the provided root.
func TestRemoveByPath(t *testing.T) {
	var (
		root         = "testdata/example.com/testRemoveByPath"
		expectedName = "testRemoveByPath"
		remover      = mock.NewRemover()
	)

	defer using(t, root)()

	client := fn.New(
		fn.WithRegistry(TestRegistry),
		fn.WithRemover(remover))

	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	remover.RemoveFn = func(name string) error {
		if name != expectedName {
			t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
		}
		return nil
	}

	if err := client.Remove(context.Background(), fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	if !remover.RemoveInvoked {
		t.Fatal("remover was not invoked")
	}

}

// TestRemoveByName ensures that the remover is invoked to remove the function
// of the name provided, with precidence over a provided root path.
func TestRemoveByName(t *testing.T) {
	var (
		root         = "testdata/example.com/testRemoveByPath"
		expectedName = "explicitName.example.com"
		remover      = mock.NewRemover()
	)

	defer using(t, root)()

	client := fn.New(
		fn.WithRegistry(TestRegistry),
		fn.WithRemover(remover))

	if err := client.Create(fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	remover.RemoveFn = func(name string) error {
		if name != expectedName {
			t.Fatalf("Expected to remove '%v', got '%v'", expectedName, name)
		}
		return nil
	}

	// Run remove with only a name
	if err := client.Remove(context.Background(), fn.Function{Name: expectedName}); err != nil {
		t.Fatal(err)
	}

	// Run remove with a name and a root, which should be ignored in favor of the name.
	if err := client.Remove(context.Background(), fn.Function{Name: expectedName, Root: root}); err != nil {
		t.Fatal(err)
	}

	if !remover.RemoveInvoked {
		t.Fatal("remover was not invoked")
	}
}

// TestRemoveUninitializedFails ensures that attempting to remove a Function
// by path only (no name) fails unless the Function has been initialized.  I.e.
// the name will not be derived from path and the Function removed by this
// derived name; which could be unexpected and destructive.
func TestRemoveUninitializedFails(t *testing.T) {
	var (
		root    = "testdata/example.com/testRemoveUninitializedFails"
		remover = mock.NewRemover()
	)
	defer using(t, root)()

	// remover fails if invoked
	remover.RemoveFn = func(name string) error {
		return fmt.Errorf("remove invoked for unitialized Function %v", name)
	}

	// Instantiate the client with the failing remover.
	client := fn.New(
		fn.WithRegistry(TestRegistry),
		fn.WithRemover(remover))

	// Attempt to remove by path (uninitialized), expecting an error.
	if err := client.Remove(context.Background(), fn.Function{Root: root}); err == nil {
		t.Fatalf("did not received expeced error removing an uninitialized func")
	}
}

// TestList merely ensures that the client invokes the configured lister.
func TestList(t *testing.T) {
	lister := mock.NewLister()

	client := fn.New(fn.WithLister(lister)) // lists deployed Functions.

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

	if !lister.ListInvoked {
		t.Fatal("list did not invoke lister implementation")
	}
}

// TestListOutsideRoot ensures that a call to a Function (in this case list)
// that is not contextually dependent on being associated with a Function,
// can be run from anywhere, thus ensuring that the client itself makes
// a distinction between Function-scoped methods and not.
func TestListOutsideRoot(t *testing.T) {
	lister := mock.NewLister()

	// Instantiate in the current working directory, with no name.
	client := fn.New(fn.WithLister(lister))

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

	if !lister.ListInvoked {
		t.Fatal("list did not invoke lister implementation")
	}
}

// TestDeployUnbuilt ensures that a call to deploy a Function which was not
// fully created (ie. was only initialized, not actually built and deploys)
// yields an expected, and informative, error.
func TestDeployUnbuilt(t *testing.T) {
	root := "testdata/example.com/testDeploy" // Root from which to run the test
	defer using(t, root)()

	// New Client
	client := fn.New(fn.WithRegistry(TestRegistry))

	// Initialize (half-create) a new Function at root
	if err := client.Create(fn.Function{Root: root}); err != nil {
		t.Fatal(err)
	}

	// Now try to deploy it.  Ie. without having run the necessary build step.
	err := client.Deploy(context.Background(), root)
	if err == nil {
		t.Fatal("did not receive an error attempting to deploy an unbuilt Function")
	}

	if !errors.Is(err, fn.ErrNotBuilt) {
		t.Fatalf("did not receive expected error type.  Expected ErrNotBuilt, got %T", err)
	}
}

// TestEmit ensures that the
func TestEmit(t *testing.T) {
	sink := "http://testy.mctestface.com"
	emitter := mock.NewEmitter()

	// Ensure sink passthrough from client
	emitter.EmitFn = func(s string) error {
		if s != sink {
			t.Fatalf("Unexpected sink %v\n", s)
		}
		return nil
	}

	// Instantiate in the current working directory, with no name.
	client := fn.New(fn.WithEmitter(emitter))

	if err := client.Emit(context.Background(), sink); err != nil {
		t.Fatal(err)
	}
	if !emitter.EmitInvoked {
		t.Fatal("Client did not invoke emitter.Emit()")
	}
}

// Helpers ----

// using the given directory (creating it) returns a closure which removes the
// directory, intended to be run in a defer statement.
func using(t *testing.T, root string) func() {
	t.Helper()
	mkdir(t, root)
	return func() {
		rm(t, root)
	}
}

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

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