// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//    http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.

package events_test

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/runatlantis/atlantis/server/events"
	"github.com/runatlantis/atlantis/server/events/command"
	"github.com/runatlantis/atlantis/server/events/models"
	"github.com/runatlantis/atlantis/server/logging"
	. "github.com/runatlantis/atlantis/testing"
)

// Strip Carriage Returns, leading and trailing spaces and replace 'dollar' with 'backtick' in the string
func normalize(s string) string {
	return strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(s, "$", "`"), "\r", ""))
}

func TestRenderErr(t *testing.T) {
	err := errors.New("err")
	cases := []struct {
		Description string
		Command     command.Name
		Error       error
		Expected    string
	}{
		{
			"apply error",
			command.Apply,
			err,
			"**Apply Error**\n```\nerr\n```",
		},
		{
			"plan error",
			command.Plan,
			err,
			"**Plan Error**\n```\nerr\n```",
		},
		{
			"policy check error",
			command.PolicyCheck,
			fmt.Errorf("some conftest error"),
			"**Policy Check Error**\n```\nsome conftest error\n```",
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		res := command.Result{
			Error: c.Error,
		}
		for _, verbose := range []bool{true, false} {
			t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
				cmd := &events.CommentCommand{
					Name:    c.Command,
					Verbose: verbose,
				}
				s := r.Render(ctx, res, cmd)
				if !verbose {
					Equals(t, normalize(c.Expected), normalize(s))
				} else {
					log := fmt.Sprintf("[INFO] %s", logText)
					Equals(t, normalize(c.Expected+
						fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log)), normalize(s))
				}
			})
		}
	}
}

func TestRenderFailure(t *testing.T) {
	cases := []struct {
		Description string
		Command     command.Name
		Failure     string
		Expected    string
	}{
		{
			"apply failure",
			command.Apply,
			"failure",
			"**Apply Failed**: failure",
		},
		{
			"plan failure",
			command.Plan,
			"failure",
			"**Plan Failed**: failure",
		},
		{
			"policy check failure",
			command.PolicyCheck,
			"failure",
			"**Policy Check Failed**: failure",
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}

	for _, c := range cases {
		res := command.Result{
			Failure: c.Failure,
		}
		for _, verbose := range []bool{true, false} {
			t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
				cmd := &events.CommentCommand{
					Name:    c.Command,
					Verbose: verbose,
				}
				s := r.Render(ctx, res, cmd)
				if !verbose {
					Equals(t, normalize(c.Expected), normalize(s))
				} else {
					log := fmt.Sprintf("[INFO] %s", logText)
					Equals(t, normalize(c.Expected+
						fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log)), normalize(s))
				}
			})
		}
	}
}

func TestRenderErrAndFailure(t *testing.T) {
	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	res := command.Result{
		Error:   errors.New("error"),
		Failure: "failure",
	}
	cmd := &events.CommentCommand{
		Name:    command.Plan,
		Verbose: false,
	}

	s := r.Render(ctx, res, cmd)
	Equals(t, "**Plan Error**\n```\nerror\n```", normalize(s))
}

func TestRenderProjectResults(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		SubCommand     string
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"no projects",
			command.Plan,
			"",
			[]command.ProjectResult{},
			models.Github,
			"Ran Plan for 0 projects:\n\n",
		},
		{
			"single successful plan",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful plan with main ahead",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						MergedAgain:     true,
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
:twisted_rightwards_arrows: Upstream was modified, a new merge was performed.

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful plan with project name",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Plan for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful policy check with multiple policy sets and project name",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								// strings.Repeat require to get wrapped result
								PolicyOutput: `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,
								Passed:       false,
								ReqApprovals: 1,
							},
							{
								PolicySetName: "policy2",
								// strings.Repeat require to get wrapped result
								PolicyOutput: "2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions",
								Passed:       true,
								ReqApprovals: 1,
							},
						},
						LockURL:   "lock-url",
						RePlanCmd: "atlantis plan -d path -w workspace",
						ApplyCmd:  "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$

#### Policy Set: $policy1$
$$$diff
FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$

#### Policy Set: $policy2$
$$$diff
2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions
$$$


#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
policy set: policy2: passed.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful policy check with project name",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								// strings.Repeat require to get wrapped result
								PolicyOutput: strings.Repeat("line\n", 13) + `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,
								Passed:       false,
								ReqApprovals: 1,
							},
						},
						LockURL:   "lock-url",
						RePlanCmd: "atlantis plan -d path -w workspace",
						ApplyCmd:  "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$

<details><summary>Show Output</summary>

#### Policy Set: $policy1$
$$$diff
line
line
line
line
line
line
line
line
line
line
line
line
line
FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$


</details>

