Ctrl K
ComunidadGuías…

cli/create_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cli_test

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"regexp"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/sync/singleflight"

	"github.com/coder/coder/v2/cli"
	"github.com/coder/coder/v2/cli/clitest"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/externalauth"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/provisioner/echo"
	"github.com/coder/coder/v2/provisionersdk/proto"
	"github.com/coder/coder/v2/testutil"
	"github.com/coder/coder/v2/testutil/expecter"
)

func TestCreateDynamic(t *testing.T) {
	t.Parallel()
	owner := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
	first := coderdtest.CreateFirstUser(t, owner)
	member, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)

	// Terraform template with conditional parameters.
	// The "region" parameter only appears when "enable_region" is true.
	const conditionalParamTF = `
		terraform {
		  required_providers {
		    coder = {
		      source = "coder/coder"
		    }
		  }
		}
		data "coder_workspace_owner" "me" {}
		data "coder_parameter" "enable_region" {
		  name         = "enable_region"
		  order        = 1
		  type         = "bool"
		  default      = "false"
		}
		data "coder_parameter" "region" {
		  name         = "region"
		  count        = data.coder_parameter.enable_region.value == "true" ? 1 : 0
		  order        = 2
		  type         = "string"
		  # No default - this makes it required when it appears
		}
	`

	// Test conditional parameters: a parameter that only appears when another
	// parameter has a certain value.
	t.Run("ConditionalParam", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)
		template, _ := coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
			MainTF: conditionalParamTF,
		})

		// Test 1: Create without enabling region - region param should not exist
		args := []string{
			"create", "ws-no-region",
			"--template", template.Name,
			"--parameter", "enable_region=false",
			"-y",
		}
		inv, root := clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)

		doneChan := make(chan error)
		go func() {
			doneChan <- inv.Run()
		}()

		stdout.ExpectMatch(ctx, "has been created")
		err := testutil.RequireReceive(ctx, t, doneChan)
		require.NoError(t, err)

		// Verify workspace created with only enable_region parameter
		ws, err := member.WorkspaceByOwnerAndName(t.Context(), codersdk.Me, "ws-no-region", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		buildParams, err := member.WorkspaceBuildParameters(t.Context(), ws.LatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParams, 1, "expected only enable_region parameter when enable_region=false")
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "enable_region", Value: "false"})

		// Test 2: Create with region enabled - region param should exist
		args = []string{
			"create", "ws-with-region",
			"--template", template.Name,
			"--parameter", "enable_region=true",
			"--parameter", "region=us-east",
			"-y",
		}
		inv, root = clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		stdout = expecter.NewAttachedToInvocation(t, inv)

		doneChan = make(chan error)
		go func() {
			doneChan <- inv.Run()
		}()

		stdout.ExpectMatch(ctx, "has been created")

		err = testutil.RequireReceive(ctx, t, doneChan)
		require.NoError(t, err)

		// Verify workspace created with both parameters
		ws, err = member.WorkspaceByOwnerAndName(t.Context(), codersdk.Me, "ws-with-region", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		buildParams, err = member.WorkspaceBuildParameters(t.Context(), ws.LatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParams, 2, "expected both enable_region and region parameters when enable_region=true")
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "enable_region", Value: "true"})
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "region", Value: "us-east"})
	})

	// Test that the CLI prompts for missing conditional parameters.
	// When enable_region=true, the region parameter becomes required and CLI should prompt.
	t.Run("PromptForConditionalParam", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, time.Hour)
		logger := testutil.Logger(t)

		template, _ := coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
			MainTF: conditionalParamTF,
		})

		// Only provide enable_region=true, don't provide region - CLI should prompt for it
		args := []string{
			"create", "ws-prompted",
			"--template", template.Name,
			"--parameter", "enable_region=true",
		}
		inv, root := clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)

		doneChan := make(chan error)
		go func() {
			doneChan <- inv.Run()
		}()

		// CLI should prompt for the region parameter since enable_region=true
		stdout.ExpectMatch(ctx, "region")
		stdin.WriteLine("eu-west")

		// Confirm creation
		stdout.ExpectMatch(ctx, "Confirm create?")
		stdin.WriteLine("yes")

		stdout.ExpectMatch(ctx, "has been created")

		err := <-doneChan
		require.NoError(t, err)

		// Verify workspace created with both parameters
		ws, err := member.WorkspaceByOwnerAndName(t.Context(), codersdk.Me, "ws-prompted", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		buildParams, err := member.WorkspaceBuildParameters(t.Context(), ws.LatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParams, 2, "expected both enable_region and region parameters")
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "enable_region", Value: "true"})
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "region", Value: "eu-west"})
	})

	// Test that updating a template with a new required parameter causes start to fail
	// when the user doesn't provide the new parameter value.
	t.Run("UpdateTemplateRequiredParamStartFails", func(t *testing.T) {
		t.Parallel()

		// Initial template with just enable_region parameter (no default, so required)
		const initialTF = `
			terraform {
			  required_providers {
			    coder = {
			      source = "coder/coder"
			    }
			  }
			}
			data "coder_workspace_owner" "me" {}
			data "coder_parameter" "enable_region" {
			  name         = "enable_region"
			  type         = "bool"
			}
		`

		template, _ := coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
			MainTF: initialTF,
		})

		// Create workspace with initial template
		inv, root := clitest.New(t, "create", "ws-update-test",
			"--template", template.Name,
			"--parameter", "enable_region=false",
			"-y",
		)
		clitest.SetupConfig(t, member, root)
		err := inv.Run()
		require.NoError(t, err)

		// Stop the workspace
		inv, root = clitest.New(t, "stop", "ws-update-test", "-y")
		clitest.SetupConfig(t, member, root)
		err = inv.Run()
		require.NoError(t, err)

		const updatedTF = `
			terraform {
			  required_providers {
			    coder = {
			      source = "coder/coder"
			    }
			  }
			}
			data "coder_workspace_owner" "me" {}
			data "coder_parameter" "enable_region" {
			  name         = "enable_region"
			  type         = "bool"
			}
			data "coder_parameter" "region" {
			  count        = data.coder_parameter.enable_region.value == "true" ? 1 : 0
			  name         = "region"
			  type         = "string"
			  # No default - required when enable_region is true
			}
		`

		coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
			MainTF:     updatedTF,
			TemplateID: template.ID,
		})

		// Try to start the workspace with update - should fail because region is now required
		// (enable_region defaults to true, making region appear, but no value provided)
		// and we're using -y to skip prompts
		inv, root = clitest.New(t, "start", "ws-update-test", "-y", "--parameter", "enable_region=true")
		clitest.SetupConfig(t, member, root)
		err = inv.Run()
		require.Error(t, err, "start should fail because new required parameter 'region' is missing")
		require.Contains(t, err.Error(), "region")
	})

	// Test that dynamic validation allows values that would be invalid with static validation.
	// A slider's max value is determined by another parameter, so a value of 8 is invalid
	// when max_slider=5, but valid when max_slider=10.
	t.Run("DynamicValidation", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitLong)

		// Template where slider's max is controlled by another parameter
		const dynamicValidationTF = `
			terraform {
			  required_providers {
			    coder = {
			      source = "coder/coder"
			    }
			  }
			}
			data "coder_workspace_owner" "me" {}
			data "coder_parameter" "max_slider" {
			  name         = "max_slider"
			  type         = "number"
			  default      = 5
			}
			data "coder_parameter" "slider" {
			  name         = "slider"
			  type         = "number"
			  default      = 1
			  validation {
			    min = 1
			    max = data.coder_parameter.max_slider.value
			  }
			}
		`

		template, _ := coderdtest.DynamicParameterTemplate(t, owner, first.OrganizationID, coderdtest.DynamicParameterTemplateParams{
			MainTF: dynamicValidationTF,
		})

		// Test 1: slider=8 should fail when max_slider=5 (default)
		inv, root := clitest.New(t, "create", "ws-validation-fail",
			"--template", template.Name,
			"--parameter", "slider=8",
			"-y",
		)
		clitest.SetupConfig(t, member, root)
		err := inv.Run()
		require.Error(t, err, "slider=8 should fail when max_slider=5")

		// Test 2: slider=8 should succeed when max_slider=10
		inv, root = clitest.New(t, "create", "ws-validation-pass",
			"--template", template.Name,
			"--parameter", "max_slider=10",
			"--parameter", "slider=8",
			"-y",
		)
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)

		doneChan := make(chan error)
		go func() {
			doneChan <- inv.Run()
		}()

		stdout.ExpectMatch(ctx, "has been created")

		err = <-doneChan
		require.NoError(t, err, "slider=8 should succeed when max_slider=10")

		// Verify workspace created with correct parameters
		ws, err := member.WorkspaceByOwnerAndName(t.Context(), codersdk.Me, "ws-validation-pass", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		buildParams, err := member.WorkspaceBuildParameters(t.Context(), ws.LatestBuild.ID)
		require.NoError(t, err)
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "max_slider", Value: "10"})
		require.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "slider", Value: "8"})
	})
}

