package apptest

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/http/httputil"
	"net/url"
	"path"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v3"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/xerrors"

	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/workspaceapps"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/codersdk/workspacesdk"
	"github.com/coder/coder/v2/testutil"
)

// Run runs the entire workspace app test suite against deployments minted
// by the provided factory.
//
// appHostIsPrimary is true if the app host is also the primary coder API
// server. This disables any tests that test API passthrough or rely on the
// app server not being the API server.
// nolint:revive
func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
	setupProxyTest := func(t *testing.T, opts *DeploymentOptions) *Details {
		return setupProxyTestWithFactory(t, factory, opts)
	}

	t.Run("ReconnectingPTY", func(t *testing.T) {
		t.Parallel()
		if runtime.GOOS == "windows" {
			// This might be our implementation, or ConPTY itself.  It's
			// difficult to find extensive tests for it, so it seems like it
			// could be either.
			t.Skip("ConPTY appears to be inconsistent on Windows.")
		}

		t.Run("OK", func(t *testing.T) {
			t.Parallel()
			appDetails := setupProxyTest(t, nil)

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			// Run the test against the path app hostname since that's where the
			// reconnecting-pty proxy server we want to test is mounted.
			client := appDetails.AppClient(t)
			testReconnectingPTY(ctx, t, client, appDetails.Agent.ID, "")
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			if appHostIsPrimary {
				t.Skip("Tickets are not used for terminal requests on the primary.")
			}

			appDetails := setupProxyTest(t, nil)

			u := *appDetails.PathAppBaseURL
			if u.Scheme == "http" {
				u.Scheme = "ws"
			} else {
				u.Scheme = "wss"
			}
			u.Path = fmt.Sprintf("/api/v2/workspaceagents/%s/pty", appDetails.Agent.ID.String())

			ctx := testutil.Context(t, testutil.WaitLong)
			issueRes, err := appDetails.SDKClient.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
				URL:     u.String(),
				AgentID: appDetails.Agent.ID,
			})
			require.NoError(t, err)

			// Make an unauthenticated client.
			unauthedAppClient := codersdk.New(appDetails.AppClient(t).URL)
			testReconnectingPTY(ctx, t, unauthedAppClient, appDetails.Agent.ID, issueRes.SignedToken)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})
	})

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

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

			appDetails := setupProxyTest(t, &DeploymentOptions{
				DisablePathApps: true,
			})

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appDetails.PathAppURL(appDetails.Apps.Owner).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusForbidden, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Contains(t, string(body), "Path-based applications are disabled")
			// Even though path-based apps are disabled, the request should indicate
			// that the workspace was used.
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			if !appHostIsPrimary {
				t.Skip("This test only applies when testing apps on the primary.")
			}

			appDetails := setupProxyTest(t, nil)
			unauthedClient := appDetails.AppClient(t)
			unauthedClient.SetSessionToken("")

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner).String()
			resp, err := requestWithRetries(ctx, t, unauthedClient, http.MethodGet, u, nil)
			require.NoError(t, err)
			defer resp.Body.Close()

			require.Equal(t, http.StatusSeeOther, resp.StatusCode)
			loc, err := resp.Location()
			require.NoError(t, err)
			require.True(t, loc.Query().Has("message"))
			require.True(t, loc.Query().Has("redirect"))
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			if appHostIsPrimary {
				t.Skip("This test only applies when testing apps on workspace proxies.")
			}

			appDetails := setupProxyTest(t, nil)
			unauthedClient := appDetails.AppClient(t)
			unauthedClient.SetSessionToken("")

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner)
			resp, err := requestWithRetries(ctx, t, unauthedClient, http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()

			require.Equal(t, http.StatusSeeOther, resp.StatusCode)
			loc, err := resp.Location()
			require.NoError(t, err)
			require.Equal(t, appDetails.SDKClient.URL.Host, loc.Host)
			require.Equal(t, "/api/v2/applications/auth-redirect", loc.Path)

			redirectURIStr := loc.Query().Get("redirect_uri")
			require.NotEmpty(t, redirectURIStr)
			redirectURI, err := url.Parse(redirectURIStr)
			require.NoError(t, err)

			require.Equal(t, u.Scheme, redirectURI.Scheme)
			require.Equal(t, u.Host, redirectURI.Host)
			// TODO(@dean): I have no idea how but the trailing slash on this
			// request is getting stripped.
			require.Equal(t, u.Path, redirectURI.Path+"/")
			require.Equal(t, u.RawQuery, redirectURI.RawQuery)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
			userAppClient := appDetails.AppClient(t)
			userAppClient.SetSessionToken(userClient.SessionToken())

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.PathAppURL(appDetails.Apps.Owner).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusNotFound, resp.StatusCode)
			// TODO(cian): A blocked request should not count as workspace usage.
			// assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner)
			u.Path = strings.TrimSuffix(u.Path, "/")
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
			// TODO(cian): The initial redirect should not count as workspace usage.
			// assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner)
			u.RawQuery = ""
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
			loc, err := resp.Location()
			require.NoError(t, err)
			require.Equal(t, proxyTestAppQuery, loc.RawQuery)
			// TODO(cian): The initial redirect should not count as workspace usage.
			// assertWorkspaceLastUsedAtNotUpdated(t, appDetails.AppClient(t), appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)

			var appTokenCookie *http.Cookie
			for _, c := range resp.Cookies() {
				if c.Name == codersdk.SignedAppTokenCookie {
					appTokenCookie = c
					break
				}
			}
			require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
			require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

			// Ensure the signed app token cookie is valid.
			appTokenClient := appDetails.AppClient(t)
			appTokenClient.SetSessionToken("")
			appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
			require.NoError(t, err)
			appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

			resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err = io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, &DeploymentOptions{
				ServeHTTPS: true,
			})

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.PathAppURL(appDetails.Apps.Owner)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)

			var appTokenCookie *http.Cookie
			for _, c := range resp.Cookies() {
				if c.Name == codersdk.SignedAppTokenCookie {
					appTokenCookie = c
					break
				}
			}
			require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
			require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

			// Ensure the signed app token cookie is valid.
			appTokenClient := appDetails.AppClient(t)
			appTokenClient.SetSessionToken("")
			appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
			require.NoError(t, err)
			appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

			resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err = io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			app := appDetails.Apps.Owner
			app.Username = codersdk.Me

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appDetails.PathAppURL(app).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusNotFound, resp.StatusCode)

			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Contains(t, string(body), "must be accessed with the full username, not @me")
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appDetails.PathAppURL(appDetails.Apps.Owner).String(), nil, func(r *http.Request) {
				r.Header.Set("Cf-Connecting-IP", "1.1.1.1")
			})
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			require.Equal(t, "1.1.1.1,127.0.0.1", resp.Header.Get("X-Forwarded-For"))
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, appDetails.PathAppURL(appDetails.Apps.Fake).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusBadGateway, resp.StatusCode)
			// An valid authenticated attempt to access a workspace app
			// should count as usage regardless of success.
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, appDetails.PathAppURL(appDetails.Apps.Port).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			// TODO(@deansheather): This should be 400. There's a todo in the
			// resolve request code to fix this.
			require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})
	})

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

		// The OK test checks the entire end-to-end flow of authentication.
		t.Run("End-to-End", func(t *testing.T) {
			t.Parallel()

			appDetails := setupProxyTest(t, nil)

			cases := []struct {
				name                   string
				appURL                 *url.URL
				sessionTokenCookieName string
			}{
				{
					name:                   "Subdomain",
					appURL:                 appDetails.SubdomainAppURL(appDetails.Apps.Owner),
					sessionTokenCookieName: codersdk.SubdomainAppSessionTokenCookie,
				},
				{
					name:                   "Path",
					appURL:                 appDetails.PathAppURL(appDetails.Apps.Owner),
					sessionTokenCookieName: codersdk.PathAppSessionTokenCookie,
				},
			}

			for _, c := range cases {
				c := c

				if c.name == "Path" && appHostIsPrimary {
					// Workspace application auth does not apply to path apps
					// served from the primary access URL as no smuggling needs
					// to take place (they're already logged in with a session
					// token).
					continue
				}

				t.Run(c.name, func(t *testing.T) {
					t.Parallel()

					ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
					defer cancel()

					// Get the current user and API key.
					user, err := appDetails.SDKClient.User(ctx, codersdk.Me)
					require.NoError(t, err)
					currentAPIKey, err := appDetails.SDKClient.APIKeyByID(ctx, appDetails.FirstUser.UserID.String(), strings.Split(appDetails.SDKClient.SessionToken(), "-")[0])
					require.NoError(t, err)

					appClient := appDetails.AppClient(t)
					appClient.SetSessionToken("")

					// Try to load the application without authentication.
					u := c.appURL
					u.Path = path.Join(u.Path, "/test")
					req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
					require.NoError(t, err)

					var resp *http.Response
					resp, err = doWithRetries(t, appClient, req)
					require.NoError(t, err)

					if !assert.Equal(t, http.StatusSeeOther, resp.StatusCode) {
						dump, err := httputil.DumpResponse(resp, true)
						require.NoError(t, err)
						t.Log(string(dump))
					}
					resp.Body.Close()

					// Check that the Location is correct.
					gotLocation, err := resp.Location()
					require.NoError(t, err)
					// This should always redirect to the primary access URL.
					require.Equal(t, appDetails.SDKClient.URL.Host, gotLocation.Host)
					require.Equal(t, "/api/v2/applications/auth-redirect", gotLocation.Path)
					require.Equal(t, u.String(), gotLocation.Query().Get("redirect_uri"))

					// Load the application auth-redirect endpoint.
					resp, err = requestWithRetries(ctx, t, appDetails.SDKClient, http.MethodGet, "/api/v2/applications/auth-redirect", nil, codersdk.WithQueryParam(
						"redirect_uri", u.String(),
					))
					require.NoError(t, err)
					defer resp.Body.Close()

					require.Equal(t, http.StatusSeeOther, resp.StatusCode)
					gotLocation, err = resp.Location()
					require.NoError(t, err)

					// Copy the query parameters and then check equality.
					u.RawQuery = gotLocation.RawQuery
					require.Equal(t, u, gotLocation)

					// Verify the API key is set.
					encryptedAPIKey := gotLocation.Query().Get(workspaceapps.SubdomainProxyAPIKeyParam)
					require.NotEmpty(t, encryptedAPIKey, "no API key was set in the query parameters")

					// Decrypt the API key by following the request.
					t.Log("navigating to: ", gotLocation.String())
					req, err = http.NewRequestWithContext(ctx, "GET", gotLocation.String(), nil)
					require.NoError(t, err)
					resp, err = doWithRetries(t, appClient, req)
					require.NoError(t, err)
					resp.Body.Close()
					require.Equal(t, http.StatusSeeOther, resp.StatusCode)

					cookies := resp.Cookies()
					var cookie *http.Cookie
					for _, co := range cookies {
						if co.Name == c.sessionTokenCookieName {
							cookie = co
							break
						}
					}
					require.NotNil(t, cookie, "no app session token cookie was set")
					apiKey := cookie.Value

					// Fetch the API key from the API.
					apiKeyInfo, err := appDetails.SDKClient.APIKeyByID(ctx, appDetails.FirstUser.UserID.String(), strings.Split(apiKey, "-")[0])
					require.NoError(t, err)
					require.Equal(t, user.ID, apiKeyInfo.UserID)
					require.Equal(t, codersdk.LoginTypePassword, apiKeyInfo.LoginType)
					require.WithinDuration(t, currentAPIKey.ExpiresAt, apiKeyInfo.ExpiresAt, 5*time.Second)
					require.EqualValues(t, currentAPIKey.LifetimeSeconds, apiKeyInfo.LifetimeSeconds)

					// Verify the API key permissions
					appTokenAPIClient := codersdk.New(appDetails.SDKClient.URL)
					appTokenAPIClient.SetSessionToken(apiKey)
					appTokenAPIClient.HTTPClient.CheckRedirect = appDetails.SDKClient.HTTPClient.CheckRedirect
					appTokenAPIClient.HTTPClient.Transport = appDetails.SDKClient.HTTPClient.Transport

					var (
						canCreateApplicationConnect = "can-create-application_connect"
						canReadUserMe               = "can-read-user-me"
					)
					authRes, err := appTokenAPIClient.AuthCheck(ctx, codersdk.AuthorizationRequest{
						Checks: map[string]codersdk.AuthorizationCheck{
							canCreateApplicationConnect: {
								Object: codersdk.AuthorizationObject{
									ResourceType:   "application_connect",
									OwnerID:        "me",
									OrganizationID: appDetails.FirstUser.OrganizationID.String(),
								},
								Action: "create",
							},
							canReadUserMe: {
								Object: codersdk.AuthorizationObject{
									ResourceType: "user",
									OwnerID:      "me",
									ResourceID:   appDetails.FirstUser.UserID.String(),
								},
								Action: "read",
							},
						},
					})
					require.NoError(t, err)

					require.True(t, authRes[canCreateApplicationConnect])
					require.False(t, authRes[canReadUserMe])

					// Load the application page with the API key set.
					gotLocation, err = resp.Location()
					require.NoError(t, err)
					t.Log("navigating to: ", gotLocation.String())
					req, err = http.NewRequestWithContext(ctx, "GET", gotLocation.String(), nil)
					require.NoError(t, err)
					req.Header.Set(codersdk.SessionTokenHeader, apiKey)
					resp, err = doWithRetries(t, appClient, req)
					require.NoError(t, err)
					resp.Body.Close()
					require.Equal(t, http.StatusOK, resp.StatusCode)
				})
			}
		})
	})

	t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/OK", func(t *testing.T) {
		t.Parallel()

		appDetails := setupProxyTest(t, nil)

		// Try to load the owner app with a prefix.
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		prefixedOwnerApp := appDetails.Apps.Owner
		prefixedOwnerApp.Prefix = "some---prefix---"

		u := appDetails.SubdomainAppURL(prefixedOwnerApp)
		require.Contains(t, u.Host, prefixedOwnerApp.Prefix)

		resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		_ = resp.Body.Close()
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)

		// Parse the returned signed token to verify that it contains the
		// prefix.
		var appTokenCookie *http.Cookie
		for _, c := range resp.Cookies() {
			if c.Name == codersdk.SignedAppTokenCookie {
				appTokenCookie = c
				break
			}
		}
		require.NotNil(t, appTokenCookie, "no signed app token cookie in response")

		// Parse the JWT without verifying it (since we can't access the key
		// from this test).
		object, err := jose.ParseSigned(appTokenCookie.Value)
		require.NoError(t, err)
		require.Len(t, object.Signatures, 1)

		// Parse the payload.
		var tok workspaceapps.SignedToken
		//nolint:gosec
		err = json.Unmarshal(object.UnsafePayloadWithoutVerification(), &tok)
		require.NoError(t, err)

		// Verify the prefix is in the token.
		require.Equal(t, prefixedOwnerApp.Prefix, tok.Request.Prefix)

		// Ensure the signed app token cookie is valid by making a request with
		// it with no session token.
		appTokenClient := appDetails.AppClient(t)
		appTokenClient.SetSessionToken("")
		appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
		require.NoError(t, err)
		appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

		resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		_ = resp.Body.Close()
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.Equal(t, resp.Header.Get("X-Got-Host"), u.Host)
		assertWorkspaceLastUsedAtUpdated(t, appDetails)
	})

	t.Run("WorkspaceAppsProxySubdomainHostnamePrefix/Different", func(t *testing.T) {
		t.Parallel()

		appDetails := setupProxyTest(t, nil)

		// Try to load the owner app with a prefix.
		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		prefixedOwnerApp := appDetails.Apps.Owner
		t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))
		prefixedOwnerApp.Prefix = "some---prefix---"
		t.Log(appDetails.SubdomainAppURL(prefixedOwnerApp))

		u := appDetails.SubdomainAppURL(prefixedOwnerApp)
		require.Contains(t, u.Host, prefixedOwnerApp.Prefix)

		resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		_ = resp.Body.Close()
		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Find the cookie.
		var appTokenCookie *http.Cookie
		for _, c := range resp.Cookies() {
			if c.Name == codersdk.SignedAppTokenCookie {
				appTokenCookie = c
				break
			}
		}
		require.NotNil(t, appTokenCookie, "no signed app token cookie in response")

		// Ensure the signed app token cookie is valid only for the given prefix
		// by making a request with it with no session token.
		appTokenClient := appDetails.AppClient(t)
		appTokenClient.SetSessionToken("")
		appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
		require.NoError(t, err)
		appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

		prefixedOwnerApp.Prefix = "different---"
		u = appDetails.SubdomainAppURL(prefixedOwnerApp)
		require.Contains(t, u.Host, prefixedOwnerApp.Prefix)

		resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		_ = resp.Body.Close()
		require.NotEqual(t, http.StatusOK, resp.StatusCode)
		assertWorkspaceLastUsedAtUpdated(t, appDetails)
	})

	// This test ensures that the subdomain handler does nothing if
	// --app-hostname is not set by the admin.
	t.Run("WorkspaceAppsProxySubdomainPassthrough", func(t *testing.T) {
		t.Parallel()
		if !appHostIsPrimary {
			t.Skip("app hostname does not serve API")
		}
		// No Hostname set.
		appDetails := setupProxyTest(t, &DeploymentOptions{
			AppHost:              "",
			DisableSubdomainApps: true,
			noWorkspace:          true,
		})

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		u := *appDetails.SDKClient.URL
		u.Host = "app--agent--workspace--username.test.coder.com"
		u.Path = "/api/v2/users/me"
		resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should look like a codersdk.User response.
		require.Equal(t, http.StatusOK, resp.StatusCode)
		var user codersdk.User
		err = json.NewDecoder(resp.Body).Decode(&user)
		require.NoError(t, err)
		require.Equal(t, appDetails.FirstUser.UserID, user.ID)
	})

	// This test ensures that the subdomain handler blocks the request if it
	// looks like a workspace app request but the configured app hostname
	// differs from the request, or the request is not a valid app subdomain but
	// the hostname matches.
	t.Run("WorkspaceAppsProxySubdomainBlocked", func(t *testing.T) {
		t.Parallel()

		appDetails := setupProxyTest(t, &DeploymentOptions{
			noWorkspace: true,
		})

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

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			host := strings.Replace(appDetails.Options.AppHost, "*", "not-an-app-subdomain", 1)
			uri := fmt.Sprintf("http://%s/api/v2/users/me", host)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, uri, nil)
			require.NoError(t, err)
			defer resp.Body.Close()

			// Should have a HTML error response.
			require.Equal(t, http.StatusBadRequest, resp.StatusCode)
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Contains(t, string(body), "Could not parse subdomain application URL")
		})
	})

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

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

			appDetails := setupProxyTest(t, nil)
			userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
			userAppClient := appDetails.AppClient(t)
			userAppClient.SetSessionToken(userClient.SessionToken())

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Owner).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusNotFound, resp.StatusCode)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			u.Path = ""
			u.RawQuery = ""
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)

			loc, err := resp.Location()
			require.NoError(t, err)
			require.Equal(t, appDetails.SubdomainAppURL(appDetails.Apps.Owner).Path, loc.Path)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			u.RawQuery = ""
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)

			loc, err := resp.Location()
			require.NoError(t, err)
			require.Equal(t, appDetails.SubdomainAppURL(appDetails.Apps.Owner).RawQuery, loc.RawQuery)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)

			var appTokenCookie *http.Cookie
			for _, c := range resp.Cookies() {
				if c.Name == codersdk.SignedAppTokenCookie {
					appTokenCookie = c
					break
				}
			}
			require.NotNil(t, appTokenCookie, "no signed token cookie in response")
			require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")

			// Ensure the signed app token cookie is valid.
			appTokenClient := appDetails.AppClient(t)
			appTokenClient.SetSessionToken("")
			appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
			require.NoError(t, err)
			appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

			resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err = io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, &DeploymentOptions{
				ServeHTTPS: true,
			})
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)

			var appTokenCookie *http.Cookie
			for _, c := range resp.Cookies() {
				if c.Name == codersdk.SignedAppTokenCookie {
					appTokenCookie = c
					break
				}
			}
			require.NotNil(t, appTokenCookie, "no signed token cookie in response")
			require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")

			// Ensure the signed app token cookie is valid.
			appTokenClient := appDetails.AppClient(t)
			appTokenClient.SetSessionToken("")
			appTokenClient.HTTPClient.Jar, err = cookiejar.New(nil)
			require.NoError(t, err)
			appTokenClient.HTTPClient.Jar.SetCookies(u, []*http.Cookie{appTokenCookie})

			resp, err = requestWithRetries(ctx, t, appTokenClient, http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err = io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Fake).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusBadGateway, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, nil)
			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			app := appDetails.Apps.Port
			app.AppSlugOrPort = strconv.Itoa(workspacesdk.AgentMinimumListeningPort - 1)
			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appDetails.SubdomainAppURL(app).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()

			// Should have an error response.
			require.Equal(t, http.StatusBadRequest, resp.StatusCode)
			var resBody codersdk.Response
			err = json.NewDecoder(resp.Body).Decode(&resBody)
			require.NoError(t, err)
			require.Contains(t, resBody.Message, "Coder reserves ports less than")
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			appDetails := setupProxyTest(t, &DeploymentOptions{
				AppHost: "*-suffix.test.coder.com",
			})

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			t.Logf("url: %s", u)

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			// Manually specifying a port should override the access url port on
			// the app host.
			appDetails := setupProxyTest(t, &DeploymentOptions{
				// Just throw both the wsproxy and primary to same url.
				AppHost:        "*.test.coder.com:4444",
				PrimaryAppHost: "*.test.coder.com:4444",
			})

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
			t.Logf("url: %s", u)
			require.Equal(t, "4444", u.Port(), "port should be 4444")

			// Assert the api response the UI uses has the port.
			apphost, err := appDetails.SDKClient.AppHost(ctx)
			require.NoError(t, err)
			require.Equal(t, "*.test.coder.com:4444", apphost.Host, "apphost has port")

			resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			body, err := io.ReadAll(resp.Body)
			require.NoError(t, err)
			require.Equal(t, proxyTestAppBody, string(body))
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

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

				appDetails := setupProxyTest(t, &DeploymentOptions{
					AppHost: "*-suffix.test.coder.com",
				})

				ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
				defer cancel()

				u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
				// Replace the -suffix with nothing.
				u.Host = strings.Replace(u.Host, "-suffix", "", 1)
				t.Logf("url: %s", u)

				resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
				require.NoError(t, err)
				defer resp.Body.Close()
				body, err := io.ReadAll(resp.Body)
				require.NoError(t, err)

				// It's probably rendering the dashboard or a 404 page, so only
				// ensure that the body doesn't match.
				require.NotContains(t, string(body), proxyTestAppBody)
				assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
			})

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

				appDetails := setupProxyTest(t, &DeploymentOptions{
					AppHost: "*-suffix.test.coder.com",
				})

				ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
				defer cancel()

				u := appDetails.SubdomainAppURL(appDetails.Apps.Owner)
				// Replace the -suffix with something else.
				u.Host = strings.Replace(u.Host, "-suffix", "-not-suffix", 1)
				t.Logf("url: %s", u)

				resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
				require.NoError(t, err)
				defer resp.Body.Close()
				body, err := io.ReadAll(resp.Body)
				require.NoError(t, err)

				// It's probably rendering the dashboard, so only ensure that the body
				// doesn't match.
				require.NotContains(t, string(body), proxyTestAppBody)
				assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
			})
		})
	})

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

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			appDetails := setupProxyTest(t, nil)
			userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
			userAppClient := appDetails.AppClient(t)
			userAppClient.SetSessionToken(userClient.SessionToken())

			resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusNotFound, resp.StatusCode)
			assertWorkspaceLastUsedAtNotUpdated(t, appDetails)
		})

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

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			appDetails := setupProxyTest(t, nil)
			port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
			require.NoError(t, err)
			// set the port we have to be shared with authenticated users
			_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
				AgentName:  proxyTestAgentName,
				Port:       int32(port),
				ShareLevel: codersdk.WorkspaceAgentPortShareLevelAuthenticated,
				Protocol:   codersdk.WorkspaceAgentPortShareProtocolHTTP,
			})
			require.NoError(t, err)

			userClient, _ := coderdtest.CreateAnotherUser(t, appDetails.SDKClient, appDetails.FirstUser.OrganizationID, rbac.RoleMember())
			userAppClient := appDetails.AppClient(t)
			userAppClient.SetSessionToken(userClient.SessionToken())

			resp, err := requestWithRetries(ctx, t, userAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			appDetails := setupProxyTest(t, nil)
			port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
			require.NoError(t, err)
			// set the port we have to be shared with public
			_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
				AgentName:  proxyTestAgentName,
				Port:       int32(port),
				ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
				Protocol:   codersdk.WorkspaceAgentPortShareProtocolHTTP,
			})
			require.NoError(t, err)

			publicAppClient := appDetails.AppClient(t)
			publicAppClient.SetSessionToken("")

			resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.Port).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})

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

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			appDetails := setupProxyTest(t, &DeploymentOptions{
				ServeHTTPS: true,
			})
			// using the fact that Apps.Port and Apps.PortHTTPS are the same port here
			port, err := strconv.ParseInt(appDetails.Apps.Port.AppSlugOrPort, 10, 32)
			require.NoError(t, err)
			_, err = appDetails.SDKClient.UpsertWorkspaceAgentPortShare(ctx, appDetails.Workspace.ID, codersdk.UpsertWorkspaceAgentPortShareRequest{
				AgentName:  proxyTestAgentName,
				Port:       int32(port),
				ShareLevel: codersdk.WorkspaceAgentPortShareLevelPublic,
				Protocol:   codersdk.WorkspaceAgentPortShareProtocolHTTPS,
			})
			require.NoError(t, err)

			publicAppClient := appDetails.AppClient(t)
			publicAppClient.SetSessionToken("")

			resp, err := requestWithRetries(ctx, t, publicAppClient, http.MethodGet, appDetails.SubdomainAppURL(appDetails.Apps.PortHTTPS).String(), nil)
			require.NoError(t, err)
			defer resp.Body.Close()
			require.Equal(t, http.StatusOK, resp.StatusCode)
			assertWorkspaceLastUsedAtUpdated(t, appDetails)
		})
	})

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

		setup := func(t *testing.T, allowPathAppSharing, allowSiteOwnerAccess bool) (appDetails *Details, workspace codersdk.Workspace, agnt codersdk.WorkspaceAgent, user codersdk.User, ownerClient *codersdk.Client, client *codersdk.Client, clientInOtherOrg *codersdk.Client, clientWithNoAuth *codersdk.Client) {
			//nolint:gosec
			const password = "SomeSecurePassword!"

			appDetails = setupProxyTest(t, &DeploymentOptions{
				DangerousAllowPathAppSharing:         allowPathAppSharing,
				DangerousAllowPathAppSiteOwnerAccess: allowSiteOwnerAccess,
				// we make the workspace below
				noWorkspace: true,
			})

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			t.Cleanup(cancel)

			// Create a template-admin user in the same org. We don't use an owner
			// since they have access to everything.
			ownerClient = appDetails.SDKClient
			user, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
				Email:          "user@coder.com",
				Username:       "user",
				Password:       password,
				OrganizationID: appDetails.FirstUser.OrganizationID,
			})
			require.NoError(t, err)

			_, err = ownerClient.UpdateUserRoles(ctx, user.ID.String(), codersdk.UpdateRoles{
				Roles: []string{"template-admin", "member"},
			})
			require.NoError(t, err)

			client = codersdk.New(ownerClient.URL)
			loginRes, err := client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
				Email:    user.Email,
				Password: password,
			})
			require.NoError(t, err)
			client.SetSessionToken(loginRes.SessionToken)
			client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			}
			forceURLTransport(t, client)

			// Create workspace.
			port := appServer(t, nil, false)
			workspace, _ = createWorkspaceWithApps(t, client, user.OrganizationIDs[0], user, port, false)

			// Verify that the apps have the correct sharing levels set.
			workspaceBuild, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
			require.NoError(t, err)
			require.NotEmpty(t, workspaceBuild.Resources, "workspace build has no resources")
			require.NotEmpty(t, workspaceBuild.Resources[0].Agents, "workspace build has no agents")
			agnt = workspaceBuild.Resources[0].Agents[0]
			found := map[string]codersdk.WorkspaceAppSharingLevel{}
			expected := map[string]codersdk.WorkspaceAppSharingLevel{
				proxyTestAppNameFake:          codersdk.WorkspaceAppSharingLevelOwner,
				proxyTestAppNameOwner:         codersdk.WorkspaceAppSharingLevelOwner,
				proxyTestAppNameAuthenticated: codersdk.WorkspaceAppSharingLevelAuthenticated,
				proxyTestAppNamePublic:        codersdk.WorkspaceAppSharingLevelPublic,
			}
			for _, app := range agnt.Apps {
				found[app.DisplayName] = app.SharingLevel
			}
			require.Equal(t, expected, found, "apps have incorrect sharing levels")

			// Create a user in a different org.
			otherOrg, err := ownerClient.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
				Name: "a-different-org",
			})
			require.NoError(t, err)
			userInOtherOrg, err := ownerClient.CreateUser(ctx, codersdk.CreateUserRequest{
				Email:          "no-template-access@coder.com",
				Username:       "no-template-access",
				Password:       password,
				OrganizationID: otherOrg.ID,
			})
			require.NoError(t, err)

			clientInOtherOrg = codersdk.New(client.URL)
			loginRes, err = clientInOtherOrg.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
				Email:    userInOtherOrg.Email,
				Password: password,
			})
			require.NoError(t, err)
			clientInOtherOrg.SetSessionToken(loginRes.SessionToken)
			clientInOtherOrg.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			}
			forceURLTransport(t, clientInOtherOrg)

			// Create an unauthenticated codersdk client.
			clientWithNoAuth = codersdk.New(client.URL)
			clientWithNoAuth.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			}
			forceURLTransport(t, clientWithNoAuth)

			return appDetails, workspace, agnt, user, ownerClient, client, clientInOtherOrg, clientWithNoAuth
		}

		verifyAccess := func(t *testing.T, appDetails *Details, isPathApp bool, username, workspaceName, agentName, appName string, client *codersdk.Client, shouldHaveAccess, shouldRedirectToLogin bool) {
			t.Helper()

			ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
			defer cancel()

			// If the client has a session token, we also want to check that a
			// scoped key works.
			sessionTokens := []string{client.SessionToken()}
			if client.SessionToken() != "" {
				token, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
					Scope: codersdk.APIKeyScopeApplicationConnect,
				})
				require.NoError(t, err)

				sessionTokens = append(sessionTokens, token.Key)
			}

			for i, sessionToken := range sessionTokens {
				msg := fmt.Sprintf("client %d", i)

				app := App{
					Username:      username,
					WorkspaceName: workspaceName,
					AgentName:     agentName,
					AppSlugOrPort: appName,
					Query:         proxyTestAppQuery,
				}
				u := appDetails.SubdomainAppURL(app)
				if isPathApp {
					u = appDetails.PathAppURL(app)
				}

				client := appDetails.AppClient(t)
				client.SetSessionToken(sessionToken)
				res, err := requestWithRetries(ctx, t, client, http.MethodGet, u.String(), nil)
				require.NoError(t, err, msg)

				dump, err := httputil.DumpResponse(res, true)
				_ = res.Body.Close()
				require.NoError(t, err, msg)
				t.Log(u)
				t.Logf("response dump: %s", dump)

				if !shouldHaveAccess {
					if shouldRedirectToLogin {
						assert.Equal(t, http.StatusSeeOther, res.StatusCode, "should not have access, expected See Other redirect. "+msg)
						location, err := res.Location()
						require.NoError(t, err, msg)

						expectedPath := "/login"
						if !isPathApp || !appHostIsPrimary {
							expectedPath = "/api/v2/applications/auth-redirect"
						}
						assert.Equal(t, expectedPath, location.Path, "should not have access, expected redirect to applicable login endpoint. "+msg)
					} else {
						// If the user doesn't have access we return 404 to avoid
						// leaking information about the existence of the app.
						assert.Equal(t, http.StatusNotFound, res.StatusCode, "should not have access, expected not found. "+msg)
					}
				}

				if shouldHaveAccess {
					assert.Equal(t, http.StatusOK, res.StatusCode, "should have access, expected ok. "+msg)
					assert.Contains(t, string(dump), "hello world", "should have access, expected hello world. "+msg)
				}
			}
		}

		testLevels := func(t *testing.T, isPathApp, pathAppSharingEnabled, siteOwnerPathAppAccessEnabled bool) {
			appDetails, workspace, agnt, user, ownerClient, client, clientInOtherOrg, clientWithNoAuth := setup(t, pathAppSharingEnabled, siteOwnerPathAppAccessEnabled)

			allowedUnlessSharingDisabled := !isPathApp || pathAppSharingEnabled
			siteOwnerCanAccess := !isPathApp || siteOwnerPathAppAccessEnabled
			siteOwnerCanAccessShared := siteOwnerCanAccess || pathAppSharingEnabled

			deploymentConfig, err := ownerClient.DeploymentConfig(context.Background())
			require.NoError(t, err)

			assert.Equal(t, pathAppSharingEnabled, deploymentConfig.Values.Dangerous.AllowPathAppSharing.Value())
			assert.Equal(t, siteOwnerPathAppAccessEnabled, deploymentConfig.Values.Dangerous.AllowPathAppSiteOwnerAccess.Value())

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

				// Site owner should be able to access all workspaces if
				// enabled.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, ownerClient, siteOwnerCanAccess, false)

				// Owner should be able to access their own workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, client, true, false)

				// Authenticated users should not have access to a workspace that
				// they do not own.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, clientInOtherOrg, false, false)

				// Unauthenticated user should not have any access.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameOwner, clientWithNoAuth, false, true)
			})

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

				// Site owner should be able to access all workspaces if
				// enabled.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, ownerClient, siteOwnerCanAccessShared, false)

				// Owner should be able to access their own workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, client, true, false)

				// Authenticated users should be able to access the workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientInOtherOrg, allowedUnlessSharingDisabled, false)

				// Unauthenticated user should not have any access.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNameAuthenticated, clientWithNoAuth, false, true)
			})

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

				// Site owner should be able to access all workspaces if
				// enabled.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, ownerClient, siteOwnerCanAccessShared, false)

				// Owner should be able to access their own workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, client, true, false)

				// Authenticated users should be able to access the workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, clientInOtherOrg, allowedUnlessSharingDisabled, false)

				// Unauthenticated user should be able to access the workspace.
				verifyAccess(t, appDetails, isPathApp, user.Username, workspace.Name, agnt.Name, proxyTestAppNamePublic, clientWithNoAuth, allowedUnlessSharingDisabled, !allowedUnlessSharingDisabled)
			})
		}

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

			t.Run("Default", func(t *testing.T) {
				t.Parallel()
				testLevels(t, true, false, false)
			})

			t.Run("AppSharingEnabled", func(t *testing.T) {
				t.Parallel()
				testLevels(t, true, true, false)
			})

			t.Run("SiteOwnerAccessEnabled", func(t *testing.T) {
				t.Parallel()
				testLevels(t, true, false, true)
			})

			t.Run("BothEnabled", func(t *testing.T) {
				t.Parallel()
				testLevels(t, true, false, true)
			})
		})

		t.Run("Subdomain", func(t *testing.T) {
			t.Parallel()
			testLevels(t, false, false, false)
		})
	})

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

		// Start a TCP server that manually parses the request. Golang's HTTP
		// server canonicalizes all HTTP request headers it receives, so we
		// can't use it to test that we forward non-canonical headers.
		// #nosec
		ln, err := net.Listen("tcp", ":0")
		require.NoError(t, err)
		go func() {
			for {
				c, err := ln.Accept()
				if xerrors.Is(err, net.ErrClosed) {
					return
				}
				require.NoError(t, err)

				go func() {
					s := bufio.NewScanner(c)

					// Read request line.
					assert.True(t, s.Scan())
					reqLine := s.Text()
					assert.True(t, strings.HasPrefix(reqLine, fmt.Sprintf("GET /?%s HTTP/1.1", proxyTestAppQuery)))

					// Read headers and discard them. We collect the
					// Sec-WebSocket-Key header (with a capital S) to respond
					// with.
					secWebSocketKey := "(none found)"
					for s.Scan() {
						if s.Text() == "" {
							break
						}

						line := strings.TrimSpace(s.Text())
						if strings.HasPrefix(line, "Sec-WebSocket-Key: ") {
							secWebSocketKey = strings.TrimPrefix(line, "Sec-WebSocket-Key: ")
						}
					}

					// Write response containing text/plain with the
					// Sec-WebSocket-Key header.
					res := fmt.Sprintf("HTTP/1.1 204 No Content\r\nSec-WebSocket-Key: %s\r\nConnection: close\r\n\r\n", secWebSocketKey)
					_, err = c.Write([]byte(res))
					assert.NoError(t, err)
					err = c.Close()
					assert.NoError(t, err)
				}()
			}
		}()
		t.Cleanup(func() {
			_ = ln.Close()
		})
		tcpAddr, ok := ln.Addr().(*net.TCPAddr)
		require.True(t, ok)

		appDetails := setupProxyTest(t, &DeploymentOptions{
			port: uint16(tcpAddr.Port),
		})

		cases := []struct {
			name string
			u    *url.URL
		}{
			{
				name: "ProxyPath",
				u:    appDetails.PathAppURL(appDetails.Apps.Owner),
			},
			{
				name: "ProxySubdomain",
				u:    appDetails.SubdomainAppURL(appDetails.Apps.Owner),
			},
		}

		for _, c := range cases {
			c := c

			t.Run(c.name, func(t *testing.T) {
				t.Parallel()

				ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
				defer cancel()

				req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.u.String(), nil)
				require.NoError(t, err)

				// Use a non-canonical header name. The S in Sec-WebSocket-Key should be
				// capitalized according to the websocket spec, but Golang will
				// lowercase it to match the HTTP/1 spec.
				//
				// Setting the header on the map directly will force the header to not
				// be canonicalized on the client, but it will be canonicalized on the
				// server.
				secWebSocketKey := "test-dean-was-here"
				req.Header["Sec-WebSocket-Key"] = []string{secWebSocketKey}
				req.Header.Set(codersdk.SessionTokenHeader, appDetails.SDKClient.SessionToken())

				resp, err := doWithRetries(t, appDetails.AppClient(t), req)
				require.NoError(t, err)
				defer resp.Body.Close()

				// The response should be a 204 No Content with the Sec-WebSocket-Key
				// header set to the value we sent.
				res, err := httputil.DumpResponse(resp, true)
				require.NoError(t, err)
				t.Log(string(res))
				require.Equal(t, http.StatusNoContent, resp.StatusCode)
				require.Equal(t, secWebSocketKey, resp.Header.Get("Sec-WebSocket-Key"))
			})
		}
	})

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

		appDetails := setupProxyTest(t, &DeploymentOptions{
			headers: http.Header{
				"X-Foobar":                         []string{"baz"},
				"Access-Control-Allow-Origin":      []string{"http://localhost"},
				"access-control-allow-origin":      []string{"http://localhost"},
				"Access-Control-Allow-Credentials": []string{"true"},
				"Access-Control-Allow-Methods":     []string{"PUT"},
				"Access-Control-Allow-Headers":     []string{"X-Foobar"},
				"Vary": []string{
					"Origin",
					"origin",
					"Access-Control-Request-Headers",
					"access-Control-request-Headers",
					"Access-Control-Request-Methods",
					"ACCESS-CONTROL-REQUEST-METHODS",
					"X-Foobar",
				},
			},
		})

		appURL := appDetails.SubdomainAppURL(appDetails.Apps.Owner)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, appURL.String(), nil)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Origin"))
		require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Credentials"))
		require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Methods"))
		require.Equal(t, []string(nil), resp.Header.Values("Access-Control-Allow-Headers"))
		// Somehow there are two "Origin"s in Vary even though there should only be
		// one (from the CORS middleware), even if you remove the headers being sent
		// above.  When I do nothing else but change the expected value below to
		// have two "Origin"s suddenly Vary only has one.  It is somehow always the
		// opposite of whatever I put for the expected.  So, reluctantly, remove
		// duplicate "Origin" values.
		var deduped []string
		var addedOrigin bool
		for _, value := range resp.Header.Values("Vary") {
			if value != "Origin" || !addedOrigin {
				if value == "Origin" {
					addedOrigin = true
				}
				deduped = append(deduped, value)
			}
		}
		require.Equal(t, []string{"Origin", "X-Foobar"}, deduped)
		require.Equal(t, []string{"baz"}, resp.Header.Values("X-Foobar"))
	})

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

		reporter := &fakeStatsReporter{}
		appDetails := setupProxyTest(t, &DeploymentOptions{
			StatsCollectorOptions: workspaceapps.StatsCollectorOptions{
				Reporter:       reporter,
				ReportInterval: time.Hour,
				RollupWindow:   time.Minute,
			},
		})

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		u := appDetails.PathAppURL(appDetails.Apps.Owner)
		resp, err := requestWithRetries(ctx, t, appDetails.AppClient(t), http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		defer resp.Body.Close()
		_, err = io.Copy(io.Discard, resp.Body)
		require.NoError(t, err)
		require.Equal(t, http.StatusOK, resp.StatusCode)

		var stats []workspaceapps.StatsReport
		require.Eventually(t, func() bool {
			// Keep flushing until we get a non-empty stats report.
			appDetails.FlushStats()
			stats = reporter.stats()
			return len(stats) > 0
		}, testutil.WaitLong, testutil.IntervalFast, "stats not reported")

		assert.Equal(t, workspaceapps.AccessMethodPath, stats[0].AccessMethod)
		assert.Equal(t, "test-app-owner", stats[0].SlugOrPort)
		assert.Equal(t, 1, stats[0].Requests)
	})

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

		appDetails := setupProxyTest(t, nil)

		ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
		defer cancel()

		_ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)

		u := appDetails.PathAppURL(appDetails.Apps.Owner)
		resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil)
		require.NoError(t, err)
		_ = resp.Body.Close()
		require.Equal(t, http.StatusBadRequest, resp.StatusCode)
		require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
	})
}