#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
$$$
policy set: policy1: 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful import",
			command.Import,
			"",
			[]command.ProjectResult{
				{
					ImportSuccess: &models.ImportSuccess{
						Output:    "import-output",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Import for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
import-output
$$$

:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.

* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"single successful state rm",
			command.State,
			"rm",
			[]command.ProjectResult{
				{
					StateRmSuccess: &models.StateRmSuccess{
						Output:    "state-rm-output",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran State $rm$ for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
state-rm-output
$$$

:put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.

* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"single successful apply",
			command.Apply,
			"",
			[]command.ProjectResult{
				{
					ApplySuccess: "success",
					Workspace:    "workspace",
					RepoRelDir:   "path",
				},
			},
			models.Github,
			`
Ran Apply for dir: $path$ workspace: $workspace$

$$$diff
success
$$$
`,
		},
		{
			"single successful apply with project name",
			command.Apply,
			"",
			[]command.ProjectResult{
				{
					ApplySuccess: "success",
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ProjectName:  "projectname",
				},
			},
			models.Github,
			`
Ran Apply for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
success
$$$
`,
		},
		{
			"multiple successful plans",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output2",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
$$$diff
terraform-output2
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path2 -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"multiple successful policy checks",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						},
						LockURL:   "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						}, LockURL: "lock-url2",
						ApplyCmd:  "atlantis apply -d path2 -w workspace",
						RePlanCmd: "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Policy Check for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
#### Policy Set: $policy1$
$$$diff
4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
$$$


* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
#### Policy Set: $policy1$
$$$diff
4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
$$$


* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path2 -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"multiple successful applies",
			command.Apply,
			"",
			[]command.ProjectResult{
				{
					RepoRelDir:   "path",
					Workspace:    "workspace",
					ProjectName:  "projectname",
					ApplySuccess: "success",
				},
				{
					RepoRelDir:   "path2",
					Workspace:    "workspace",
					ApplySuccess: "success2",
				},
			},
			models.Github,
			`
Ran Apply for 2 projects:

1. project: $projectname$ dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
---

### 1. project: $projectname$ dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
$$$diff
success2
$$$

---
### Apply Summary

2 projects, 2 successful, 0 failed, 0 errored
`,
		},
		{
			"single errored plan",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					Error:      errors.New("error"),
					RepoRelDir: "path",
					Workspace:  "workspace",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

**Plan Error**
$$$
error
$$$
`,
		},
		{
			"single failed plan",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					RepoRelDir: "path",
					Workspace:  "workspace",
					Failure:    "failure",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

**Plan Failed**: failure
`,
		},
		{
			"successful, failed, and errored plan",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname",
					Error:       errors.New("error"),
				},
			},
			models.Github,
			`
Ran Plan for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. project: $projectname$ dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. dir: $path2$ workspace: $workspace$
**Plan Failed**: failure

---
### 3. project: $projectname$ dir: $path3$ workspace: $workspace$
**Plan Error**
$$$
error
$$$

---
### Plan Summary

3 projects, 1 with changes, 0 with no changes, 2 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"successful, failed, and errored policy check",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						}, LockURL: "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions",
								Passed:        false,
								ReqApprovals:  1,
							},
						}, LockURL: "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname",
					Error:       errors.New("error"),
				},
			},
			models.Github,
			`
Ran Policy Check for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. project: $projectname$ dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
#### Policy Set: $policy1$
$$$diff
4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
$$$


* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. dir: $path2$ workspace: $workspace$
**Policy Check Failed**: failure
#### Policy Set: $policy1$
$$$diff
4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions
$$$


#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 3. project: $projectname$ dir: $path3$ workspace: $workspace$
**Policy Check Error**
$$$
error
$$$

---
* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis approve_policies
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan
  $$$
`,
		},
		{
			"successful, failed, and errored apply",
			command.Apply,
			"",
			[]command.ProjectResult{
				{
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ApplySuccess: "success",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path3",
					Error:      errors.New("error"),
				},
			},
			models.Github,
			`
Ran Apply for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
**Apply Failed**: failure

---
### 3. dir: $path3$ workspace: $workspace$
**Apply Error**
$$$
error
$$$

---
### Apply Summary

3 projects, 1 successful, 1 failed, 1 errored
`,
		},
		{
			"successful, failed, and errored apply",
			command.Apply,
			"",
			[]command.ProjectResult{
				{
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ApplySuccess: "success",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path3",
					Error:      errors.New("error"),
				},
			},
			models.Github,
			`
Ran Apply for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
**Apply Failed**: failure

---
### 3. dir: $path3$ workspace: $workspace$
**Apply Error**
$$$
error
$$$

---
### Apply Summary

3 projects, 1 successful, 1 failed, 1 errored
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						SubName: c.SubCommand,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected+
							fmt.Sprintf("<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log)), normalize(s))
					}
				})
			}
		})
	}
}

func TestRenderProjectResultsWithQuietPolicyChecks(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		SubCommand     string
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"single successful policy check with multiple policy sets and project name",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput: `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,
								Passed:       false,
								ReqApprovals: 1,
							},
							{
								PolicySetName: "policy2",
								PolicyOutput:  "2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions",
								Passed:        true,
								ReqApprovals:  1,
							},
						},
						LockURL:   "lock-url",
						RePlanCmd: "atlantis plan -d path -w workspace",
						ApplyCmd:  "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$

#### Policy Set: $policy1$
$$$diff
FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$

#### Policy Set: $policy2$
$$$diff
2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions
$$$


#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
policy set: policy2: passed.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful policy check with project name",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								// strings.Repeat require to get wrapped result
								PolicyOutput: strings.Repeat("line\n", 13) + `FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`,
								Passed:       false,
								ReqApprovals: 1,
							},
						},
						LockURL:   "lock-url",
						RePlanCmd: "atlantis plan -d path -w workspace",
						ApplyCmd:  "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Policy Check for project: $projectname$ dir: $path$ workspace: $workspace$

<details><summary>Show Output</summary>

#### Policy Set: $policy1$
$$$diff
line
line
line
line
line
line
line
line
line
line
line
line
line
FAIL - <redacted plan file> - main - WARNING: Null Resource creation is prohibited.

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$


</details>

#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
$$$
policy set: policy1: 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
$$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"multiple successful policy checks",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						},
						LockURL:   "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						}, LockURL: "lock-url2",
						ApplyCmd:  "atlantis apply -d path2 -w workspace",
						RePlanCmd: "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Policy Check for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"successful, failed, and errored policy check",
			command.PolicyCheck,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
								Passed:        true,
							},
						}, LockURL: "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
					PolicyCheckResults: &models.PolicyCheckResults{
						PolicySetResults: []models.PolicySetResult{
							{
								PolicySetName: "policy1",
								PolicyOutput:  "4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions",
								Passed:        false,
								ReqApprovals:  1,
							},
						}, LockURL: "lock-url",
						ApplyCmd:  "atlantis apply -d path -w workspace",
						RePlanCmd: "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname",
					Error:       errors.New("error"),
				},
			},
			models.Github,
			`
Ran Policy Check for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. project: $projectname$ dir: $path3$ workspace: $workspace$
---

### 2. dir: $path2$ workspace: $workspace$
**Policy Check Failed**: failure
#### Policy Set: $policy1$
$$$diff
4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions
$$$


#### Policy Approval Status:
$$$
policy set: policy1: requires: 1 approval(s), have: 0.
$$$
* :heavy_check_mark: To **approve** this project, comment:
  $$$shell
  
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 3. project: $projectname$ dir: $path3$ workspace: $workspace$
**Policy Check Error**
$$$
error
$$$

---
* :heavy_check_mark: To **approve** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis approve_policies
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan
  $$$
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		true,       // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						SubName: c.SubCommand,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected+
							fmt.Sprintf("<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log)), normalize(s))
					}
				})
			}
		})
	}
}

// Test that if disable apply all is set then the apply all footer is not added
func TestRenderProjectResultsDisableApplyAll(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"single successful plan with disable apply all set",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"single successful plan with project name with disable apply all set",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Plan for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"multiple successful plans, disable apply all set",
			command.Plan,
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output2",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
$$$diff
terraform-output2
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path2 -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed
`,
		},
	}
	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		true,       // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected)+
							fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log), normalize(s))
					}
				})
			}
		})
	}
}

// Test that if disable apply is set then the apply footer is not added
func TestRenderProjectResultsDisableApply(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"single successful plan with disable apply set",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"single successful plan with project name with disable apply set",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Plan for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`,
		},
		{
			"multiple successful plans, disable apply set",
			command.Plan,
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output2",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
$$$diff
terraform-output2
$$$

* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url2)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		true,       // disableApplyAll
		true,       // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected)+
							fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log), normalize(s))
					}
				})
			}
		})
	}
}

