package cmd

import (
	"context"
	"os"
	"testing"

	fn "knative.dev/func/pkg/functions"
	"knative.dev/func/pkg/mock"
	. "knative.dev/func/pkg/testing"
)

// TestDelete_Default ensures that the deployed function is deleted correctly
// with default options and the default situation: running "delete" from
// within the same directory of the function which is to be deleted.
func TestDelete_Default(t *testing.T) {
	var (
		err       error
		root      = FromTempDirectory(t)
		name      = "myfunc"
		namespace = "testns"
		remover   = mock.NewRemover()
		ctx       = context.Background()
	)

	// Remover which confirms the name and namespace received are those
	// originally requested via the CLI flags.
	remover.RemoveFn = func(n, ns string) error {
		if n != name {
			t.Errorf("expected name '%v', got '%v'", name, n)
		}
		if ns != namespace {
			t.Errorf("expected namespace '%v', got '%v'", namespace, ns)
		}
		return nil
	}

	// A function which will be created in the requested namespace
	f := fn.Function{
		Runtime:   "go",
		Name:      name,
		Namespace: namespace,
		Root:      root,
		Registry:  TestRegistry,
	}

	if _, f, err = fn.New().New(ctx, f); err != nil {
		t.Fatal(err)
	}
	if err = f.Write(); err != nil {
		t.Fatal(err)
	}

	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
	cmd.SetArgs([]string{})
	if err := cmd.Execute(); err != nil {
		t.Fatal(err)
	}

	// Fail if remover's .Remove not invoked at all
	if !remover.RemoveInvoked {
		t.Fatal("fn.Remover not invoked")
	}
}

// TestDelete_ByName ensures that running delete specifying the name of the
// function explicitly as an argument invokes the remover appropriately.
func TestDelete_ByName(t *testing.T) {
	var (
		root          = FromTempDirectory(t)
		testname      = "testname"        // explicit name for the function
		testnamespace = "testnamespace"   // explicit namespace for the function
		remover       = mock.NewRemover() // with a mock remover
		err           error
	)

	// Remover fails the test if it receives the incorrect name
	remover.RemoveFn = func(n, _ string) error {
		if n != testname {
			t.Fatalf("expected delete name %v, got %v", testname, n)
		}
		return nil
	}

	f := fn.Function{
		Root:     root,
		Runtime:  "go",
		Registry: TestRegistry,
		Name:     "testname",
	}

	if f, err = fn.New().Init(f); err != nil {
		t.Fatal(err)
	}

	// simulate deployed function in namespace for the client Remover
	f.Deploy.Namespace = testnamespace

	if err = f.Write(); err != nil {
		t.Fatal(err)
	}

	// Create a command with a client constructor fn that instantiates a client
	// with a mocked remover.
	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
	cmd.SetArgs([]string{testname}) // run: func delete <name>
	if err := cmd.Execute(); err != nil {
		t.Fatal(err)
	}

	// Fail if remover's .Remove not invoked at all
	if !remover.RemoveInvoked {
		t.Fatal("fn.Remover not invoked")
	}
}