type fakeStatsReporter struct {
	mu sync.Mutex
	s  []workspaceapps.StatsReport
}

func (r *fakeStatsReporter) stats() []workspaceapps.StatsReport {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.s
}

func (r *fakeStatsReporter) Report(_ context.Context, stats []workspaceapps.StatsReport) error {
	r.mu.Lock()
	r.s = append(r.s, stats...)
	r.mu.Unlock()
	return nil
}

func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Client, agentID uuid.UUID, signedToken string) {
	opts := workspacesdk.WorkspaceAgentReconnectingPTYOpts{
		AgentID:   agentID,
		Reconnect: uuid.New(),
		Width:     80,
		Height:    80,
		// --norc disables executing .bashrc, which is often used to customize the bash prompt
		Command:     "bash --norc",
		SignedToken: signedToken,
	}
	matchPrompt := func(line string) bool {
		return strings.Contains(line, "$ ") || strings.Contains(line, "# ")
	}
	matchEchoCommand := func(line string) bool {
		return strings.Contains(line, "echo test")
	}
	matchEchoOutput := func(line string) bool {
		return strings.Contains(line, "test") && !strings.Contains(line, "echo")
	}
	matchExitCommand := func(line string) bool {
		return strings.Contains(line, "exit")
	}
	matchExitOutput := func(line string) bool {
		return strings.Contains(line, "exit") || strings.Contains(line, "logout")
	}

	conn, err := workspacesdk.New(client).AgentReconnectingPTY(ctx, opts)
	require.NoError(t, err)
	defer conn.Close()

	tr := testutil.NewTerminalReader(t, conn)
	// Wait for the prompt before writing commands.  If the command arrives before the prompt is written, screen
	// will sometimes put the command output on the same line as the command and the test will flake
	require.NoError(t, tr.ReadUntil(ctx, matchPrompt), "find prompt")

	data, err := json.Marshal(workspacesdk.ReconnectingPTYRequest{
		Data: "echo test\r",
	})
	require.NoError(t, err)
	_, err = conn.Write(data)
	require.NoError(t, err)

	require.NoError(t, tr.ReadUntil(ctx, matchEchoCommand), "find echo command")
	require.NoError(t, tr.ReadUntil(ctx, matchEchoOutput), "find echo output")

	// Exit should cause the connection to close.
	data, err = json.Marshal(workspacesdk.ReconnectingPTYRequest{
		Data: "exit\r",
	})
	require.NoError(t, err)
	_, err = conn.Write(data)
	require.NoError(t, err)

	// Once for the input and again for the output.
	require.NoError(t, tr.ReadUntil(ctx, matchExitCommand), "find exit command")
	require.NoError(t, tr.ReadUntil(ctx, matchExitOutput), "find exit output")

	// Ensure the connection closes.
	require.ErrorIs(t, tr.ReadUntil(ctx, nil), io.EOF)
}