// Run policy check with a custom template to validate custom template rendering.
func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) {
	var exp string
	tmpDir := t.TempDir()
	filePath := fmt.Sprintf("%s/templates.tmpl", tmpDir)
	_, err := os.Create(filePath)
	Ok(t, err)
	err = os.WriteFile(filePath, []byte("{{ define \"PolicyCheckResultsUnwrapped\" -}}somecustometext{{- end}}\n"), 0600)
	Ok(t, err)
	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		true,       // disableApplyAll
		true,       // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		tmpDir,     // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}

	res := command.Result{
		ProjectResults: []command.ProjectResult{
			{
				Workspace:  "workspace",
				RepoRelDir: "path",
				PolicyCheckResults: &models.PolicyCheckResults{
					PolicySetResults: []models.PolicySetResult{
						{
							PolicySetName: "policy1",
							PolicyOutput:  "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions",
							Passed:        true,
						},
					}, LockURL: "lock-url",
					ApplyCmd:  "atlantis apply -d path -w workspace",
					RePlanCmd: "atlantis plan -d path -w workspace",
				},
			},
		},
	}
	cmd := &events.CommentCommand{
		Name:    command.PolicyCheck,
		Verbose: false,
	}
	rendered := r.Render(ctx, res, cmd)
	exp = `
Ran Policy Check for dir: $path$ workspace: $workspace$

#### Policy Set: $policy1$
$$$diff
4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions
$$$


* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To re-run policies **plan** this project again by commenting:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
`

	Equals(t, normalize(exp), normalize(rendered))
}

// Test that if folding is disabled that it's not used.
func TestRenderProjectResults_DisableFolding(t *testing.T) {
	mr := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		true,       // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	res := command.Result{
		ProjectResults: []command.ProjectResult{
			{
				RepoRelDir: ".",
				Workspace:  "default",
				Error:      errors.New(strings.Repeat("line\n", 13)),
			},
		},
	}
	cmd := &events.CommentCommand{
		Name:    command.Plan,
		Verbose: false,
	}
	rendered := mr.Render(ctx, res, cmd)
	Equals(t, false, strings.Contains(rendered, "\n<details>"))
}

// Test that if the output is longer than 12 lines, it gets wrapped on the right
// VCS hosts during an error.
func TestRenderProjectResults_WrappedErr(t *testing.T) {
	cases := []struct {
		VCSHost                 models.VCSHostType
		GitlabCommonMarkSupport bool
		Output                  string
		ShouldWrap              bool
	}{
		{
			VCSHost:    models.Github,
			Output:     strings.Repeat("line\n", 1),
			ShouldWrap: false,
		},
		{
			VCSHost:    models.Github,
			Output:     strings.Repeat("line\n", 13),
			ShouldWrap: true,
		},
		{
			VCSHost:                 models.Gitlab,
			GitlabCommonMarkSupport: false,
			Output:                  strings.Repeat("line\n", 1),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			GitlabCommonMarkSupport: false,
			Output:                  strings.Repeat("line\n", 13),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			GitlabCommonMarkSupport: true,
			Output:                  strings.Repeat("line\n", 1),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			GitlabCommonMarkSupport: true,
			Output:                  strings.Repeat("line\n", 13),
			ShouldWrap:              true,
		},
		{
			VCSHost:    models.BitbucketCloud,
			Output:     strings.Repeat("line\n", 1),
			ShouldWrap: false,
		},
		{
			VCSHost:    models.BitbucketCloud,
			Output:     strings.Repeat("line\n", 13),
			ShouldWrap: false,
		},
		{
			VCSHost:    models.BitbucketServer,
			Output:     strings.Repeat("line\n", 1),
			ShouldWrap: false,
		},
		{
			VCSHost:    models.BitbucketServer,
			Output:     strings.Repeat("line\n", 13),
			ShouldWrap: false,
		},
	}

	for _, c := range cases {
		t.Run(fmt.Sprintf("%s_%v", c.VCSHost.String(), c.ShouldWrap),
			func(t *testing.T) {
				mr := events.NewMarkdownRenderer(
					c.GitlabCommonMarkSupport, // gitlabSupportsCommonMark
					false,                     // disableApplyAll
					false,                     // disableApply
					false,                     // disableMarkdownFolding
					false,                     // disableRepoLocking
					false,                     // enableDiffMarkdownFormat
					"",                        // markdownTemplateOverridesDir
					"atlantis",                // executableName
					false,                     // hideUnchangedPlanComments
					false,                     // quietPolicyChecks
				)
				logger := logging.NewNoopLogger(t).WithHistory()
				logText := "log"
				logger.Info(logText)
				ctx := &command.Context{
					Log: logger,
					Pull: models.PullRequest{
						BaseRepo: models.Repo{
							VCSHost: models.VCSHost{
								Type: c.VCSHost,
							},
						},
					},
				}
				res := command.Result{
					ProjectResults: []command.ProjectResult{
						{
							RepoRelDir: ".",
							Workspace:  "default",
							Error:      errors.New(c.Output),
						},
					},
				}
				cmd := &events.CommentCommand{
					Name:    command.Plan,
					Verbose: false,
				}
				rendered := mr.Render(ctx, res, cmd)
				var exp string
				if c.ShouldWrap {
					exp = `
Ran Plan for dir: $.$ workspace: $default$

**Plan Error**
<details><summary>Show Output</summary>

$$$
` + c.Output + `
$$$
</details>
`
				} else {
					exp = `Ran Plan for dir: $.$ workspace: $default$

**Plan Error**
$$$
` + c.Output + `
$$$
`
				}
				Equals(t, normalize(exp), normalize(rendered))
			})
	}
}