func TestCreate(t *testing.T) {
	t.Parallel()
	t.Run("Create", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		args := []string{
			"create",
			"my-workspace",
			"--template", template.Name,
			"--start-at", "9:30AM Mon-Fri US/Central",
			"--stop-after", "8h",
			"--automatic-updates", "always",
		}
		inv, root := clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()
		matches := []struct {
			match string
			write string
		}{
			{match: "compute.main"},
			{match: "smith (linux, i386)"},
			{match: "Confirm create", write: "yes"},
		}
		for _, m := range matches {
			stdout.ExpectMatch(ctx, m.match)
			if len(m.write) > 0 {
				stdin.WriteLine(m.write)
			}
		}
		<-doneChan

		ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		if assert.NoError(t, err, "expected workspace to be created") {
			assert.Equal(t, ws.TemplateName, template.Name)
			if assert.NotNil(t, ws.AutostartSchedule) {
				assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri")
			}
			if assert.NotNil(t, ws.TTLMillis) {
				assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
			}
			assert.Equal(t, codersdk.AutomaticUpdatesAlways, ws.AutomaticUpdates)
		}
	})

	t.Run("CreateForOtherUser", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		args := []string{
			"create",
			user.Username + "/their-workspace",
			"--template", template.Name,
			"--start-at", "9:30AM Mon-Fri US/Central",
			"--stop-after", "8h",
		}

		inv, root := clitest.New(t, args...)
		//nolint:gocritic // Creating a workspace for another user requires owner permissions.
		clitest.SetupConfig(t, client, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()
		matches := []struct {
			match string
			write string
		}{
			{match: "compute.main"},
			{match: "smith (linux, i386)"},
			{match: "coder ssh " + user.Username + "/their-workspace"},
			{match: "Confirm create", write: "yes"},
		}
		for _, m := range matches {
			stdout.ExpectMatch(ctx, m.match)
			if len(m.write) > 0 {
				stdin.WriteLine(m.write)
			}
		}
		<-doneChan

		ws, err := client.WorkspaceByOwnerAndName(context.Background(), user.Username, "their-workspace", codersdk.WorkspaceOptions{})
		if assert.NoError(t, err, "expected workspace to be created") {
			assert.Equal(t, ws.TemplateName, template.Name)
			if assert.NotNil(t, ws.AutostartSchedule) {
				assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri")
			}
			if assert.NotNil(t, ws.TTLMillis) {
				assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
			}
		}
	})

	t.Run("CreateWithSpecificTemplateVersion", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent(), func(ctvr *codersdk.CreateTemplateVersionRequest) {
			ctvr.Name = "v1"
		})
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// Create a new version
		version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent(), func(ctvr *codersdk.CreateTemplateVersionRequest) {
			ctvr.Name = "v2"
			ctvr.TemplateID = template.ID
		})
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)

		args := []string{
			"create",
			"my-workspace",
			"--template", template.Name,
			"--template-version", version2.Name,
			"--start-at", "9:30AM Mon-Fri US/Central",
			"--stop-after", "8h",
			"--automatic-updates", "always",
		}
		inv, root := clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()
		matches := []struct {
			match string
			write string
		}{
			{match: "compute.main"},
			{match: "smith (linux, i386)"},
			{match: "Confirm create", write: "yes"},
		}
		for _, m := range matches {
			stdout.ExpectMatch(ctx, m.match)
			if len(m.write) > 0 {
				stdin.WriteLine(m.write)
			}
		}
		<-doneChan

		ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		if assert.NoError(t, err, "expected workspace to be created") {
			assert.Equal(t, ws.TemplateName, template.Name)
			// Check if the workspace is using the new template version
			assert.Equal(t, ws.LatestBuild.TemplateVersionID, version2.ID, "expected workspace to use the specified template version")
			if assert.NotNil(t, ws.AutostartSchedule) {
				assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri")
			}
			if assert.NotNil(t, ws.TTLMillis) {
				assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
			}
			assert.Equal(t, codersdk.AutomaticUpdatesAlways, ws.AutomaticUpdates)
		}
	})

	t.Run("InheritStopAfterFromTemplate", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
			var defaultTTLMillis int64 = 2 * 60 * 60 * 1000 // 2 hours
			ctr.DefaultTTLMillis = &defaultTTLMillis
		})
		args := []string{
			"create",
			"my-workspace",
			"--template", template.Name,
		}
		inv, root := clitest.New(t, args...)
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		waiter := clitest.StartWithWaiter(t, inv)
		matches := []struct {
			match string
			write string
		}{
			{match: "compute.main"},
			{match: "smith (linux, i386)"},
			{match: "Confirm create", write: "yes"},
		}
		for _, m := range matches {
			stdout.ExpectMatch(ctx, m.match)
			if len(m.write) > 0 {
				stdin.WriteLine(m.write)
			}
		}
		waiter.RequireSuccess()

		ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		require.NoError(t, err, "expected workspace to be created")
		assert.Equal(t, ws.TemplateName, template.Name)
		assert.Equal(t, *ws.TTLMillis, template.DefaultTTLMillis)
	})

	t.Run("CreateFromListWithSkip", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		user := coderdtest.CreateFirstUser(t, client)
		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
		inv, root := clitest.New(t, "create", "my-workspace", "-y")

		member, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
		clitest.SetupConfig(t, member, root)
		cmdCtx, done := context.WithTimeout(context.Background(), testutil.WaitLong)
		go func() {
			defer done()
			err := inv.WithContext(cmdCtx).Run()
			assert.NoError(t, err)
		}()
		// No pty interaction needed since we use the -y skip prompt flag
		<-cmdCtx.Done()
		require.ErrorIs(t, cmdCtx.Err(), context.Canceled)
	})

	t.Run("FromNothing", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
		inv, root := clitest.New(t, "create", "")
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()
		matches := []string{
			"Specify a name", "my-workspace",
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			stdin.WriteLine(value)
		}
		<-doneChan

		ws, err := member.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		if assert.NoError(t, err, "expected workspace to be created") {
			assert.Equal(t, ws.TemplateName, template.Name)
			assert.Nil(t, ws.AutostartSchedule, "expected workspace autostart schedule to be nil")
		}
	})

	t.Run("NoWait", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		ctx := testutil.Context(t, testutil.WaitLong)
		inv, root := clitest.New(t, "create", "my-workspace",
			"--template", template.Name,
			"-y",
			"--no-wait",
		)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		stdout.ExpectMatch(ctx, "building in the background")
		_ = testutil.TryReceive(ctx, t, doneChan)

		// Verify workspace was actually created.
		ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		assert.Equal(t, ws.TemplateName, template.Name)
	})

	t.Run("NoWaitWithParameterDefaults", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses([]*proto.RichParameter{
			{Name: "region", Type: "string", DefaultValue: "us-east-1"},
			{Name: "instance_type", Type: "string", DefaultValue: "t3.micro"},
		}))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		ctx := testutil.Context(t, testutil.WaitLong)
		inv, root := clitest.New(t, "create", "my-workspace",
			"--template", template.Name,
			"-y",
			"--use-parameter-defaults",
			"--no-wait",
		)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		stdout.ExpectMatch(ctx, "building in the background")
		_ = testutil.TryReceive(ctx, t, doneChan)

		// Verify workspace was created and parameters were applied.
		ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		require.NoError(t, err)
		assert.Equal(t, ws.TemplateName, template.Name)

		buildParams, err := member.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
		require.NoError(t, err)
		assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "region", Value: "us-east-1"})
		assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "instance_type", Value: "t3.micro"})
	})

	// Verifies that --use-parameter-defaults accepts empty-string
	// defaults without prompting. Uses the classic parameter flow
	// because the echo provisioner sets Required via proto fields,
	// which the dynamic parameter evaluator does not read.
	t.Run("EmptyStringDefaultNoPrompt", func(t *testing.T) {
		t.Parallel()
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses([]*proto.RichParameter{
			{Name: "region", Type: "string", DefaultValue: "us-east-1"},
			{Name: "optional_field", Type: "string", DefaultValue: ""},
		}))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
			ctr.UseClassicParameterFlow = new(true)
		})

		ctx := testutil.Context(t, testutil.WaitLong)
		inv, root := clitest.New(t, "create", "my-workspace",
			"--template", template.Name,
			"-y",
			"--use-parameter-defaults",
			"--no-wait",
		)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		stdout.ExpectMatch(ctx, "building in the background")
		_ = testutil.TryReceive(ctx, t, doneChan)

		ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
		require.NoError(t, err)

		buildParams, err := member.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
		require.NoError(t, err)
		assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "region", Value: "us-east-1"})
		assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "optional_field", Value: ""})
	})
}