// TestDelete_Namespace ensures that remover is envoked when --namespace flag is
// given --> func delete myfunc --namespace myns
func TestDelete_Namespace(t *testing.T) {
	var (
		namespace = "myns"
		remover   = mock.NewRemover()
		testname  = "testname"
	)

	remover.RemoveFn = func(_, ns string) error {
		if ns != namespace {
			t.Fatalf("expected delete namespace '%v', got '%v'", namespace, ns)
		}
		return nil
	}

	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
	cmd.SetArgs([]string{testname, "--namespace", namespace})
	if err := cmd.Execute(); err != nil {
		t.Fatal(err)
	}

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

// TestDelete_NamespaceFlagPriority ensures that even thought there is
// a deployed function the namespace flag takes precedence and essentially
// ignores the the function on disk
func TestDelete_NamespaceFlagPriority(t *testing.T) {
	var (
		root       = FromTempDirectory(t)
		namespace  = "myns"
		namespace2 = "myns2"
		remover    = mock.NewRemover()
		testname   = "testname"
		err        error
	)

	remover.RemoveFn = func(_, ns string) error {
		if ns != namespace2 {
			t.Fatalf("expected delete namespace '%v', got '%v'", namespace2, ns)
		}
		return nil
	}

	// Ensure the extant function's namespace is used
	f := fn.Function{
		Name:      testname,
		Root:      root,
		Runtime:   "go",
		Registry:  TestRegistry,
		Namespace: namespace,
	}
	client := fn.New()
	_, _, err = client.New(context.Background(), f)
	if err != nil {
		t.Fatal(err)
	}

	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
	cmd.SetArgs([]string{testname, "--namespace", namespace2})
	if err := cmd.Execute(); err != nil {
		t.Fatal(err)
	}

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

// TestDelete_NamespaceWithoutNameFails ensures that providing wrong argument
// combination fails nice and fast (no name of the Function)
func TestDelete_NamespaceWithoutNameFails(t *testing.T) {
	_ = FromTempDirectory(t)

	cmd := NewDeleteCmd(NewTestClient())
	cmd.SetArgs([]string{"--namespace=myns"})
	if err := cmd.Execute(); err == nil {
		t.Fatal("invoking Delete with namespace BUT without name provided anywhere")
	}
}

// TestDelete_ByProject ensures that running delete with a valid project as its
// context invokes remove and with the correct name (reads name from func.yaml)
func TestDelete_ByProject(t *testing.T) {
	_ = FromTempDirectory(t)

	// Write a func.yaml config which specifies a name
	funcYaml := `name: bar
namespace: "func"
runtime: go
image: ""
builder: quay.io/boson/faas-go-builder
builders:
  default: quay.io/boson/faas-go-builder
envs: []
annotations: {}
labels: []
created: 2021-01-01T00:00:00+00:00
`
	if err := os.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
		t.Fatal(err)
	}

	// A mock remover which fails if the name from the func.yaml is not received.
	remover := mock.NewRemover()
	remover.RemoveFn = func(n, _ string) error {
		if n != "bar" {
			t.Fatalf("expected name 'bar', got '%v'", n)
		}
		return nil
	}

	// Command with a Client constructor that returns  client with the
	// mocked remover.
	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))
	cmd.SetArgs([]string{}) // Do not use test command args

	// Execute the command simulating no arguments.
	err := cmd.Execute()
	if err != nil {
		t.Fatal(err)
	}

	// Also fail if remover's .Remove is not invoked
	if !remover.RemoveInvoked {
		t.Fatal("fn.Remover not invoked")
	}
}

// TestDelete_ByPath ensures that providing only path deletes the Function
// successfully
func TestDelete_ByPath(t *testing.T) {
	var (

		// A mock remover which will be sampled to ensure it is not invoked.
		remover   = mock.NewRemover()
		root      = FromTempDirectory(t)
		err       error
		namespace = "func"
	)

	// Ensure the extant function's namespace is used
	f := fn.Function{
		Root:     root,
		Runtime:  "go",
		Registry: TestRegistry,
		Deploy:   fn.DeploySpec{Namespace: namespace},
	}

	// Initialize a function in temp dir
	if f, err = fn.New().Init(f); err != nil {
		t.Fatal(err)
	}
	if err = f.Write(); err != nil {
		t.Fatal(err)
	}

	// Command with a Client constructor using the mock remover.
	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))

	// Execute the command only with the path argument
	cmd.SetArgs([]string{"-p", root})
	err = cmd.Execute()
	if err != nil {
		t.Fatalf("failed with: %v", err)
	}

	// Also fail if remover's .Remove is not invoked.
	if !remover.RemoveInvoked {
		t.Fatal("fn.Remover not invoked despite valid argument")
	}
}

// TestDelete_NameAndPathExclusivity ensures that providing both a name and a
// path generates an error.
// Providing the --path (-p) flag indicates the name of the function to delete
// is to be taken from the function at the given path.  Providing the name as
// an argument as well is therefore redundant and an error.
func TestDelete_NameAndPathExclusivity(t *testing.T) {

	// A mock remover which will be sampled to ensure it is not invoked.
	remover := mock.NewRemover()

	// Command with a Client constructor using the mock remover.
	cmd := NewDeleteCmd(NewTestClient(fn.WithRemover(remover)))

	// Execute the command simulating the invalid argument combination of both
	// a path and an explicit name.
	cmd.SetArgs([]string{"-p", "./testpath", "testname"})
	err := cmd.Execute()
	if err == nil {
		// TODO should really either parse the output or use typed errors to ensure it's
		// failing for the expected reason.
		t.Fatalf("expected error on conflicting flags not received")
	}

	// Also fail if remover's .Remove is invoked.
	if remover.RemoveInvoked {
		t.Fatal("fn.Remover invoked despite invalid combination and an error")
	}
}