// Test that if the output is longer than 12 lines, it gets wrapped on the right
// VCS hosts for a single project.
func TestRenderProjectResults_WrapSingleProject(t *testing.T) {
	cases := []struct {
		VCSHost                 models.VCSHostType
		VcsRequestType          string
		GitlabCommonMarkSupport bool
		Output                  string
		ShouldWrap              bool
	}{
		{
			VCSHost:        models.Github,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 1),
			ShouldWrap:     false,
		},
		{
			VCSHost:        models.Github,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 13) + "No changes. Infrastructure is up-to-date.",
			ShouldWrap:     true,
		},
		{
			VCSHost:                 models.Gitlab,
			VcsRequestType:          "Merge Request",
			GitlabCommonMarkSupport: false,
			Output:                  strings.Repeat("line\n", 1),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			VcsRequestType:          "Merge Request",
			GitlabCommonMarkSupport: false,
			Output:                  strings.Repeat("line\n", 13),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			VcsRequestType:          "Merge Request",
			GitlabCommonMarkSupport: true,
			Output:                  strings.Repeat("line\n", 1),
			ShouldWrap:              false,
		},
		{
			VCSHost:                 models.Gitlab,
			VcsRequestType:          "Merge Request",
			GitlabCommonMarkSupport: true,
			Output:                  strings.Repeat("line\n", 13) + "No changes. Infrastructure is up-to-date.",
			ShouldWrap:              true,
		},
		{
			VCSHost:        models.BitbucketCloud,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 1),
			ShouldWrap:     false,
		},
		{
			VCSHost:        models.BitbucketCloud,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 13),
			ShouldWrap:     false,
		},
		{
			VCSHost:        models.BitbucketServer,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 1),
			ShouldWrap:     false,
		},
		{
			VCSHost:        models.BitbucketServer,
			VcsRequestType: "Pull Request",
			Output:         strings.Repeat("line\n", 13),
			ShouldWrap:     false,
		},
	}

	for _, c := range cases {
		for _, cmdName := range []command.Name{command.Plan, command.Apply} {
			t.Run(fmt.Sprintf("%s_%s_%v", c.VCSHost.String(), cmdName.String(), c.ShouldWrap),
				func(t *testing.T) {
					mr := events.NewMarkdownRenderer(
						c.GitlabCommonMarkSupport, // gitlabSupportsCommonMark
						false,                     // disableApplyAll
						false,                     // disableApply
						false,                     // disableMarkdownFolding
						false,                     // disableRepoLocking
						false,                     // enableDiffMarkdownFormat
						"",                        // markdownTemplateOverridesDir
						"atlantis",                // executableName
						false,                     // hideUnchangedPlanComments
						false,                     // quietPolicyChecks
					)
					logger := logging.NewNoopLogger(t).WithHistory()
					logText := "log"
					logger.Info(logText)
					ctx := &command.Context{
						Log: logger,
						Pull: models.PullRequest{
							BaseRepo: models.Repo{
								VCSHost: models.VCSHost{
									Type: c.VCSHost,
								},
							},
						},
					}

					var pr command.ProjectResult
					switch cmdName {
					case command.Plan:
						pr = command.ProjectResult{
							RepoRelDir: ".",
							Workspace:  "default",
							PlanSuccess: &models.PlanSuccess{
								TerraformOutput: c.Output,
								LockURL:         "lock-url",
								RePlanCmd:       "replancmd",
								ApplyCmd:        "applycmd",
							},
						}
					case command.Apply:
						pr = command.ProjectResult{
							RepoRelDir:   ".",
							Workspace:    "default",
							ApplySuccess: c.Output,
						}
					}
					res := command.Result{
						ProjectResults: []command.ProjectResult{pr},
					}
					cmd := &events.CommentCommand{
						Name:    cmdName,
						Verbose: false,
					}
					rendered := mr.Render(ctx, res, cmd)

					// Check result.
					var exp string
					switch cmdName {
					case command.Plan:
						if c.ShouldWrap {
							exp = `
Ran Plan for dir: $.$ workspace: $default$

<details><summary>Show Output</summary>

$$$diff
` + strings.TrimSpace(c.Output) + `
$$$
</details>

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  applycmd
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  replancmd
  $$$
No changes. Infrastructure is up-to-date.

---
* :fast_forward: To **apply** all unapplied plans from this ` + c.VcsRequestType + `, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this ` + c.VcsRequestType + `, comment:
  $$$shell
  atlantis unlock
  $$$
`
						} else {
							exp = `
Ran Plan for dir: $.$ workspace: $default$

$$$diff
` + strings.TrimSpace(c.Output) + `
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  applycmd
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  replancmd
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this ` + c.VcsRequestType + `, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this ` + c.VcsRequestType + `, comment:
  $$$shell
  atlantis unlock
  $$$
`
						}
					case command.Apply:
						if c.ShouldWrap {
							exp = `
Ran Apply for dir: $.$ workspace: $default$

<details><summary>Show Output</summary>

$$$diff
` + strings.TrimSpace(c.Output) + `
$$$

</details>
`
						} else {
							exp = `
Ran Apply for dir: $.$ workspace: $default$

$$$diff
` + strings.TrimSpace(c.Output) + `
$$$
`
						}
					}

					Equals(t, normalize(exp), normalize(rendered))
				})
		}
	}
}

func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) {
	mr := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	tfOut := strings.Repeat("line\n", 13)
	res := command.Result{
		ProjectResults: []command.ProjectResult{
			{
				RepoRelDir:   ".",
				Workspace:    "staging",
				ApplySuccess: tfOut,
			},
			{
				RepoRelDir:   ".",
				Workspace:    "production",
				ApplySuccess: tfOut,
			},
		},
	}
	cmd := &events.CommentCommand{
		Name:    command.Apply,
		Verbose: false,
	}
	rendered := mr.Render(ctx, res, cmd)
	exp := `
Ran Apply for 2 projects:

1. dir: $.$ workspace: $staging$
1. dir: $.$ workspace: $production$
---

### 1. dir: $.$ workspace: $staging$
<details><summary>Show Output</summary>

$$$diff
` + strings.TrimSpace(tfOut) + `
$$$

</details>

---
### 2. dir: $.$ workspace: $production$
<details><summary>Show Output</summary>

$$$diff
` + strings.TrimSpace(tfOut) + `
$$$

</details>

---
### Apply Summary

2 projects, 2 successful, 0 failed, 0 errored
`
	Equals(t, normalize(exp), normalize(rendered))
}

func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) {
	mr := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	tfOut := strings.Repeat("line\n", 13) + "Plan: 1 to add, 0 to change, 0 to destroy."
	res := command.Result{
		ProjectResults: []command.ProjectResult{
			{
				RepoRelDir: ".",
				Workspace:  "staging",
				PlanSuccess: &models.PlanSuccess{
					TerraformOutput: tfOut,
					LockURL:         "staging-lock-url",
					ApplyCmd:        "staging-apply-cmd",
					RePlanCmd:       "staging-replan-cmd",
				},
			},
			{
				RepoRelDir: ".",
				Workspace:  "production",
				PlanSuccess: &models.PlanSuccess{
					TerraformOutput: tfOut,
					LockURL:         "production-lock-url",
					ApplyCmd:        "production-apply-cmd",
					RePlanCmd:       "production-replan-cmd",
				},
			},
		},
	}
	cmd := &events.CommentCommand{
		Name:    command.Plan,
		Verbose: false,
	}
	rendered := mr.Render(ctx, res, cmd)
	exp := `
Ran Plan for 2 projects:

1. dir: $.$ workspace: $staging$
1. dir: $.$ workspace: $production$
---

### 1. dir: $.$ workspace: $staging$
<details><summary>Show Output</summary>

$$$diff
` + tfOut + `
$$$
</details>

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  staging-apply-cmd
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](staging-lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  staging-replan-cmd
  $$$
Plan: 1 to add, 0 to change, 0 to destroy.

---
### 2. dir: $.$ workspace: $production$
<details><summary>Show Output</summary>

$$$diff
` + tfOut + `
$$$
</details>

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  production-apply-cmd
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](production-lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  production-replan-cmd
  $$$
Plan: 1 to add, 0 to change, 0 to destroy.

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`
	Equals(t, normalize(exp), normalize(rendered))
}

