package commands_test

import (
	"encoding/base64"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	cli_fakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/cli/fakes"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/models"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient"
	mtafake "github.com/cloudfoundry-incubator/multiapps-cli-plugin/clients/mtaclient/fakes"

	plugin_fakes "code.cloudfoundry.org/cli/plugin/pluginfakes"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/commands"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/testutil"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/ui"
	"github.com/cloudfoundry-incubator/multiapps-cli-plugin/util"
	util_fakes "github.com/cloudfoundry-incubator/multiapps-cli-plugin/util/fakes"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

type mockFile struct {
	reader io.Reader
}

type mockFileInfo struct{}

func (mockFileInfo) Name() string {
	return ""
}

func (mockFileInfo) Size() int64 {
	return 0
}

func (mockFileInfo) Mode() fs.FileMode {
	return ^fs.ModeCharDevice
}

func (mockFileInfo) ModTime() time.Time {
	return time.Time{}
}

func (mockFileInfo) IsDir() bool {
	return false
}

func (mockFileInfo) Sys() any {
	return nil
}

func (f *mockFile) Stat() (fs.FileInfo, error) {
	return mockFileInfo{}, nil
}

func (f *mockFile) Read(p []byte) (int, error) {
	return f.reader.Read(p)
}

func (f *mockFile) Close() error {
	return nil
}

var _ = Describe("DeployCommand", func() {
	Describe("Execute", func() {
		const org = "test-org"
		const space = "test-space"
		const namespace = "namespace"
		const user = "test-user"
		const testFilesLocation = "../test_resources/commands/"
		const testArchive = "mtaArchive.mtar"
		const mtaArchivePath = testFilesLocation + testArchive
		const extDescriptorPath = testFilesLocation + "extDescriptor.mtaext"

		var name string
		var cliConnection *plugin_fakes.FakeCliConnection
		// var fakeSession csrffake.FakeSessionProvider
		var mtaClient *mtafake.FakeMtaClientOperations
		// var restClient *restfake.FakeRestClientOperations
		var testClientFactory *commands.TestClientFactory
		var command *commands.DeployCommand
		var oc = testutil.NewUIOutputCapturer()
		var ex = testutil.NewUIExpector()

		var fullMtaArchivePath, _ = filepath.Abs(mtaArchivePath)
		var fullExtDescriptorPath, _ = filepath.Abs(extDescriptorPath)
		var correctMtaUrl = "https://localhost/path/anatz.mtar?query=true"
		var incorrectMtaUrl = "https://localhost"

		var newMockFileReader = func(url string) fs.File {
			return &mockFile{strings.NewReader(url)}
		}

		var getLinesForAbortingProcess = func() []string {
			return []string{
				"Executing action \"abort\" on operation test-process-id...",
				"OK",
			}
		}

		var getOutputLines = func(extDescriptor, processAborted, fromUrl bool) []string {
			var lines []string
			mtaNameToPrint := mtaArchivePath
			if fromUrl {
				mtaNameToPrint = "from url"
			}
			lines = append(lines,
				"Deploying multi-target app archive "+mtaNameToPrint+" in org "+org+" / space "+space+" as "+user+"...")
			lines = append(lines, "")
			if processAborted {
				lines = append(lines,
					"Executing action \"abort\" on operation test-process-id...",
					"OK",
				)
			}
			if fromUrl {
				lines = append(lines, "OK")
			} else {
				lines = append(lines,
					"Uploading 1 files...",
					"  "+fullMtaArchivePath,
					"OK")
			}
			if extDescriptor {
				lines = append(lines,
					"Uploading 1 files...",
					"  "+fullExtDescriptorPath,
					"OK")
			}
			lines = append(lines,
				"Test message",
				"Process finished.",
				"Use \"cf dmol -i 1000\" to download the logs of the process.")
			return lines
		}

		// var getProcessParameters = func(additional bool) map[string]string {
		// 	params := map[string]string{
		// 		"appArchiveId":   "mtaArchive.mtar",
		// 		"failOnCrashed":  "false",
		// 	}
		// 	if additional {
		// 		params["deleteServices"] = "true"
		// 		params["keepFiles"] = "true"
		// 		params["noStart"] = "true"
		// 	}
		// 	return params
		// }

		var getFile = func(path string) (*os.File, *models.FileMetadata) {
			file, _ := os.Open(path)
			digest, _ := util.ComputeFileChecksum(path, "MD5")
			f := testutil.GetFile(file, strings.ToUpper(digest), namespace)
			return file, f
		}

		// var expectProcessParameters = func(expectedParameters map[string]string, processParameters map[string]interface{}) {
		// 	for processParam, processParamValue := range processParameters {
		// 		if expectedParameters[processParam] != "" {
		// 			Expect(processParamValue).To(Equal(expectedParameters[processParamValue.(string)]))
		// 		}
		// 	}
		// }

		BeforeEach(func() {
			ui.DisableTerminalOutput(true)
			name = command.GetPluginCommand().Name
			cliConnection = cli_fakes.NewFakeCliConnectionBuilder().
				CurrentOrg("test-org-guid", org, nil).
				CurrentSpace("test-space-guid", space, nil).
				Username(user, nil).
				AccessToken("bearer test-token", nil).Build()
			mtaArchiveFile, mtaArchive := getFile(mtaArchivePath)
			defer mtaArchiveFile.Close()
			extDescriptorFile, extDescriptor := getFile(extDescriptorPath)
			defer extDescriptorFile.Close()
			fileUploadJobId := make(http.Header)
			jobId := "one"
			fileUploadJobId.Add("Location", jobId)
			jobResult := mtaclient.AsyncUploadJobResult{
				File:  mtaArchive,
				MtaId: "anatz",
			}
			mtaClient = mtafake.NewFakeMtaClientBuilder().
				GetMtaFiles([]*models.FileMetadata{&testutil.SimpleFile}, nil).
				UploadMtaFile(mtaArchiveFile, mtaArchive, nil).
				UploadMtaFile(extDescriptorFile, extDescriptor, nil).
				StartUploadMtaArchiveFromUrl(base64.URLEncoding.EncodeToString([]byte(correctMtaUrl)), nil, fileUploadJobId, nil).
				StartUploadMtaArchiveFromUrl(base64.URLEncoding.EncodeToString([]byte(incorrectMtaUrl)), nil, nil, fmt.Errorf("connection refused")).
				GetAsyncUploadJob(jobId, nil, "", jobResult, nil).
				StartMtaOperation(testutil.OperationResult, mtaclient.ResponseHeader{Location: "operations/1000?embed=messages"}, nil).
				GetMtaOperation("1000", "messages", &testutil.OperationResult, nil).
				GetMtaOperationLogContent("1000", testutil.LogID, testutil.LogContent, nil).
				GetMtaOperations(nil, nil, nil, []*models.Operation{&testutil.OperationResult}, nil).Build()
			testClientFactory = commands.NewTestClientFactory(mtaClient, nil, nil)
			command = commands.NewDeployCommand()
			testTokenFactory := commands.NewTestTokenFactory(cliConnection)
			deployServiceURLCalculator := util_fakes.NewDeployServiceURLFakeCalculator("deploy-service.test.ondemand.com")
			command.InitializeAll(name, cliConnection, testutil.NewCustomTransport(200), testClientFactory, testTokenFactory, deployServiceURLCalculator)
			command.FileUrlReadTimeout = time.Second
		})

		// with argument that is not a directory or MTA and a valid and unknown flag (unknown flag - error)
		Context("with argument that is not a directory or MTA and a valid and unknown flag", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"x", "-l"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: -l")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// with argument that is a directory or MTA and with unknown flag (unknown flag - error)
		Context("with argument that is a directory or MTA and with unknown flag", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "-l"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: -l")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// with argument that is a directory or MTA and with unknown flags - some even duplicated (unknown flag - error)
		Context("with argument that is a directory or MTA and with unknown flags - some even duplicated", func() {
			It("should print incorrect usage (and print the duplicating flags only once), call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "-nonValidFlag", "-u", "-nonValidFlag", "-nonValidFlag", "--flagInvalid"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: -nonValidFlag, --flagInvalid")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		Context("with a correct URL argument", func() {
			It("should upload the MTAR from the correct URL and initiate a deploy", func() {
				command.FileUrlReader = newMockFileReader(correctMtaUrl)
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, true))
			})
		})

		Context("with an incorrect URL argument", func() {
			It("should fail with the error returned from the server", func() {
				command.FileUrlReader = newMockFileReader(incorrectMtaUrl)
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{}).ToInt()
				})
				ex.ExpectFailureOnLine(status, output, "Could not upload from url: connection refused", 3)
			})
		})

		// wrong arguments - error
		Context("with wrong arguments", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"x", "y", "z"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Wrong arguments")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// non-existing MTA archive - error
		Context("with a non-existing mta archive", func() {
			It("should print a file not found error and exit with a non-zero status", func() {
				const fileName = "non-existing-mtar.mtar"
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{fileName}).ToInt()
				})
				ex.ExpectFailure(status, output, "Could not find MTA "+fileName)
			})
		})

		// strategy flag set to "" - error
		Context("with strategy flag set to blank string", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--strategy"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Parsing of arguments has failed")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// more than one unknown flag and one valid flag, which is a boolean, but its value is missing
		Context("with argument that is a directory or MTA and with more than one unknown flag and one valid flag, which is a boolean, but its value is missing", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--nonValidFlag", "--apply-namespace-app-names", "-notAValidOne", "-not-a-valid-one"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: --nonValidFlag, -notAValidOne, -not-a-valid-one")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// more than one unknown flag and one valid flag, which is a boolean and its value is set
		Context("with argument that is a directory or MTA and with more than one unknown flag and one valid flag, which is a boolean and its value is set", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--nonValidFlag", "--apply-namespace-app-names", "true", "-notAValidOne", "-not-a-valid-one"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: --nonValidFlag, -notAValidOne, -not-a-valid-one")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// more than one unknown flag and one valid flag that is not boolean and has a value set
		Context("with argument that is a directory or MTA and with more than one unknown flag and one valid flag that is not boolean and has a value set", func() {
			It("should print incorrect usage, call cf help, and exit with a non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--nonValidFlag", "--apps-stage-timeout", "8000", "-notAValidOne", "-inavalid-flag"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Incorrect usage. Unknown or wrong flags: --nonValidFlag, -notAValidOne, -inavalid-flag")
				Expect(cliConnection.CliCommandArgsForCall(0)).To(Equal([]string{"help", name}))
			})
		})

		// strategy flag set to invalid deployment strategy - error
		Context("with strategy flag set to an invalid deployment strategy", func() {
			It("should print the available strategies and exit with a non-zero status", func() {
				invalidStrategy := "asd"
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--strategy", invalidStrategy}).ToInt()
				})
				message := fmt.Sprintf("%s is not a valid deployment strategy, available strategies: %v", invalidStrategy, commands.AvailableStrategies())
				ex.ExpectFailure(status, output, message)
			})
		})

		// TODO: can't connect to backend - error

		// TODO: backend returns an an error response - error

		// existing MTA archive - success
		Context("with an existing mta archive", func() {
			It("should upload 1 file and start the deployment process", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
				// operation := mtaClient.StartMtaOperationArgsForCall(1)
				// expectProcessParameters(getProcessParameters(false), operation.Parameters)
			})
		})

		// existing MTA archive and an extension descriptor - success
		Context("with an existing mta archive and an extension descriptor", func() {
			It("should upload 2 files and start the deployment process", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "-e", extDescriptorPath}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getOutputLines(true, false, false))
				// operation := mtaClient.StartMtaOperationArgsForCall(1)
				// expectProcessParameters(getProcessParameters(false), operation.Parameters)
			})
		})

		// existing MTA archive and additional options - success
		Context("with an existing mta archive and some options", func() {
			It("should upload 1 file and start the deployment process", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "-f", "-delete-services", "-no-start", "-keep-files", "-do-not-fail-on-missing-permissions"}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
				// operation := mtaClient.StartMtaOperationArgsForCall(1)
				// expectProcessParameters(getProcessParameters(true), operation.Parameters)
			})
		})

		// non-existing ongoing operations - success
		// Context("with correct mta id from archive and no ongoing operations", func() {
		// 	It("should not try to abort confliction operations", func() {
		// 		testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
		// 			GetMtaOperations(nil, nil, []*models.Operation{}, nil).
		// 			StartMtaOperation(models.Operation{}, mtaclient.ResponseHeader{Location: "operations/1000?embed=messages"}, nil).Build()
		// 		output, status := oc.CaptureOutputAndStatus(func() int {
		// 			return command.Execute([]string{mtaArchivePath}).ToInt()
		// 		})
		// 		fmt.Println(output)
		// 		ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false))
		// 		// operation := mtaClient.StartMtaOperationArgsForCall(1)
		// 		// expectProcessParameters(getProcessParameters(false), operation.Parameters)
		// 	})
		// })

		Context("with namespace starting with whitespace and ongoing operations", func() {
			It("should correctly detect a conflict with ongoing operation and result in error", func() {
				conflictingOperation := testutil.GetOperation("test", "test-space-guid", "test", "test", "deploy", "ERROR", true)
				testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
					GetMtaOperations(nil, nil, nil, []*models.Operation{conflictingOperation}, nil).Build()
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath, "--namespace", "    test   "}).ToInt()
				})
				ex.ExpectFailureOnLine(status, output, "Deploy cancelled", 2)
			})
		})

		// existing ongoing operations and force option not supplied - success
		Context("with correct mta id from archive, with ongoing operations provided and no force option", func() {
			It("should not try to abort conflicting operations", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, false, false))
				// operation := mtaClient.StartMtaOperationArgsForCall(1)
				// expectProcessParameters(getProcessParameters(false), operation.Parameters)
			})
		})

		// existing ongoing operations and force option supplied - success
		// Context("with correct mta id from archive, with ongoing operations provided and with force option", func() {
		// 	It("should try to abort confliction operations", func() {
		// 		testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
		// 			GetMtaOperations(nil, nil, []*models.Operation{testutil.GetOperation("process-id", "test-space-guid", "test", "deploy", "ERROR", true)}, nil).Build()
		// 		output, status := oc.CaptureOutputAndStatus(func() int {
		// 			return command.Execute([]string{mtaArchivePath, "-f"}).ToInt()
		// 		})
		// 		ex.ExpectSuccessWithOutput(status, output, getOutputLines(false, true))
		// 		// operation := mtaClient.StartMtaOperationArgsForCall(1)
		// 		// expectProcessParameters(getProcessParameters(false), operation.Parameters)
		// 	})
		// })
		Context("with an error returned from getting ongoing operations", func() {
			It("should display error and exit with non-zero status", func() {
				testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
					GetMtaOperations(nil, nil, nil, []*models.Operation{}, fmt.Errorf("test-error-from backend")).Build()
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{mtaArchivePath}).ToInt()
				})
				ex.ExpectFailureOnLine(status, output, "Could not get ongoing operation", 3)
			})
		})

		Context("with non-valid operation id and action id provided", func() {
			It("should return error and exit with non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"-i", "test", "-a", "abort"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Multi-target app operation with ID test not found")
			})
		})
		Context("with valid operation id and non-valid action id provided", func() {
			It("should return error and exit with non-zero status", func() {
				testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
					GetMtaOperations(nil, nil, nil, []*models.Operation{
						testutil.GetOperation("test-process-id", space, "test-mta-id", namespace, "deploy", "ERROR", true),
					}, nil).Build()
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"-i", "test-process-id", "-a", "test"}).ToInt()
				})
				ex.ExpectFailure(status, output, "Invalid action test")
			})
		})
		Context("with valid operation id and no action id provided", func() {
			It("should return error and exit with non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"-i", "test-process-id"}).ToInt()
				})
				ex.ExpectFailure(status, output, "All the a i options should be specified together")
			})
		})

		Context("with valid action id and no operation id provided", func() {
			It("should return error and exit with non-zero status", func() {
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"-a", "abort"}).ToInt()
				})
				ex.ExpectFailure(status, output, "All the a i options should be specified together")
			})
		})

		Context("with valid operation id and valid action id provided", func() {
			It("should execute action on the process specified with process id and exit with zero status", func() {
				testClientFactory.MtaClient = mtafake.NewFakeMtaClientBuilder().
					GetMtaOperations(nil, nil, nil, []*models.Operation{
						testutil.GetOperation("test-process-id", space, "test-mta-id", namespace, "deploy", "ERROR", true),
					}, nil).
					GetOperationActions("test", []string{"abort", "retry"}, nil).
					ExecuteAction("test-process-id", "test", mtaclient.ResponseHeader{Location: ""}, nil).Build()
				output, status := oc.CaptureOutputAndStatus(func() int {
					return command.Execute([]string{"-i", "test-process-id", "-a", "abort"}).ToInt()
				})
				ex.ExpectSuccessWithOutput(status, output, getLinesForAbortingProcess())
			})
		})
	})
})