// Accessing an app should update the workspace's LastUsedAt.
// NOTE: Despite our efforts with the flush channel, this is inherently racy when used with
// parallel tests on the same workspace/app.
func assertWorkspaceLastUsedAtUpdated(t testing.TB, details *Details) {
	t.Helper()

	require.NotNil(t, details.Workspace, "can't assert LastUsedAt on a nil workspace!")
	before, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID)
	require.NoError(t, err)
	require.Eventually(t, func() bool {
		// We may need to flush multiple times, since the stats from the app we are testing might be
		// collected asynchronously from when we see the connection close, and thus, could race
		// against being flushed.
		details.FlushStats()
		after, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID)
		return assert.NoError(t, err) && after.LastUsedAt.After(before.LastUsedAt)
	}, testutil.WaitShort, testutil.IntervalMedium)
}

// Except when it sometimes shouldn't (e.g. no access)
// NOTE: Despite our efforts with the flush channel, this is inherently racy when used with
// parallel tests on the same workspace/app.
func assertWorkspaceLastUsedAtNotUpdated(t testing.TB, details *Details) {
	t.Helper()

	require.NotNil(t, details.Workspace, "can't assert LastUsedAt on a nil workspace!")
	before, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID)
	require.NoError(t, err)
	details.FlushStats()
	after, err := details.SDKClient.Workspace(context.Background(), details.Workspace.ID)
	require.NoError(t, err)
	require.Equal(t, before.LastUsedAt, after.LastUsedAt, "workspace LastUsedAt updated when it should not have been")
}