// Test rendering when there was an error in one of the plans and we deleted
// all the plans as a result.
func TestRenderProjectResults_PlansDeleted(t *testing.T) {
	cases := map[string]struct {
		res command.Result
		exp string
	}{
		"one failure": {
			res: command.Result{
				ProjectResults: []command.ProjectResult{
					{
						RepoRelDir: ".",
						Workspace:  "staging",
						Failure:    "failure",
					},
				},
				PlansDeleted: true,
			},
			exp: `
Ran Plan for dir: $.$ workspace: $staging$

**Plan Failed**: failure
`,
		},
		"two failures": {
			res: command.Result{
				ProjectResults: []command.ProjectResult{
					{
						RepoRelDir: ".",
						Workspace:  "staging",
						Failure:    "failure",
					},
					{
						RepoRelDir: ".",
						Workspace:  "production",
						Failure:    "failure",
					},
				},
				PlansDeleted: true,
			},
			exp: `
Ran Plan for 2 projects:

1. dir: $.$ workspace: $staging$
1. dir: $.$ workspace: $production$
---

### 1. dir: $.$ workspace: $staging$
**Plan Failed**: failure

---
### 2. dir: $.$ workspace: $production$
**Plan Failed**: failure

---
### Plan Summary

2 projects, 0 with changes, 0 with no changes, 2 failed
`,
		},
		"one failure, one success": {
			res: command.Result{
				ProjectResults: []command.ProjectResult{
					{
						RepoRelDir: ".",
						Workspace:  "staging",
						Failure:    "failure",
					},
					{
						RepoRelDir: ".",
						Workspace:  "production",
						PlanSuccess: &models.PlanSuccess{
							TerraformOutput: "tf out",
							LockURL:         "lock-url",
							RePlanCmd:       "re-plan cmd",
							ApplyCmd:        "apply cmd",
						},
					},
				},
				PlansDeleted: true,
			},
			exp: `
Ran Plan for 2 projects:

1. dir: $.$ workspace: $staging$
1. dir: $.$ workspace: $production$
---

### 1. dir: $.$ workspace: $staging$
**Plan Failed**: failure

---
### 2. dir: $.$ workspace: $production$
$$$diff
tf out
$$$

This plan was not saved because one or more projects failed and automerge requires all plans pass.

---
### Plan Summary

2 projects, 1 with changes, 0 with no changes, 1 failed
`,
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			mr := events.NewMarkdownRenderer(
				false,      // gitlabSupportsCommonMark
				false,      // disableApplyAll
				false,      // disableApply
				false,      // disableMarkdownFolding
				false,      // disableRepoLocking
				false,      // enableDiffMarkdownFormat
				"",         // markdownTemplateOverridesDir
				"atlantis", // executableName
				false,      // hideUnchangedPlanComments
				false,      // quietPolicyChecks
			)
			logger := logging.NewNoopLogger(t).WithHistory()
			logText := "log"
			logger.Info(logText)
			ctx := &command.Context{
				Log: logger,
				Pull: models.PullRequest{
					BaseRepo: models.Repo{
						VCSHost: models.VCSHost{
							Type: models.Github,
						},
					},
				},
			}
			cmd := &events.CommentCommand{
				Name:    command.Plan,
				Verbose: false,
			}
			rendered := mr.Render(ctx, c.res, cmd)
			Equals(t, normalize(c.exp), normalize(rendered))
		})
	}
}

// test that id repo locking is disabled the link to unlock the project is not rendered
func TestRenderProjectResultsWithRepoLockingDisabled(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"no projects",
			command.Plan,
			[]command.ProjectResult{},
			models.Github,
			"Ran Plan for 0 projects:\n\n",
		},
		{
			"single successful plan",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful plan with main ahead",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						MergedAgain:     true,
					},
					Workspace:  "workspace",
					RepoRelDir: "path",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
:twisted_rightwards_arrows: Upstream was modified, a new merge was performed.

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful plan with project name",
			command.Plan,
			[]command.ProjectResult{
				{
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						RePlanCmd:       "atlantis plan -d path -w workspace",
						ApplyCmd:        "atlantis apply -d path -w workspace",
					},
					Workspace:   "workspace",
					RepoRelDir:  "path",
					ProjectName: "projectname",
				},
			},
			models.Github,
			`
Ran Plan for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"single successful apply",
			command.Apply,
			[]command.ProjectResult{
				{
					ApplySuccess: "success",
					Workspace:    "workspace",
					RepoRelDir:   "path",
				},
			},
			models.Github,
			`
Ran Apply for dir: $path$ workspace: $workspace$

$$$diff
success
$$$
`,
		},
		{
			"single successful apply with project name",
			command.Apply,
			[]command.ProjectResult{
				{
					ApplySuccess: "success",
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ProjectName:  "projectname",
				},
			},
			models.Github,
			`
Ran Apply for project: $projectname$ dir: $path$ workspace: $workspace$

$$$diff
success
$$$
`,
		},
		{
			"multiple successful plans",
			command.Plan,
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output2",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
$$$diff
terraform-output2
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path2 -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"multiple successful applies",
			command.Apply,
			[]command.ProjectResult{
				{
					RepoRelDir:   "path",
					Workspace:    "workspace",
					ProjectName:  "projectname",
					ApplySuccess: "success",
				},
				{
					RepoRelDir:   "path2",
					Workspace:    "workspace",
					ApplySuccess: "success2",
				},
			},
			models.Github,
			`
Ran Apply for 2 projects:

1. project: $projectname$ dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
---

### 1. project: $projectname$ dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
$$$diff
success2
$$$

---
### Apply Summary