func prepareEchoResponses(parameters []*proto.RichParameter, presets ...*proto.Preset) *echo.Responses {
	return &echo.Responses{
		Parse:         echo.ParseComplete,
		ProvisionInit: echo.InitComplete,
		ProvisionPlan: echo.PlanComplete,
		ProvisionGraph: []*proto.Response{
			{
				Type: &proto.Response_Graph{
					Graph: &proto.GraphComplete{
						Parameters: parameters,
						Presets:    presets,
					},
				},
			},
		},
		ProvisionApply: echo.ApplyComplete,
	}
}

type param struct {
	name    string
	ptype   string
	value   string
	mutable bool
}

func TestCreateWithRichParameters(t *testing.T) {
	t.Parallel()

	// Default parameters and their expected values.
	params := []param{
		{
			name:    "number_param",
			ptype:   "number",
			value:   "777",
			mutable: true,
		},
		{
			name:    "string_param",
			ptype:   "string",
			value:   "qux",
			mutable: true,
		},
		{
			name: "bool_param",
			// TODO: Setting the type breaks booleans.  It claims the default is false
			// but when you then accept this default it errors saying that the value
			// must be true or false.  For now, use a string.
			ptype:   "string",
			value:   "false",
			mutable: true,
		},
		{
			name:    "immutable_string_param",
			ptype:   "string",
			value:   "i am eternal",
			mutable: false,
		},
	}

	type testContext struct {
		client        *codersdk.Client
		member        *codersdk.Client
		owner         codersdk.CreateFirstUserResponse
		template      codersdk.Template
		workspaceName string
	}

	tests := []struct {
		name string
		// setup runs before the command is started and return arguments that will
		// be appended to the create command.
		setup func() []string
		// handlePty optionally runs after the command is started.  It should handle
		// all expected prompts from the pty.
		handlePty func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer)
		// postRun runs after the command has finished but before the workspace is
		// verified.  It must return the workspace name to check (used for the copy
		// workspace tests).
		postRun func(t *testing.T, args testContext) string
		// errors contains expected errors.  The workspace will not be verified if
		// errors are expected.
		errors []string
		// inputParameters overrides the default parameters.
		inputParameters []param
		// expectedParameters defaults to inputParameters.
		expectedParameters []param
		// withDefaults sets DefaultValue to each parameter's value.
		withDefaults bool
	}{
		{
			name: "ValuesFromPrompt",
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Enter the value for each parameter as prompted.
				for _, param := range params {
					stdout.ExpectMatch(ctx, param.name)
					stdin.WriteLine(param.value)
				}
				// Confirm the creation.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
		},
		{
			name: "ValuesFromDefaultFlags",
			setup: func() []string {
				// Provide the defaults on the command line.
				args := []string{}
				for _, param := range params {
					args = append(args, "--parameter-default", fmt.Sprintf("%s=%s", param.name, param.value))
				}
				return args
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Simply accept the defaults.
				for _, param := range params {
					stdout.ExpectMatch(ctx, param.name)
					stdout.ExpectMatch(ctx, `Enter a value (default: "`+param.value+`")`)
					stdin.WriteLine("")
				}
				// Confirm the creation.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
		},
		{
			name: "ValuesFromFile",
			setup: func() []string {
				// Create a file with the values.
				tempDir := t.TempDir()
				removeTmpDirUntilSuccessAfterTest(t, tempDir)
				parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
				for _, param := range params {
					_, err := fmt.Fprintf(parameterFile, "%s: %s\n", param.name, param.value)
					require.NoError(t, err)
				}

				return []string{"--rich-parameter-file", parameterFile.Name()}
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// No prompts, we only need to confirm.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
		},
		{
			name: "ValuesFromFlags",
			setup: func() []string {
				// Provide the values on the command line.
				var args []string
				for _, param := range params {
					args = append(args, "--parameter", fmt.Sprintf("%s=%s", param.name, param.value))
				}
				return args
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// No prompts, we only need to confirm.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
		},
		{
			name: "MisspelledParameter",
			setup: func() []string {
				// Provide the values on the command line.
				args := []string{}
				for i, param := range params {
					if i == 0 {
						// Slightly misspell the first parameter with an extra character.
						args = append(args, "--parameter", fmt.Sprintf("n%s=%s", param.name, param.value))
					} else {
						args = append(args, "--parameter", fmt.Sprintf("%s=%s", param.name, param.value))
					}
				}
				return args
			},
			errors: []string{
				"parameter \"n" + params[0].name + "\" is not present in the template",
				"Did you mean: " + params[0].name,
			},
		},
		{
			name: "ValuesFromWorkspace",
			setup: func() []string {
				// Provide the values on the command line.
				args := []string{"-y"}
				for _, param := range params {
					args = append(args, "--parameter", fmt.Sprintf("%s=%s", param.name, param.value))
				}
				return args
			},
			postRun: func(t *testing.T, tctx testContext) string {
				inv, root := clitest.New(t, "create", "--copy-parameters-from", tctx.workspaceName, "other-workspace", "-y")
				clitest.SetupConfig(t, tctx.member, root)
				err := inv.Run()
				require.NoError(t, err, "failed to create a workspace based on the source workspace")
				return "other-workspace"
			},
		},
		{
			name: "ValuesFromOutdatedWorkspace",
			setup: func() []string {
				// Provide the values on the command line.
				args := []string{"-y"}
				for _, param := range params {
					args = append(args, "--parameter", fmt.Sprintf("%s=%s", param.name, param.value))
				}
				return args
			},
			postRun: func(t *testing.T, tctx testContext) string {
				// Update the template to a new version.
				version2 := coderdtest.CreateTemplateVersion(t, tctx.client, tctx.owner.OrganizationID, prepareEchoResponses([]*proto.RichParameter{
					{Name: "another_parameter", Type: "string", DefaultValue: "not-relevant"},
				}), func(ctvr *codersdk.CreateTemplateVersionRequest) {
					ctvr.Name = "v2"
					ctvr.TemplateID = tctx.template.ID
				})
				coderdtest.AwaitTemplateVersionJobCompleted(t, tctx.client, version2.ID)
				coderdtest.UpdateActiveTemplateVersion(t, tctx.client, tctx.template.ID, version2.ID)

				// Then create the copy.  It should use the old template version.
				inv, root := clitest.New(t, "create", "--copy-parameters-from", tctx.workspaceName, "other-workspace", "-y")
				clitest.SetupConfig(t, tctx.member, root)
				err := inv.Run()
				require.NoError(t, err, "failed to create a workspace based on the source workspace")
				return "other-workspace"
			},
		},
		{
			name: "ValuesFromTemplateDefaults",
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Simply accept the defaults.
				for _, param := range params {
					stdout.ExpectMatch(ctx, param.name)
					stdout.ExpectMatch(ctx, `Enter a value (default: "`+param.value+`")`)
					stdin.WriteLine("")
				}
				// Confirm the creation.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
			withDefaults: true,
		},
		{
			name: "ValuesFromTemplateDefaultsNoPrompt",
			setup: func() []string {
				return []string{"--use-parameter-defaults"}
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Default values should get printed.
				for _, param := range params {
					stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", param.name, param.value))
				}
				// No prompts, we only need to confirm.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
			withDefaults: true,
		},
		{
			name: "ValuesFromDefaultFlagsNoPrompt",
			setup: func() []string {
				// Provide the defaults on the command line.
				args := []string{"--use-parameter-defaults"}
				for _, param := range params {
					args = append(args, "--parameter-default", fmt.Sprintf("%s=%s", param.name, param.value))
				}
				return args
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Default values should get printed.
				for _, param := range params {
					stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", param.name, param.value))
				}
				// No prompts, we only need to confirm.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
		},
		{
			// File and flags should override template defaults.  Additionally, if a
			// value has no default value we should still get a prompt for it.
			name: "ValuesFromMultipleSources",
			setup: func() []string {
				tempDir := t.TempDir()
				removeTmpDirUntilSuccessAfterTest(t, tempDir)
				parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
				_, err := parameterFile.WriteString(`
file_param: from file
cli_param: from file`)
				require.NoError(t, err)
				return []string{
					"--use-parameter-defaults",
					"--rich-parameter-file", parameterFile.Name(),
					"--parameter-default", "file_param=from cli default",
					"--parameter-default", "cli_param=from cli default",
					"--parameter", "cli_param=from cli",
				}
			},
			handlePty: func(ctx context.Context, stdout *expecter.Expecter, stdin *testutil.Writer) {
				// Should get prompted for the input param since it has no default.
				stdout.ExpectMatch(ctx, "input_param")
				stdin.WriteLine("from input")

				// Confirm the creation.
				stdout.ExpectMatch(ctx, "Confirm create?")
				stdin.WriteLine("yes")
			},
			withDefaults: true,
			inputParameters: []param{
				{
					name:  "template_param",
					value: "from template default",
				},
				{
					name:  "file_param",
					value: "from template default",
				},
				{
					name:  "cli_param",
					value: "from template default",
				},
				{
					name: "input_param",
				},
			},
			expectedParameters: []param{
				{
					name:  "template_param",
					value: "from template default",
				},
				{
					name:  "file_param",
					value: "from file",
				},
				{
					name:  "cli_param",
					value: "from cli",
				},
				{
					name:  "input_param",
					value: "from input",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			logger := testutil.Logger(t)
			ctx := testutil.Context(t, testutil.WaitMedium)

			parameters := params
			if len(tt.inputParameters) > 0 {
				parameters = tt.inputParameters
			}

			// Convert parameters for the echo provisioner response.
			var rparams []*proto.RichParameter
			for i, param := range parameters {
				defaultValue := ""
				if tt.withDefaults {
					defaultValue = param.value
				}
				rparams = append(rparams, &proto.RichParameter{
					Name:         param.name,
					Type:         param.ptype,
					Mutable:      param.mutable,
					DefaultValue: defaultValue,
					Order:        int32(i), //nolint:gosec
				})
			}

			// Set up the template.
			client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
			owner := coderdtest.CreateFirstUser(t, client)
			member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
			version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(rparams))

			coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
			template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

			// Run the command, possibly setting up values.
			workspaceName := "my-workspace"
			args := []string{"create", workspaceName, "--template", template.Name}
			if tt.setup != nil {
				args = append(args, tt.setup()...)
			}
			inv, root := clitest.New(t, args...)
			clitest.SetupConfig(t, member, root)
			doneChan := make(chan error)
			stdout := expecter.NewAttachedToInvocation(t, inv)
			stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
			go func() {
				doneChan <- inv.Run()
			}()

			// The test may do something with the pty.
			if tt.handlePty != nil {
				tt.handlePty(ctx, stdout, stdin)
			}

			// Wait for the command to exit.
			err := <-doneChan

			// The test may want to run additional setup like copying the workspace.
			if tt.postRun != nil {
				workspaceName = tt.postRun(t, testContext{
					client:        client,
					member:        member,
					owner:         owner,
					template:      template,
					workspaceName: workspaceName,
				})
			}

			if len(tt.errors) > 0 {
				require.Error(t, err)
				for _, errstr := range tt.errors {
					assert.ErrorContains(t, err, errstr)
				}
			} else {
				require.NoError(t, err)

				// Verify the workspace was created and has the right template and values.
				ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
				defer cancel()

				workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{Name: workspaceName})
				require.NoError(t, err, "expected to find created workspace")
				require.Len(t, workspaces.Workspaces, 1)

				workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
				require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)

				buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
				require.NoError(t, err)
				if len(tt.expectedParameters) > 0 {
					parameters = tt.expectedParameters
				}
				require.Len(t, buildParameters, len(parameters))
				for _, param := range parameters {
					require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: param.name, Value: param.value})
				}
			}
		})
	}
}

