// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package multi_pipeline_test

import (
	"fmt"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/aws/copilot-cli/e2e/internal/client"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("pipeline flow", func() {
	Context("set up CodeCommit repository", func() {
		It("creates the codecommit repository", func() {
			url, err := aws.CreateCodeCommitRepo(repoName)
			Expect(err).NotTo(HaveOccurred())
			repoURL = url
		})

		It("clones the repository", func() {
			endpoint := strings.TrimPrefix(repoURL, "https://")
			url := fmt.Sprintf("https://%s:%s@%s", url.PathEscape(codeCommitCreds.UserName), url.PathEscape(codeCommitCreds.Password), endpoint)

			Eventually(func() error {
				cmd := exec.Command("git", "clone", url)
				cmd.Stdout = os.Stdout
				cmd.Stderr = os.Stderr
				return cmd.Run()
			}, "60s", "5s").ShouldNot(HaveOccurred())
		})

		It("copies source code to the git repository", func() {
			cmd := exec.Command("cp", "-r", "frontend", repoName)
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})

		It("sets git config", func() {
			cmd := exec.Command("git", "config", "user.email", "e2etest@amazon.com")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())

			cmd = exec.Command("git", "config", "user.name", "e2etest")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
	})

	Context("create a new app", func() {
		It("app init succeeds", func() {
			_, err := copilot.AppInit(&client.AppInitRequest{
				AppName: appName,
			})
			Expect(err).NotTo(HaveOccurred())
		})
		It("app init creates an copilot directory and workspace file", func() {
			Expect(filepath.Join(repoName, "copilot")).Should(BeADirectory())
			Expect(filepath.Join(repoName, "copilot", ".workspace")).Should(BeAnExistingFile())
		})
		It("app ls includes new app", func() {
			Eventually(copilot.AppList, "30s", "5s").Should(ContainSubstring(appName))
		})
		It("app show includes app name", func() {
			appShowOutput, err := copilot.AppShow(appName)
			Expect(err).NotTo(HaveOccurred())
			Expect(appShowOutput.Name).To(Equal(appName))
			Expect(appShowOutput.URI).To(BeEmpty())
		})
	})

	Context("when adding a new environment", func() {
		It("test env init should succeed", func() {
			_, err := copilot.EnvInit(&client.EnvInitRequest{
				AppName: appName,
				EnvName: "test",
				Profile: "test",
			})
			Expect(err).NotTo(HaveOccurred())
		})
		It("prod env init should succeed", func() {
			_, err := copilot.EnvInit(&client.EnvInitRequest{
				AppName: appName,
				EnvName: "prod",
				Profile: "prod",
			})
			Expect(err).NotTo(HaveOccurred())
		})

		It("env ls should list both envs", func() {
			out, err := copilot.EnvList(appName)
			Expect(err).NotTo(HaveOccurred())
			Expect(len(out.Envs)).To(Equal(2))
			envs := map[string]client.EnvDescription{}
			for _, env := range out.Envs {
				envs[env.Name] = env
				Expect(env.ExecutionRole).NotTo(BeEmpty())
				Expect(env.ManagerRole).NotTo(BeEmpty())
			}

			Expect(envs["test"]).NotTo(BeNil())
			Expect(envs["prod"]).NotTo(BeNil())

			// Make sure, for the sake of coverage, these are cross account,
			// cross region environments if we're not doing a dryrun.
			if os.Getenv("DRYRUN") != "true" {
				Expect(envs["test"].Region).NotTo(Equal(envs["prod"].Region))
				Expect(envs["test"].Account).NotTo(Equal(envs["prod"].Account))
			}
		})
	})

	Context("when deploying the environments", func() {
		It("test env deploy should succeed", func() {
			_, err := copilot.EnvDeploy(&client.EnvDeployRequest{
				AppName: appName,
				Name:    "test",
			})
			Expect(err).NotTo(HaveOccurred())
		})
		It("prod env deploy should succeed", func() {
			_, err := copilot.EnvDeploy(&client.EnvDeployRequest{
				AppName: appName,
				Name:    "prod",
			})
			Expect(err).NotTo(HaveOccurred())
		})
	})

	Context("when creating the frontend service", func() {
		It("should initialize the service", func() {
			_, err := copilot.SvcInit(&client.SvcInitRequest{
				Name:       "frontend",
				SvcType:    "Load Balanced Web Service",
				Dockerfile: "./frontend/Dockerfile",
				SvcPort:    "80",
			})
			Expect(err).NotTo(HaveOccurred())
		})
		It("should generate a manifest file", func() {
			Expect(filepath.Join(repoName, "copilot", "frontend", "manifest.yml")).Should(BeAnExistingFile())
		})
		It("creates a new addons dir", func() {
			cmd := exec.Command("mkdir", "-p", filepath.Join(repoName, "copilot", "frontend", "addons"))
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("copies a template to the addons file", func() {
			cmd := exec.Command("cp", "s3template.yml", filepath.Join(repoName, "copilot", "frontend", "addons", "e2e-pipeline-addon.yml"))
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("should list the service", func() {
			out, err := copilot.SvcList(appName)
			Expect(err).NotTo(HaveOccurred())
			Expect(len(out.Services)).To(Equal(1))
			Expect(out.Services[0].Name).To(Equal("frontend"))
		})
	})

	Context("when creating the test pipeline manifest", func() {
		It("creates a 'test' git branch", func() {
			cmd := exec.Command("git", "checkout", "-b", "test")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("should initialize the pipeline", func() {
			_, err := copilot.PipelineInit(client.PipelineInitInput{
				Name:         testPipelineName,
				URL:          repoURL,
				GitBranch:    "test",
				Environments: []string{"test"},
				Type:         "Workloads",
			})
			Expect(err).NotTo(HaveOccurred())
		})
		It("should generate pipeline artifacts", func() {
			Expect(filepath.Join(repoName, "copilot", "pipelines", testPipelineName, "manifest.yml")).Should(BeAnExistingFile())
			Expect(filepath.Join(repoName, "copilot", "pipelines", testPipelineName, "buildspec.yml")).Should(BeAnExistingFile())
		})
		It("should push repo changes upstream", func() {
			cmd := exec.Command("git", "add", ".")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())

			cmd = exec.Command("git", "commit", "-m", "first commit")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())

			cmd = exec.Command("git", "push", "--set-upstream", "origin", "test")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
	})

	Context("when creating the prod pipeline manifest", func() {
		It("creates a 'prod' git branch", func() {
			cmd := exec.Command("git", "checkout", "-b", "prod")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("should initialize the pipeline", func() {
			_, err := copilot.PipelineInit(client.PipelineInitInput{
				Name:         prodPipelineName,
				URL:          repoURL,
				GitBranch:    "prod",
				Environments: []string{"prod"},
				Type:         "Workloads",
			})
			Expect(err).NotTo(HaveOccurred())
		})

		It("should generate pipeline artifacts", func() {
			Expect(filepath.Join(repoName, "copilot", "pipelines", prodPipelineName, "manifest.yml")).Should(BeAnExistingFile())
			Expect(filepath.Join(repoName, "copilot", "pipelines", prodPipelineName, "buildspec.yml")).Should(BeAnExistingFile())
		})

		It("should push repo changes upstream", func() {
			cmd := exec.Command("git", "add", ".")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())

			cmd = exec.Command("git", "commit", "-m", "first commit")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())

			cmd = exec.Command("git", "push", "--set-upstream", "origin", "prod")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
	})

	Context("when creating the test pipeline stack", func() {
		It("checks out the test git branch", func() {
			cmd := exec.Command("git", "checkout", "test")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("should start creating the pipeline stack", func() {
			_, err := copilot.PipelineDeploy(client.PipelineDeployInput{
				Name: testPipelineName,
			})
			Expect(err).NotTo(HaveOccurred())
		})

		It("should show test pipeline details once the stack is created", func() {
			type stage struct {
				Name     string
				Category string
			}
			wantedStages := []stage{
				{
					Name:     "Source",
					Category: "Source",
				},
				{
					Name:     "Build",
					Category: "Build",
				},
				{
					Name:     "DeployTo-test",
					Category: "Deploy",
				},
			}

			Eventually(func() error {
				out, err := copilot.PipelineShow(client.PipelineShowInput{
					Name: testPipelineName,
				})
				switch {
				case err != nil:
					return err
				case out.Name == "":
					return fmt.Errorf("pipeline name is empty: %v", out)
				case out.Name != testPipelineName:
					return fmt.Errorf("expected pipeline name %q, got %q", testPipelineName, out.Name)
				case len(out.Stages) != len(wantedStages):
					return fmt.Errorf("pipeline stages do not match: %v", out.Stages)
				}

				for idx, actualStage := range out.Stages {
					if wantedStages[idx].Name != actualStage.Name {
						return fmt.Errorf("stage name %s at index %d does not match", actualStage.Name, idx)
					}
					if wantedStages[idx].Category != actualStage.Category {
						return fmt.Errorf("stage category %s at index %d does not match", actualStage.Category, idx)
					}
				}

				return nil
			}, "600s", "10s").Should(BeNil())
		})

		It("should deploy the service to the test environment", func() {
			type state struct {
				Name         string
				ActionName   string
				ActionStatus string
			}
			wantedStates := []state{
				{
					Name:         "Source",
					ActionName:   fmt.Sprintf("SourceCodeFor-%s", appName),
					ActionStatus: "Succeeded",
				},
				{
					Name:         "Build",
					ActionName:   "Build",
					ActionStatus: "Succeeded",
				},
				{
					Name:         "DeployTo-test",
					ActionName:   "CreateOrUpdate-frontend-test",
					ActionStatus: "Succeeded",
				},
			}

			Eventually(func() error {
				out, err := copilot.PipelineStatus(client.PipelineStatusInput{
					Name: testPipelineName,
				})
				if err != nil {
					return err
				}
				if len(wantedStates) != len(out.States) {
					return fmt.Errorf("len of pipeline states do not match: %v", out.States)
				}
				for idx, actualState := range out.States {
					if wantedStates[idx].Name != actualState.Name {
						return fmt.Errorf("state name %s at index %d does not match", actualState.Name, idx)
					}
					if len(actualState.Actions) != 1 {
						return fmt.Errorf("no action yet for state name %s", actualState.Name)
					}
					if wantedStates[idx].ActionName != actualState.Actions[0].Name {
						return fmt.Errorf("action name %v for state %s does not match at index %d", actualState.Actions[0], actualState.Name, idx)
					}
					if wantedStates[idx].ActionStatus != actualState.Actions[0].Status {
						return fmt.Errorf("action status %v for state %s does not match at index %d", actualState.Actions[0], actualState.Name, idx)
					}
				}
				return nil
			}, "1200s", "15s").Should(BeNil())
		})
	})

	Context("test pipeline service should be queryable post-release", func() {
		It("service should include a valid URL", func() {
			out, err := copilot.SvcShow(&client.SvcShowRequest{
				AppName: appName,
				Name:    "frontend",
			})
			Expect(err).NotTo(HaveOccurred())

			routes := make(map[string]string)
			for _, route := range out.Routes {
				routes[route.Environment] = route.URL
			}
			for _, env := range []string{"test"} {
				Eventually(func() (int, error) {
					resp, fetchErr := http.Get(routes[env])
					return resp.StatusCode, fetchErr
				}, "30s", "1s").Should(Equal(200))
			}
			// Check that the addons stack was created.
			Eventually(func() error {
				for _, variable := range out.Variables {
					if variable.Name == "E2EPIPELINEADDON_NAME" {
						return nil
					}
				}
				return fmt.Errorf("addons variable %s not found", "E2EPIPELINEADDON_NAME")
			})
		})
	})
	Context("when creating the prod pipeline stack", func() {
		It("checks out the prod git branch", func() {
			cmd := exec.Command("git", "checkout", "prod")
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
			cmd.Dir = repoName
			Expect(cmd.Run()).NotTo(HaveOccurred())
		})
		It("should start creating the pipeline stack", func() {
			_, err := copilot.PipelineDeploy(client.PipelineDeployInput{
				Name: prodPipelineName,
			})
			Expect(err).NotTo(HaveOccurred())
		})

		It("should show prod pipeline details once the stack is created", func() {
			type stage struct {
				Name     string
				Category string
			}
			wantedStages := []stage{
				{
					Name:     "Source",
					Category: "Source",
				},
				{
					Name:     "Build",
					Category: "Build",
				},
				{
					Name:     "DeployTo-prod",
					Category: "Deploy",
				},
			}

			Eventually(func() error {
				out, err := copilot.PipelineShow(client.PipelineShowInput{
					Name: prodPipelineName,
				})
				switch {
				case err != nil:
					return err
				case out.Name == "":
					return fmt.Errorf("pipeline name is empty: %v", out)
				case out.Name != prodPipelineName:
					return fmt.Errorf("expected pipeline name %q, got %q", prodPipelineName, out.Name)
				case len(out.Stages) != len(wantedStages):
					return fmt.Errorf("pipeline stages do not match: %v", out.Stages)
				}

				for idx, actualStage := range out.Stages {
					if wantedStages[idx].Name != actualStage.Name {
						return fmt.Errorf("stage name %s at index %d does not match", actualStage.Name, idx)
					}
					if wantedStages[idx].Category != actualStage.Category {
						return fmt.Errorf("stage category %s at index %d does not match", actualStage.Category, idx)
					}
				}
				return nil
			}, "600s", "10s").Should(BeNil())
		})

		It("should deploy the service to the prod environment", func() {
			type state struct {
				Name         string
				ActionName   string
				ActionStatus string
			}
			wantedStates := []state{
				{
					Name:         "Source",
					ActionName:   fmt.Sprintf("SourceCodeFor-%s", appName),
					ActionStatus: "Succeeded",
				},
				{
					Name:         "Build",
					ActionName:   "Build",
					ActionStatus: "Succeeded",
				},
				{
					Name:         "DeployTo-prod",
					ActionName:   "CreateOrUpdate-frontend-prod",
					ActionStatus: "Succeeded",
				},
			}

			Eventually(func() error {
				out, err := copilot.PipelineStatus(client.PipelineStatusInput{
					Name: prodPipelineName,
				})
				if err != nil {
					return err
				}
				if len(wantedStates) != len(out.States) {
					return fmt.Errorf("len of pipeline states do not match: %v", out.States)
				}
				for idx, actualState := range out.States {
					if wantedStates[idx].Name != actualState.Name {
						return fmt.Errorf("state name %s at index %d does not match", actualState.Name, idx)
					}
					if len(actualState.Actions) != 1 {
						return fmt.Errorf("no action yet for state name %s", actualState.Name)
					}
					if wantedStates[idx].ActionName != actualState.Actions[0].Name {
						return fmt.Errorf("action name %v for state %s does not match at index %d", actualState.Actions[0], actualState.Name, idx)
					}
					if wantedStates[idx].ActionStatus != actualState.Actions[0].Status {
						return fmt.Errorf("action status %v for state %s does not match at index %d", actualState.Actions[0], actualState.Name, idx)
					}
				}
				return nil
			}, "1200s", "15s").Should(BeNil())
		})
	})

	Context("prod pipeline service should be queryable post-release", func() {
		It("service should include a valid URL", func() {
			out, err := copilot.SvcShow(&client.SvcShowRequest{
				AppName: appName,
				Name:    "frontend",
			})
			Expect(err).NotTo(HaveOccurred())

			routes := make(map[string]string)
			for _, route := range out.Routes {
				routes[route.Environment] = route.URL
			}
			for _, env := range []string{"prod"} {
				Eventually(func() (int, error) {
					resp, fetchErr := http.Get(routes[env])
					return resp.StatusCode, fetchErr
				}, "30s", "1s").Should(Equal(200))
			}
			// Check that the addons stack was created.
			Eventually(func() error {
				for _, variable := range out.Variables {
					if variable.Name == "E2EPIPELINEADDON_NAME" {
						return nil
					}
				}
				return fmt.Errorf("addons variable %s not found", "E2EPIPELINEADDON_NAME")
			})
		})
	})
})