2 projects, 2 successful, 0 failed, 0 errored
`,
		},
		{
			"single errored plan",
			command.Plan,
			[]command.ProjectResult{
				{
					Error:      errors.New("error"),
					RepoRelDir: "path",
					Workspace:  "workspace",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

**Plan Error**
$$$
error
$$$
`,
		},
		{
			"single failed plan",
			command.Plan,
			[]command.ProjectResult{
				{
					RepoRelDir: "path",
					Workspace:  "workspace",
					Failure:    "failure",
				},
			},
			models.Github,
			`
Ran Plan for dir: $path$ workspace: $workspace$

**Plan Failed**: failure
`,
		},
		{
			"successful, failed, and errored plan",
			command.Plan,
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname",
					Error:       errors.New("error"),
				},
			},
			models.Github,
			`
Ran Plan for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. project: $projectname$ dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. dir: $path2$ workspace: $workspace$
**Plan Failed**: failure

---
### 3. project: $projectname$ dir: $path3$ workspace: $workspace$
**Plan Error**
$$$
error
$$$

---
### Plan Summary

3 projects, 1 with changes, 0 with no changes, 2 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"successful, failed, and errored apply",
			command.Apply,
			[]command.ProjectResult{
				{
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ApplySuccess: "success",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path3",
					Error:      errors.New("error"),
				},
			},
			models.Github,
			`
Ran Apply for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
**Apply Failed**: failure

---
### 3. dir: $path3$ workspace: $workspace$
**Apply Error**
$$$
error
$$$

---
### Apply Summary

3 projects, 1 successful, 1 failed, 1 errored
`,
		},
		{
			"successful, failed, and errored apply",
			command.Apply,
			[]command.ProjectResult{
				{
					Workspace:    "workspace",
					RepoRelDir:   "path",
					ApplySuccess: "success",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path2",
					Failure:    "failure",
				},
				{
					Workspace:  "workspace",
					RepoRelDir: "path3",
					Error:      errors.New("error"),
				},
			},
			models.Github,
			`
Ran Apply for 3 projects:

1. dir: $path$ workspace: $workspace$
1. dir: $path2$ workspace: $workspace$
1. dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
success
$$$

---
### 2. dir: $path2$ workspace: $workspace$
**Apply Failed**: failure

---
### 3. dir: $path3$ workspace: $workspace$
**Apply Error**
$$$
error
$$$

---
### Apply Summary

3 projects, 1 successful, 1 failed, 1 errored
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		true,       // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	ctx := &command.Context{
		Log: logger,
		Pull: models.PullRequest{
			BaseRepo: models.Repo{
				VCSHost: models.VCSHost{
					Type: models.Github,
				},
			},
		},
	}
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected+
							fmt.Sprintf("<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log)), normalize(s))
					}
				})
			}
		})
	}
}