func TestCreateWithPreset(t *testing.T) {
	t.Parallel()

	const (
		firstParameterName        = "first_parameter"
		firstParameterDisplayName = "First Parameter"
		firstParameterDescription = "This is the first parameter"
		firstParameterValue       = "1"

		firstOptionalParameterName         = "first_optional_parameter"
		firstOptionalParameterDescription  = "This is the first optional parameter"
		firstOptionalParameterValue        = "1"
		secondOptionalParameterName        = "second_optional_parameter"
		secondOptionalParameterDescription = "This is the second optional parameter"
		secondOptionalParameterValue       = "2"

		thirdParameterName        = "third_parameter"
		thirdParameterDescription = "This is the third parameter"
		thirdParameterValue       = "3"
	)

	echoResponses := func(presets ...*proto.Preset) *echo.Responses {
		return prepareEchoResponses([]*proto.RichParameter{
			{
				Name:         firstParameterName,
				DisplayName:  firstParameterDisplayName,
				Description:  firstParameterDescription,
				Mutable:      true,
				DefaultValue: firstParameterValue,
				Options: []*proto.RichParameterOption{
					{
						Name:        firstOptionalParameterName,
						Description: firstOptionalParameterDescription,
						Value:       firstOptionalParameterValue,
					},
					{
						Name:        secondOptionalParameterName,
						Description: secondOptionalParameterDescription,
						Value:       secondOptionalParameterValue,
					},
				},
			},
			{
				Name:         thirdParameterName,
				Description:  thirdParameterDescription,
				DefaultValue: thirdParameterValue,
				Mutable:      true,
			},
		}, presets...)
	}

	// This test verifies that when a template has presets,
	// including a default preset, and the user specifies a `--preset` flag,
	// the CLI uses the specified preset instead of the default
	t.Run("PresetFlag", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version with two presets, including a default
		defaultPreset := proto.Preset{
			Name:    "preset-default",
			Default: true,
			Parameters: []*proto.PresetParameter{
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&defaultPreset, &preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command with the specified preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", "--preset", preset.Name)
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)

		// Should: display the selected preset as well as its parameters
		presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
		stdout.ExpectMatch(ctx, presetName)
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))

		// Verify if the new workspace uses expected parameters.
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
		defer cancel()

		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 2)
		var selectedPreset *codersdk.Preset
		for _, tvPreset := range tvPresets {
			if tvPreset.Name == preset.Name {
				selectedPreset = &tvPreset
			}
		}
		require.NotNil(t, selectedPreset)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version and the preset-defined parameters
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Equal(t, selectedPreset.ID, *workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: secondOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when a template has presets,
	// including a default preset, and the user does not specify the `--preset` flag,
	// the CLI automatically uses the default preset to create the workspace
	t.Run("DefaultPreset", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version with two presets, including a default
		defaultPreset := proto.Preset{
			Name:    "preset-default",
			Default: true,
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&defaultPreset, &preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command without a preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y")
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)

		// Should: display the default preset as well as its parameters
		presetName := fmt.Sprintf("Preset '%s' (default) applied:", defaultPreset.Name)
		stdout.ExpectMatch(ctx, presetName)
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", thirdParameterName, thirdParameterValue))

		// Verify if the new workspace uses expected parameters.
		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 2)
		var selectedPreset *codersdk.Preset
		for _, tvPreset := range tvPresets {
			if tvPreset.Default {
				selectedPreset = &tvPreset
			}
		}
		require.NotNil(t, selectedPreset)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version and the default preset parameters
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Equal(t, selectedPreset.ID, *workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: secondOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when a template has presets but no default preset,
	// and the user does not provide the `--preset` flag,
	// the CLI prompts the user to select a preset, offering "None" first.
	// Selecting "None" behaves like `--preset none`.
	t.Run("NoDefaultPresetPromptUser", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version with a single, non-default preset.
		preset := proto.Preset{
			Name:        "preset-test",
			Description: "Preset Test.",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command without specifying a preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name,
			"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
			"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		// Should: prompt the user for the preset
		stdout.ExpectMatch(ctx, "Select a preset below:")
		// We don't actually have to respond to the selector, since we hardcode the cliui.Select to return the
		// first option in test scenarios (c.f. cliui/select.go). The first option is "None".
		stdout.ExpectMatch(ctx, "No preset applied.")
		stdout.ExpectMatch(ctx, "Confirm create?")
		stdin.WriteLine("yes")

		<-doneChan

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version, no preset,
		// and the parameters from the command line flags
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Nil(t, workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when a template version has no presets,
	// the CLI does not prompt the user to select a preset and proceeds
	// with workspace creation without applying any preset.
	t.Run("TemplateVersionWithoutPresets", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version without presets
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command without a preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y",
			"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
			"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)
		stdout.ExpectMatch(ctx, "No preset applied.")

		// Verify if the new workspace uses expected parameters.
		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version and no preset
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Nil(t, workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when the user provides `--preset none`,
	// the CLI skips applying any preset, even if the template version has a default preset.
	// The workspace should be created without using any preset-defined parameters.
	t.Run("PresetFlagNone", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version with a default preset
		preset := proto.Preset{
			Name:    "preset-test",
			Default: true,
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command with flag '--preset none'
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", "--preset", cli.PresetNone,
			"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
			"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)
		stdout.ExpectMatch(ctx, "No preset applied.")

		// Verify that the new workspace doesn't use the preset parameters.
		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 1)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version and no preset
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Nil(t, workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that the CLI returns an appropriate error
	// when a user provides a `--preset` value that does not correspond
	// to any existing preset in the template version.
	t.Run("FailsWhenPresetDoesNotExist", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template and a template version where the preset defines values for all required parameters
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
				{Name: thirdParameterName, Value: thirdParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command with a non-existent preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y", "--preset", "invalid-preset")
		clitest.SetupConfig(t, member, root)
		err := inv.Run()

		// Should: fail with an error indicating the preset was not found
		require.Contains(t, err.Error(), "preset \"invalid-preset\" not found")
	})

	// This test verifies that when both a preset and a user-provided
	// `--parameter` flag define a value for the same parameter,
	// the preset's value takes precedence over the user's.
	//
	// The preset defines one parameter (A), and two `--parameter` flags provide A and B.
	// The workspace should be created using:
	// - the value of parameter A from the preset (overriding the parameter flag's value),
	// - and the value of parameter B from the parameter flag.
	t.Run("PresetOverridesParameterFlagValues", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template version with a preset that defines one parameter
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: creating a workspace with a preset and passing overlapping and additional parameters via `--parameter`
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y",
			"--preset", preset.Name,
			"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstOptionalParameterValue),
			"--parameter", fmt.Sprintf("%s=%s", thirdParameterName, thirdParameterValue))
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)

		// Should: display the selected preset as well as its parameter
		presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
		stdout.ExpectMatch(ctx, presetName)
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))

		// Verify if the new workspace uses expected parameters.
		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 1)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: include both parameters, one from the preset and one from the `--parameter` flag
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Equal(t, tvPresets[0].ID, *workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: secondOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when both a preset and a user-provided
	// `--rich-parameter-file` define a value for the same parameter,
	// the preset's value takes precedence over the one in the file.
	//
	// The preset defines one parameter (A), and the parameter file provides two parameters (A and B).
	// The workspace should be created using:
	// - the value of parameter A from the preset (overriding the file's value),
	// - and the value of parameter B from the file.
	t.Run("PresetOverridesParameterFileValues", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template version with a preset that defines one parameter
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: creating a workspace with the preset and passing the second required parameter via `--rich-parameter-file`
		workspaceName := "my-workspace"
		tempDir := t.TempDir()
		removeTmpDirUntilSuccessAfterTest(t, tempDir)
		parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
		_, _ = parameterFile.WriteString(
			firstParameterName + ": " + firstOptionalParameterValue + "\n" +
				thirdParameterName + ": " + thirdParameterValue)
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "-y",
			"--preset", preset.Name,
			"--rich-parameter-file", parameterFile.Name())
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		err := inv.Run()
		require.NoError(t, err)

		// Should: display the selected preset as well as its parameter
		presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
		stdout.ExpectMatch(ctx, presetName)
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))

		// Verify if the new workspace uses expected parameters.
		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 1)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: include both parameters, one from the preset and one from the `--rich-parameter-file` flag
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Equal(t, tvPresets[0].ID, *workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: secondOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})

	// This test verifies that when a preset provides only some parameters,
	// and the remaining ones are not provided via flags,
	// the CLI prompts the user for input to fill in the missing parameters.
	t.Run("PromptsForMissingParametersWhenPresetIsIncomplete", func(t *testing.T) {
		t.Parallel()
		ctx := testutil.Context(t, testutil.WaitMedium)
		logger := testutil.Logger(t)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)

		// Given: a template version with a preset that defines one parameter
		preset := proto.Preset{
			Name: "preset-test",
			Parameters: []*proto.PresetParameter{
				{Name: firstParameterName, Value: secondOptionalParameterValue},
			},
		}
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses(&preset))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		// When: running the create command with the specified preset
		workspaceName := "my-workspace"
		inv, root := clitest.New(t, "create", workspaceName, "--template", template.Name, "--preset", preset.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		// Should: display the selected preset as well as its parameters
		presetName := fmt.Sprintf("Preset '%s' applied:", preset.Name)
		stdout.ExpectMatch(ctx, presetName)
		stdout.ExpectMatch(ctx, fmt.Sprintf("%s: '%s'", firstParameterName, secondOptionalParameterValue))

		// Should: prompt for the missing parameter
		stdout.ExpectMatch(ctx, thirdParameterDescription)
		stdin.WriteLine(thirdParameterValue)
		stdout.ExpectMatch(ctx, "Confirm create?")
		stdin.WriteLine("yes")

		<-doneChan

		// Verify if the new workspace uses expected parameters.
		tvPresets, err := client.TemplateVersionPresets(ctx, version.ID)
		require.NoError(t, err)
		require.Len(t, tvPresets, 1)

		workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
			Name: workspaceName,
		})
		require.NoError(t, err)
		require.Len(t, workspaces.Workspaces, 1)

		// Should: create a workspace using the expected template version and the preset-defined parameters
		workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
		require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
		require.Equal(t, tvPresets[0].ID, *workspaceLatestBuild.TemplateVersionPresetID)
		buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
		require.NoError(t, err)
		require.Len(t, buildParameters, 2)
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: secondOptionalParameterValue})
		require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: thirdParameterName, Value: thirdParameterValue})
	})
}

func TestCreateValidateRichParameters(t *testing.T) {
	t.Parallel()

	const (
		stringParameterName  = "string_parameter"
		stringParameterValue = "abc"

		listOfStringsParameterName = "list_of_strings_parameter"

		numberParameterName  = "number_parameter"
		numberParameterValue = "7"

		boolParameterName  = "bool_parameter"
		boolParameterValue = "true"
	)

	numberRichParameters := []*proto.RichParameter{
		{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: new(int32(3)), ValidationMax: new(int32(10))},
	}

	numberCustomErrorRichParameters := []*proto.RichParameter{
		{
			Name: numberParameterName, Type: "number", Mutable: true,
			ValidationMin: new(int32(3)), ValidationMax: new(int32(10)),
			ValidationError: "These are values: {min}, {max}, and {value}.",
		},
	}

	stringRichParameters := []*proto.RichParameter{
		{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
	}

	listOfStringsRichParameters := []*proto.RichParameter{
		{Name: listOfStringsParameterName, Type: "list(string)", Mutable: true, DefaultValue: `["aaa","bbb","ccc"]`},
	}

	boolRichParameters := []*proto.RichParameter{
		{Name: boolParameterName, Type: "bool", Mutable: true},
	}

	t.Run("ValidateString", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)
		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)

		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		matches := []string{
			stringParameterName, "$$",
			"does not match", "",
			"Enter a value", "abc",
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			if value != "" {
				stdin.WriteLine(value)
			}
		}
		<-doneChan
	})

	t.Run("ValidateNumber", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(numberRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)

		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		matches := []string{
			numberParameterName, "12",
			"is more than the maximum", "",
			"Enter a value", "8",
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			if value != "" {
				stdin.WriteLine(value)
			}
		}
		<-doneChan
	})

	t.Run("ValidateNumber_CustomError", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(numberCustomErrorRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)

		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		matches := []string{
			numberParameterName, "12",
			"These are values: 3, 10, and 12.", "",
			"Enter a value", "8",
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			if value != "" {
				stdin.WriteLine(value)
			}
		}
		<-doneChan
	})

	t.Run("ValidateBool", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(boolRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)

		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
		clitest.SetupConfig(t, member, root)
		doneChan := make(chan struct{})
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		go func() {
			defer close(doneChan)
			err := inv.Run()
			assert.NoError(t, err)
		}()

		matches := []string{
			boolParameterName, "cat",
			"boolean value can be either", "",
			"Enter a value", "true",
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			if value != "" {
				stdin.WriteLine(value)
			}
		}
		<-doneChan
	})

	t.Run("ValidateListOfStrings", func(t *testing.T) {
		t.Parallel()

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		t.Run("Prompt", func(t *testing.T) {
			logger := testutil.Logger(t)
			ctx := testutil.Context(t, testutil.WaitMedium)
			inv, root := clitest.New(t, "create", "my-workspace-1", "--template", template.Name)
			clitest.SetupConfig(t, member, root)
			stdout := expecter.NewAttachedToInvocation(t, inv)
			stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
			clitest.Start(t, inv)

			stdout.ExpectMatch(ctx, listOfStringsParameterName)
			stdout.ExpectMatch(ctx, "aaa, bbb, ccc")
			stdout.ExpectMatch(ctx, "Confirm create?")
			stdin.WriteLine("yes")
		})

		t.Run("Default", func(t *testing.T) {
			t.Parallel()
			inv, root := clitest.New(t, "create", "my-workspace-2", "--template", template.Name, "--yes")
			clitest.SetupConfig(t, member, root)
			clitest.Run(t, inv)
		})

		t.Run("CLIOverride/DoubleQuote", func(t *testing.T) {
			t.Parallel()

			// Note: see https://go.dev/play/p/vhTUTZsVrEb for how to escape this properly
			parameterArg := fmt.Sprintf(`"%s=[""ddd=foo"",""eee=bar"",""fff=baz""]"`, listOfStringsParameterName)
			inv, root := clitest.New(t, "create", "my-workspace-3", "--template", template.Name, "--parameter", parameterArg, "--yes")
			clitest.SetupConfig(t, member, root)
			clitest.Run(t, inv)
		})
	})

	t.Run("ValidateListOfStrings_YAMLFile", func(t *testing.T) {
		t.Parallel()
		logger := testutil.Logger(t)
		ctx := testutil.Context(t, testutil.WaitMedium)

		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
		owner := coderdtest.CreateFirstUser(t, client)
		member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
		version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
		coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
		template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

		tempDir := t.TempDir()
		removeTmpDirUntilSuccessAfterTest(t, tempDir)
		parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
		_, _ = parameterFile.WriteString(listOfStringsParameterName + `:
  - ddd
  - eee
  - fff`)
		inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name())
		clitest.SetupConfig(t, member, root)
		stdout := expecter.NewAttachedToInvocation(t, inv)
		stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
		clitest.Start(t, inv)

		matches := []string{
			"Confirm create?", "yes",
		}
		for i := 0; i < len(matches); i += 2 {
			match := matches[i]
			value := matches[i+1]
			stdout.ExpectMatch(ctx, match)
			if value != "" {
				stdin.WriteLine(value)
			}
		}
	})
}