func TestRenderProjectResultsWithGitLab(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"multiple successful plans",
			command.Plan,
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output2",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
			},
			models.Gitlab,
			`
Ran Plan for 2 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
$$$diff
terraform-output2
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path2 -w workspace
  $$$
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path2 -w workspace
  $$$

---
### Plan Summary

2 projects, 2 with changes, 0 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Merge Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Merge Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		true,       // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)
	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			ctx := &command.Context{
				Log: logger,
				Pull: models.PullRequest{
					BaseRepo: models.Repo{
						VCSHost: models.VCSHost{
							Type: c.VCSHost,
						},
					},
				},
			}
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected)+
							fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log), normalize(s))
					}
				})
			}
		})
	}
}

const tfOutput = `
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.redacted.aws_instance.redacted must be replaced
-/+ resource "aws_instance" "redacted" {
      ~ ami                          = "ami-redacted" -> "ami-redacted" # forces replacement
      ~ arn                          = "arn:aws:ec2:us-east-1:redacted:instance/i-redacted" -> (known after apply)
      ~ associate_public_ip_address  = false -> (known after apply)
        availability_zone            = "us-east-1b"
      ~ cpu_core_count               = 4 -> (known after apply)
      ~ cpu_threads_per_core         = 2 -> (known after apply)
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
        get_password_data            = false
      - hibernation                  = false -> null
      + host_id                      = (known after apply)
        iam_instance_profile         = "remote_redacted_profile"
      ~ id                           = "i-redacted" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
        instance_type                = "c5.2xlarge"
      ~ ipv6_address_count           = 0 -> (known after apply)
      ~ ipv6_addresses               = [] -> (known after apply)
        key_name                     = "RedactedRedactedRedacted"
      - monitoring                   = false -> null
      + outpost_arn                  = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      ~ primary_network_interface_id = "eni-redacted" -> (known after apply)
      ~ private_dns                  = "ip-redacted.ec2.internal" -> (known after apply)
      ~ private_ip                   = "redacted" -> (known after apply)
      + public_dns                   = (known after apply)
      + public_ip                    = (known after apply)
      ~ secondary_private_ips        = [] -> (known after apply)
      ~ security_groups              = [] -> (known after apply)
        source_dest_check            = true
        subnet_id                    = "subnet-redacted"
        tags                         = {
            "Name" = "redacted-redacted"
        }
      ~ tenancy                      = "default" -> (known after apply)
        user_data                    = "redacted"
      ~ volume_tags                  = {} -> (known after apply)
        vpc_security_group_ids       = [
            "sg-redactedsecuritygroup",
        ]

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      ~ metadata_options {
          ~ http_endpoint               = "enabled" -> (known after apply)
          ~ http_put_response_hop_limit = 1 -> (known after apply)
          ~ http_tokens                 = "optional" -> (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ delete_on_termination = true -> (known after apply)
          ~ device_name           = "/dev/sda1" -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 600 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ volume_id             = "vol-redacted" -> (known after apply)
          ~ volume_size           = 200 -> (known after apply)
          ~ volume_type           = "gp2" -> (known after apply)
        }
    }

  # module.redacted.aws_route53_record.redacted_record will be updated in-place
~ resource "aws_route53_record" "redacted_record" {
        fqdn    = "redacted.redacted.redacted.io"
        id      = "redacted_redacted.redacted.redacted.io_A"
        name    = "redacted.redacted.redacted.io"
      ~ records = [
            "foo",
          - "redacted",
        ] -> (known after apply)
        ttl     = 300
        type    = "A"
        zone_id = "redacted"
    }

  # module.redacted.aws_route53_record.redacted_record_2 will be created
+ resource "aws_route53_record" "redacted_record" {
      + fqdn    = "redacted.redacted.redacted.io"
      + id      = "redacted_redacted.redacted.redacted.io_A"
      + name    = "redacted.redacted.redacted.io"
      + records = [
            "foo",
        ]
      + ttl     = 300
      + type    = "A"
      + zone_id = "redacted"
    }

# helm_release.external_dns[0] will be updated in-place
~ resource "helm_release" "external_dns" {
      id                         = "external-dns"
      name                       = "external-dns"
    ~ values                     = [
        - <<-EOT
            image:
              tag: "0.12.0"
              pullSecrets:
              - XXXXX

            domainFilters: ["xxxxx","xxxxx"]
            base64:
              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==
          EOT,
        + <<-EOT
            image:
              tag: "0.12.0"
              pullSecrets:
              - XXXXX

            domainFilters: ["xxxxx","xxxxx"]
            base64:
              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==
          EOT,
      ]
    }

# aws_api_gateway_rest_api.rest_api will be updated in-place
~ resource "aws_api_gateway_rest_api" "rest_api" {
    ~ body                         = <<-EOT
          openapi: 3.0.0
          security:
            - SomeAuth: []
          paths:
            /someEndpoint:
              get:
        -       operationId: someOperation
        +       operationId: someOperation2
                responses:
                  204:
                    description: Empty response.
          components:
            schemas:
              SomeEnum:
                type: string
                enum:
                  - value1
                  - value2
            securitySchemes:
              SomeAuth:
                type: apiKey
                in: header
                name: Authorization
      EOT
      id                           = "4i5suz5c4l"
      name                         = "test"
      tags                         = {}
      # (9 unchanged attributes hidden)
      # (1 unchanged block hidden)
  }

Plan: 1 to add, 2 to change, 1 to destroy.
`

var cases = []struct {
	Description    string
	Command        command.Name
	ProjectResults []command.ProjectResult
	VCSHost        models.VCSHostType
	Expected       string
}{
	{
		"single successful plan with diff markdown formatted",
		command.Plan,
		[]command.ProjectResult{
			{
				PlanSuccess: &models.PlanSuccess{
					TerraformOutput: tfOutput,
					LockURL:         "lock-url",
					RePlanCmd:       "atlantis plan -d path -w workspace",
					ApplyCmd:        "atlantis apply -d path -w workspace",
				},
				Workspace:  "workspace",
				RepoRelDir: "path",
			},
		},
		models.Github,
		`
Ran Plan for dir: $path$ workspace: $workspace$

<details><summary>Show Output</summary>

$$$diff
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
! update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.redacted.aws_instance.redacted must be replaced
-/+ resource "aws_instance" "redacted" {
!       ami                          = "ami-redacted" -> "ami-redacted" # forces replacement
!       arn                          = "arn:aws:ec2:us-east-1:redacted:instance/i-redacted" -> (known after apply)
!       associate_public_ip_address  = false -> (known after apply)
        availability_zone            = "us-east-1b"
!       cpu_core_count               = 4 -> (known after apply)
!       cpu_threads_per_core         = 2 -> (known after apply)
-       disable_api_termination      = false -> null
-       ebs_optimized                = false -> null
        get_password_data            = false
-       hibernation                  = false -> null
+       host_id                      = (known after apply)
        iam_instance_profile         = "remote_redacted_profile"
!       id                           = "i-redacted" -> (known after apply)
!       instance_state               = "running" -> (known after apply)
        instance_type                = "c5.2xlarge"
!       ipv6_address_count           = 0 -> (known after apply)
!       ipv6_addresses               = [] -> (known after apply)
        key_name                     = "RedactedRedactedRedacted"
-       monitoring                   = false -> null
+       outpost_arn                  = (known after apply)
+       password_data                = (known after apply)
+       placement_group              = (known after apply)
!       primary_network_interface_id = "eni-redacted" -> (known after apply)
!       private_dns                  = "ip-redacted.ec2.internal" -> (known after apply)
!       private_ip                   = "redacted" -> (known after apply)
+       public_dns                   = (known after apply)
+       public_ip                    = (known after apply)
!       secondary_private_ips        = [] -> (known after apply)
!       security_groups              = [] -> (known after apply)
        source_dest_check            = true
        subnet_id                    = "subnet-redacted"
        tags                         = {
            "Name" = "redacted-redacted"
        }
!       tenancy                      = "default" -> (known after apply)
        user_data                    = "redacted"
!       volume_tags                  = {} -> (known after apply)
        vpc_security_group_ids       = [
            "sg-redactedsecuritygroup",
        ]

+       ebs_block_device {
+           delete_on_termination = (known after apply)
+           device_name           = (known after apply)
+           encrypted             = (known after apply)
+           iops                  = (known after apply)
+           kms_key_id            = (known after apply)
+           snapshot_id           = (known after apply)
+           volume_id             = (known after apply)
+           volume_size           = (known after apply)
+           volume_type           = (known after apply)
        }

+       ephemeral_block_device {
+           device_name  = (known after apply)
+           no_device    = (known after apply)
+           virtual_name = (known after apply)
        }

!       metadata_options {
!           http_endpoint               = "enabled" -> (known after apply)
!           http_put_response_hop_limit = 1 -> (known after apply)
!           http_tokens                 = "optional" -> (known after apply)
        }

+       network_interface {
+           delete_on_termination = (known after apply)
+           device_index          = (known after apply)
+           network_interface_id  = (known after apply)
        }

!       root_block_device {
!           delete_on_termination = true -> (known after apply)
!           device_name           = "/dev/sda1" -> (known after apply)
!           encrypted             = false -> (known after apply)
!           iops                  = 600 -> (known after apply)
+           kms_key_id            = (known after apply)
!           volume_id             = "vol-redacted" -> (known after apply)
!           volume_size           = 200 -> (known after apply)
!           volume_type           = "gp2" -> (known after apply)
        }
    }

  # module.redacted.aws_route53_record.redacted_record will be updated in-place
! resource "aws_route53_record" "redacted_record" {
        fqdn    = "redacted.redacted.redacted.io"
        id      = "redacted_redacted.redacted.redacted.io_A"
        name    = "redacted.redacted.redacted.io"
!       records = [
            "foo",
-           "redacted",
        ] -> (known after apply)
        ttl     = 300
        type    = "A"
        zone_id = "redacted"
    }

  # module.redacted.aws_route53_record.redacted_record_2 will be created
+ resource "aws_route53_record" "redacted_record" {
+       fqdn    = "redacted.redacted.redacted.io"
+       id      = "redacted_redacted.redacted.redacted.io_A"
+       name    = "redacted.redacted.redacted.io"
+       records = [
            "foo",
        ]
+       ttl     = 300
+       type    = "A"
+       zone_id = "redacted"
    }

# helm_release.external_dns[0] will be updated in-place
! resource "helm_release" "external_dns" {
      id                         = "external-dns"
      name                       = "external-dns"
!     values                     = [
-         <<-EOT
            image:
              tag: "0.12.0"
              pullSecrets:
              - XXXXX

            domainFilters: ["xxxxx","xxxxx"]
            base64:
              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==
          EOT,
+         <<-EOT
            image:
              tag: "0.12.0"
              pullSecrets:
              - XXXXX

            domainFilters: ["xxxxx","xxxxx"]
            base64:
              +dGhpcyBpcyBzb21lIHN0cmluZyBvciBzb21ldGhpbmcKCg==
          EOT,
      ]
    }

# aws_api_gateway_rest_api.rest_api will be updated in-place
! resource "aws_api_gateway_rest_api" "rest_api" {
!     body                         = <<-EOT
          openapi: 3.0.0
          security:
            - SomeAuth: []
          paths:
            /someEndpoint:
              get:
-               operationId: someOperation
+               operationId: someOperation2
                responses:
                  204:
                    description: Empty response.
          components:
            schemas:
              SomeEnum:
                type: string
                enum:
                  - value1
                  - value2
            securitySchemes:
              SomeAuth:
                type: apiKey
                in: header
                name: Authorization
      EOT
      id                           = "4i5suz5c4l"
      name                         = "test"
      tags                         = {}
      # (9 unchanged attributes hidden)
      # (1 unchanged block hidden)
  }

Plan: 1 to add, 2 to change, 1 to destroy.
$$$
</details>

* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$
Plan: 1 to add, 2 to change, 1 to destroy.
`,
	},
}

func TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) {
	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		true,       // disableApplyAll
		true,       // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		true,       // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)

	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			ctx := &command.Context{
				Log: logger,
				Pull: models.PullRequest{
					BaseRepo: models.Repo{
						VCSHost: models.VCSHost{
							Type: models.Github,
						},
					},
				},
			}
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected)+
							fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log), normalize(s))
					}
				})
			}
		})
	}
}

var Render string

func BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) {
	var render string

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		true,       // disableApplyAll
		true,       // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		true,       // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		false,      // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(b).WithHistory()
	logText := "log"
	logger.Info(logText)

	for _, c := range cases {
		b.Run(c.Description, func(b *testing.B) {
			ctx := &command.Context{
				Log: logger,
				Pull: models.PullRequest{
					BaseRepo: models.Repo{
						VCSHost: models.VCSHost{
							Type: c.VCSHost,
						},
					},
				},
			}
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				b.Run(fmt.Sprintf("verbose %t", verbose), func(b *testing.B) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						Verbose: verbose,
					}
					b.ReportAllocs()
					for i := 0; i < b.N; i++ {
						render = r.Render(ctx, res, cmd)
					}
					Render = render
				})
			}
		})
	}
}

func TestRenderProjectResultsHideUnchangedPlans(t *testing.T) {
	cases := []struct {
		Description    string
		Command        command.Name
		SubCommand     string
		ProjectResults []command.ProjectResult
		VCSHost        models.VCSHostType
		Expected       string
	}{
		{
			"multiple successful plans, hide unchanged plans",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "No changes. Infrastructure is up-to-date.",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname2",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "terraform-output3",
						LockURL:         "lock-url3",
						ApplyCmd:        "atlantis apply -d path3 -w workspace",
						RePlanCmd:       "atlantis plan -d path3 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 3 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
1. project: $projectname2$ dir: $path3$ workspace: $workspace$
---

### 1. dir: $path$ workspace: $workspace$
$$$diff
terraform-output
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path -w workspace
  $$$

---
### 3. project: $projectname2$ dir: $path3$ workspace: $workspace$
$$$diff
terraform-output3
$$$

* :arrow_forward: To **apply** this plan, comment:
  $$$shell
  atlantis apply -d path3 -w workspace
  $$$
* :put_litter_in_its_place: To **delete** this plan and lock, click [here](lock-url3)
* :repeat: To **plan** this project again, comment:
  $$$shell
  atlantis plan -d path3 -w workspace
  $$$

---
### Plan Summary

3 projects, 2 with changes, 1 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
		{
			"multiple successful plans, hide unchanged plans, all plans are unchanged",
			command.Plan,
			"",
			[]command.ProjectResult{
				{
					Workspace:  "workspace",
					RepoRelDir: "path",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "No changes. Infrastructure is up-to-date.",
						LockURL:         "lock-url",
						ApplyCmd:        "atlantis apply -d path -w workspace",
						RePlanCmd:       "atlantis plan -d path -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path2",
					ProjectName: "projectname",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "No changes. Infrastructure is up-to-date.",
						LockURL:         "lock-url2",
						ApplyCmd:        "atlantis apply -d path2 -w workspace",
						RePlanCmd:       "atlantis plan -d path2 -w workspace",
					},
				},
				{
					Workspace:   "workspace",
					RepoRelDir:  "path3",
					ProjectName: "projectname2",
					PlanSuccess: &models.PlanSuccess{
						TerraformOutput: "No changes. Infrastructure is up-to-date.",
						LockURL:         "lock-url3",
						ApplyCmd:        "atlantis apply -d path3 -w workspace",
						RePlanCmd:       "atlantis plan -d path3 -w workspace",
					},
				},
			},
			models.Github,
			`
Ran Plan for 3 projects:

1. dir: $path$ workspace: $workspace$
1. project: $projectname$ dir: $path2$ workspace: $workspace$
1. project: $projectname2$ dir: $path3$ workspace: $workspace$
---

### Plan Summary

3 projects, 0 with changes, 3 with no changes, 0 failed

* :fast_forward: To **apply** all unapplied plans from this Pull Request, comment:
  $$$shell
  atlantis apply
  $$$
* :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment:
  $$$shell
  atlantis unlock
  $$$
`,
		},
	}

	r := events.NewMarkdownRenderer(
		false,      // gitlabSupportsCommonMark
		false,      // disableApplyAll
		false,      // disableApply
		false,      // disableMarkdownFolding
		false,      // disableRepoLocking
		false,      // enableDiffMarkdownFormat
		"",         // markdownTemplateOverridesDir
		"atlantis", // executableName
		true,       // hideUnchangedPlanComments
		false,      // quietPolicyChecks
	)
	logger := logging.NewNoopLogger(t).WithHistory()
	logText := "log"
	logger.Info(logText)

	for _, c := range cases {
		t.Run(c.Description, func(t *testing.T) {
			ctx := &command.Context{
				Log: logger,
				Pull: models.PullRequest{
					BaseRepo: models.Repo{
						VCSHost: models.VCSHost{
							Type: c.VCSHost,
						},
					},
				},
			}
			res := command.Result{
				ProjectResults: c.ProjectResults,
			}
			for _, verbose := range []bool{true, false} {
				t.Run(c.Description, func(t *testing.T) {
					cmd := &events.CommentCommand{
						Name:    c.Command,
						SubName: c.SubCommand,
						Verbose: verbose,
					}
					s := r.Render(ctx, res, cmd)
					if !verbose {
						Equals(t, normalize(c.Expected), normalize(s))
					} else {
						log := fmt.Sprintf("[INFO] %s", logText)
						Equals(t, normalize(c.Expected)+
							fmt.Sprintf("\n<details><summary>Log</summary>\n<p>\n\n```\n%s\n```\n</p></details>", log), normalize(s))
					}
				})
			}
		})
	}
}