func TestCreateWithGitAuth(t *testing.T) {
	t.Parallel()
	logger := testutil.Logger(t)
	ctx := testutil.Context(t, testutil.WaitMedium)
	echoResponses := &echo.Responses{
		Parse:         echo.ParseComplete,
		ProvisionInit: echo.InitComplete,
		ProvisionPlan: echo.PlanComplete,
		ProvisionGraph: []*proto.Response{
			{
				Type: &proto.Response_Graph{
					Graph: &proto.GraphComplete{
						ExternalAuthProviders: []*proto.ExternalAuthProviderResource{{Id: "github"}},
					},
				},
			},
		},
		ProvisionApply: echo.ApplyComplete,
	}

	client := coderdtest.New(t, &coderdtest.Options{
		ExternalAuthConfigs: []*externalauth.Config{{
			InstrumentedOAuth2Config: &testutil.OAuth2Config{},
			ID:                       "github",
			Regex:                    regexp.MustCompile(`github\.com`),
			Type:                     codersdk.EnhancedExternalAuthProviderGitHub.String(),
			DisplayName:              "GitHub",
			RefreshGroup:             new(singleflight.Group),
		}},
		IncludeProvisionerDaemon: true,
	})
	owner := coderdtest.CreateFirstUser(t, client)
	member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
	version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
	coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
	template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)

	inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
	clitest.SetupConfig(t, member, root)
	stdout := expecter.NewAttachedToInvocation(t, inv)
	stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
	clitest.Start(t, inv)

	stdout.ExpectMatch(ctx, "You must authenticate with GitHub to create a workspace")
	resp := coderdtest.RequestExternalAuthCallback(t, "github", member)
	_ = resp.Body.Close()
	require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
	stdout.ExpectMatch(ctx, "Confirm create?")
	stdin.WriteLine("yes")
}