// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package providercache

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"github.com/apparentlymart/go-versions/versions"
	"github.com/apparentlymart/go-versions/versions/constraints"
	"github.com/davecgh/go-spew/spew"
	"github.com/google/go-cmp/cmp"
	svchost "github.com/hashicorp/terraform-svchost"
	"github.com/hashicorp/terraform-svchost/disco"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders"
)

func TestEnsureProviderVersions(t *testing.T) {
	// This is a sort of hybrid between table-driven and imperative-style
	// testing, because the overall sequence of steps is the same for all
	// of the test cases but the setup and verification have enough different
	// permutations that it ends up being more concise to express them as
	// normal code.
	type Test struct {
		Source     getproviders.Source
		Prepare    func(*testing.T, *Installer, *Dir)
		LockFile   string
		Reqs       getproviders.Requirements
		Mode       InstallMode
		Check      func(*testing.T, *Dir, *depsfile.Locks)
		WantErr    string
		WantEvents func(*Installer, *Dir) map[addrs.Provider][]*testInstallerEventLogItem
	}

	// noProvider is just the zero value of addrs.Provider, which we're
	// using in this test as the key for installer events that are not
	// specific to a particular provider.
	var noProvider addrs.Provider
	beepProvider := addrs.MustParseProviderSourceString("example.com/foo/beep")
	beepProviderDir := getproviders.PackageLocalDir("testdata/beep-provider")
	fakePlatform := getproviders.Platform{OS: "bleep", Arch: "bloop"}
	wrongPlatform := getproviders.Platform{OS: "wrong", Arch: "wrong"}
	beepProviderHash := getproviders.HashScheme1.New("2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=")
	terraformProvider := addrs.MustParseProviderSourceString("terraform.io/builtin/terraform")

	tests := map[string]Test{
		"no dependencies": {
			Mode: InstallNewProvidersOnly,
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
					t.Errorf("unexpected cache directory entries\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 0 {
					t.Errorf("unexpected provider lock entries\n%s", spew.Sdump(allLocked))
				}
			},
			WantEvents: func(*Installer, *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args:  map[addrs.Provider]getproviders.VersionConstraints(nil),
						},
					},
				}
			},
		},
		"successful initial install of one provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"2.1.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								nil,
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"successful initial install of one provider through a cold global cache": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				inst.SetGlobalCacheDir(globalCacheDir)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"2.1.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								nil,
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"successful initial install of one provider through a warm global cache but without a lock file entry": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				_, err := globalCacheDir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("failed to populate global cache: %s", err)
				}
				inst.SetGlobalCacheDir(globalCacheDir)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						// Existing cache entry is ineligible for linking because
						// we have no lock file checksums to compare it to.
						// Instead, we install from upstream and lock with
						// whatever checksums we learn in that process.
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{
								"2.1.0",
								beepProviderDir,
							},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								nil,
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"successful initial install of one provider through a warm global cache and correct locked checksum": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				# The existing cache entry is valid only if it matches a
				# checksum already recorded in the lock file.
				provider "example.com/foo/beep" {
					version     = "2.1.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				_, err := globalCacheDir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("failed to populate global cache: %s", err)
				}
				inst.SetGlobalCacheDir(globalCacheDir)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "LinkFromCacheBegin",
							Provider: beepProvider,
							Args: struct {
								Version   string
								CacheRoot string
							}{
								"2.1.0",
								inst.globalCacheDir.BasePath(),
							},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
							},
						},
						{
							Event:    "LinkFromCacheSuccess",
							Provider: beepProvider,
							Args: struct {
								Version  string
								LocalDir string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
							},
						},
					},
				}
			},
		},
		"successful initial install of one provider through a warm global cache with an incompatible checksum": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				# This is approximating the awkward situation where the lock
				# file was populated by someone who installed from a location
				# other than the origin registry annd so the set of checksums
				# is incomplete. In this case we can't prove that our cache
				# entry is valid and so we silently ignore the cache entry
				# and try to install from upstream anyway, in the hope that
				# this will give us an opportunity to access the origin
				# registry and get a checksum that works for the current
				# platform.
				provider "example.com/foo/beep" {
					version     = "2.1.0"
					constraints = ">= 1.0.0"
					hashes = [
						# NOTE: This is the correct checksum for the
						# beepProviderDir package, but we're going to
						# intentionally install from a different directory
						# below so that the entry in the cache will not
						# match this checksum.
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				// This is another "beep provider" package directory that
				// has a different checksum than the one in beepProviderDir.
				// We're mimicking the situation where the lock file was
				// originally built from beepProviderDir but the local system
				// is running on a different platform and so its existing
				// cache entry doesn't match the checksum.
				beepProviderOtherPlatformDir := getproviders.PackageLocalDir("testdata/beep-provider-other-platform")

				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				_, err := globalCacheDir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderOtherPlatformDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("failed to populate global cache: %s", err)
				}
				inst.SetGlobalCacheDir(globalCacheDir)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{
								"2.1.0",
								beepProviderDir,
							},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"successful initial install of one provider through a warm global cache without a lock file entry but allowing the cache to break the lock file": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				# (intentionally empty)
			`,
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				_, err := globalCacheDir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("failed to populate global cache: %s", err)
				}
				inst.SetGlobalCacheDir(globalCacheDir)
				inst.SetGlobalCacheDirMayBreakDependencyLockFile(true)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{beepProviderHash},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "LinkFromCacheBegin",
							Provider: beepProvider,
							Args: struct {
								Version   string
								CacheRoot string
							}{
								"2.1.0",
								inst.globalCacheDir.BasePath(),
							},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								nil,
							},
						},
						{
							Event:    "LinkFromCacheSuccess",
							Provider: beepProvider,
							Args: struct {
								Version  string
								LocalDir string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"),
							},
						},
					},
				}
			},
		},
		"failing install of one provider through a warm global cache with an incorrect locked checksum while allowing the cache to break the lock file": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				# The existing cache entry is valid only if it matches a
				# checksum already recorded in the lock file, but this
				# test is overriding that rule using a special setting.
				provider "example.com/foo/beep" {
					version     = "2.1.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:wrong-not-matchy",
					]
				}
			`,
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				globalCacheDirPath := tmpDir(t)
				globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform)
				_, err := globalCacheDir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("failed to populate global cache: %s", err)
				}
				inst.SetGlobalCacheDir(globalCacheDir)
				inst.SetGlobalCacheDirMayBreakDependencyLockFile(true)
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
					t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					// The lock file entry hasn't changed because the cache
					// entry didn't match the existing lock file entry.
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 1.0.0"),
					[]getproviders.Hash{"h1:wrong-not-matchy"},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				// The provider wasn't installed into the local cache directory
				// because that would make the local cache mismatch the
				// lock file.
				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := (*CachedProvider)(nil)
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantErr: `doesn't match any of the checksums`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "LinkFromCacheBegin",
							Provider: beepProvider,
							Args: struct {
								Version   string
								CacheRoot string
							}{
								"2.1.0",
								inst.globalCacheDir.BasePath(),
							},
						},
						{
							Event:    "LinkFromCacheFailure",
							Provider: beepProvider,
							Args: struct {
								Version string
								Error   string
							}{
								"2.1.0",
								fmt.Sprintf(
									"the provider cache at %s has a copy of example.com/foo/beep 2.1.0 that doesn't match any of the checksums recorded in the dependency lock file",
									dir.BasePath(),
								),
							},
						},
					},
				}
			},
		},
		"successful reinstall of one previously-locked provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "2.0.0"
					constraints = ">= 2.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.0.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.0.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.0.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.0.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"2.0.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.0.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.0.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"skipped install of one previously-locked and installed provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "2.0.0"
					constraints = ">= 2.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				_, err := dir.InstallPackage(
					context.Background(),
					getproviders.PackageMeta{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					nil,
				)
				if err != nil {
					t.Fatalf("installation to the test dir failed: %s", err)
				}
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.0.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.0.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.0.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.0.0",
						},
						{
							Event:    "ProviderAlreadyInstalled",
							Provider: beepProvider,
							Args:     versions.Version{Major: 2, Minor: 0, Patch: 0},
						},
					},
				}
			},
		},
		"successful upgrade of one previously-locked provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.1.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "2.0.0"
					constraints = ">= 2.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Mode: InstallUpgrades,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("2.1.0"),
					getproviders.MustParseVersionConstraints(">= 2.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("2.1.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "2.1.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"2.1.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"2.1.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								nil,
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"2.1.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"successful install of a built-in provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{},
				nil,
			),
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				inst.SetBuiltInProviderTypes([]string{"terraform"})
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				terraformProvider: nil,
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				// Built-in providers are neither included in the cache
				// directory nor mentioned in the lock file, because they
				// are compiled directly into the Terraform executable.
				if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
					t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 0 {
					t.Errorf("wrong number of provider lock entries; want none\n%s", spew.Sdump(allLocked))
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								terraformProvider: constraints.IntersectionSpec(nil),
							},
						},
					},
					terraformProvider: {
						{
							Event:    "BuiltInProviderAvailable",
							Provider: terraformProvider,
						},
					},
				}
			},
		},
		"remove no-longer-needed provider from lock file": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "1.0.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
				provider "example.com/foo/obsolete" {
					version     = "2.0.0"
					constraints = ">= 2.0.0"
					hashes = [
						"no:irrelevant",
					]
				}
			`,
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("1.0.0"),
					getproviders.MustParseVersionConstraints(">= 1.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("1.0.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					// Note: intentionally no entries for example.com/foo/obsolete
					// here, because it's no longer needed and therefore not
					// installed.
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 1.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"1.0.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"1.0.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"1.0.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
		"failed install of a non-existing built-in provider": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{},
				nil,
			),
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				// NOTE: We're intentionally not calling
				// inst.SetBuiltInProviderTypes to make the "terraform"
				// built-in provider available here, so requests for it
				// should fail.
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				terraformProvider: nil,
			},
			WantErr: `some providers could not be installed:
- terraform.io/builtin/terraform: this Terraform release has no built-in provider named "terraform"`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								terraformProvider: constraints.IntersectionSpec(nil),
							},
						},
					},
					terraformProvider: {
						{
							Event:    "BuiltInProviderFailure",
							Provider: terraformProvider,
							Args:     `this Terraform release has no built-in provider named "terraform"`,
						},
					},
				}
			},
		},
		"failed install when a built-in provider has a version constraint": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{},
				nil,
			),
			Prepare: func(t *testing.T, inst *Installer, dir *Dir) {
				inst.SetBuiltInProviderTypes([]string{"terraform"})
			},
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				terraformProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			WantErr: `some providers could not be installed:
- terraform.io/builtin/terraform: built-in providers do not support explicit version constraints`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								terraformProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
					},
					terraformProvider: {
						{
							Event:    "BuiltInProviderFailure",
							Provider: terraformProvider,
							Args:     `built-in providers do not support explicit version constraints`,
						},
					},
				}
			},
		},
		"locked version is excluded by new version constraint": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "1.0.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
					t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("1.0.0"),
					getproviders.MustParseVersionConstraints(">= 1.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}
			},
			WantErr: `some providers could not be installed:
- example.com/foo/beep: locked provider example.com/foo/beep 1.0.0 does not match configured version constraint >= 2.0.0; must use terraform init -upgrade to allow selection of new versions`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", true},
						},
						{
							Event:    "QueryPackagesFailure",
							Provider: beepProvider,
							Args:     `locked provider example.com/foo/beep 1.0.0 does not match configured version constraint >= 2.0.0; must use terraform init -upgrade to allow selection of new versions`,
						},
					},
				}
			},
		},
		"locked version is no longer available": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("2.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "1.2.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=",
					]
				}
			`,
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 0 {
					t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("1.2.0"),
					getproviders.MustParseVersionConstraints(">= 1.0.0"),
					[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}
			},
			WantErr: `some providers could not be installed:
- example.com/foo/beep: the previously-selected version 1.2.0 is no longer available`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 1.0.0", true},
						},
						{
							Event:    "QueryPackagesFailure",
							Provider: beepProvider,
							Args:     `the previously-selected version 1.2.0 is no longer available`,
						},
					},
				}
			},
		},
		"no versions match the version constraint": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
			},
			WantErr: `some providers could not be installed:
- example.com/foo/beep: no available releases match the given constraints >= 2.0.0`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 2.0.0", false},
						},
						{
							Event:    "QueryPackagesFailure",
							Provider: beepProvider,
							Args:     `no available releases match the given constraints >= 2.0.0`,
						},
					},
				}
			},
		},
		"version exists but doesn't support the current platform": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: wrongPlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			WantErr: `some providers could not be installed:
- example.com/foo/beep: provider example.com/foo/beep 1.0.0 is not available for bleep_bloop`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 1.0.0", false},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageFailure",
							Provider: beepProvider,
							Args: struct {
								Version string
								Error   string
							}{
								"1.0.0",
								"provider example.com/foo/beep 1.0.0 is not available for bleep_bloop",
							},
						},
					},
				}
			},
		},
		"available package doesn't match locked hash": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "1.0.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:does-not-match",
					]
				}
			`,
			Mode: InstallNewProvidersOnly,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			WantErr: `some providers could not be installed:
- example.com/foo/beep: the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://www.terraform.io/language/provider-checksum-verification`,
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 1.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"1.0.0", beepProviderDir},
						},
						{
							Event:    "FetchPackageFailure",
							Provider: beepProvider,
							Args: struct {
								Version string
								Error   string
							}{
								"1.0.0",
								`the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://www.terraform.io/language/provider-checksum-verification`,
							},
						},
					},
				}
			},
		},
		"force mode ignores hashes": {
			Source: getproviders.NewMockSource(
				[]getproviders.PackageMeta{
					{
						Provider:       beepProvider,
						Version:        getproviders.MustParseVersion("1.0.0"),
						TargetPlatform: fakePlatform,
						Location:       beepProviderDir,
					},
				},
				nil,
			),
			LockFile: `
				provider "example.com/foo/beep" {
					version     = "1.0.0"
					constraints = ">= 1.0.0"
					hashes = [
						"h1:does-not-match",
					]
				}
			`,
			Mode: InstallNewProvidersForce,
			Reqs: getproviders.Requirements{
				beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
			},
			Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) {
				if allCached := dir.AllAvailablePackages(); len(allCached) != 1 {
					t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached))
				}
				if allLocked := locks.AllProviders(); len(allLocked) != 1 {
					t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked))
				}

				gotLock := locks.Provider(beepProvider)
				wantLock := depsfile.NewProviderLock(
					beepProvider,
					getproviders.MustParseVersion("1.0.0"),
					getproviders.MustParseVersionConstraints(">= 1.0.0"),
					[]getproviders.Hash{beepProviderHash, "h1:does-not-match"},
				)
				if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" {
					t.Errorf("wrong lock entry\n%s", diff)
				}

				gotEntry := dir.ProviderLatestVersion(beepProvider)
				wantEntry := &CachedProvider{
					Provider:   beepProvider,
					Version:    getproviders.MustParseVersion("1.0.0"),
					PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
				}
				if diff := cmp.Diff(wantEntry, gotEntry); diff != "" {
					t.Errorf("wrong cache entry\n%s", diff)
				}
			},
			WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem {
				return map[addrs.Provider][]*testInstallerEventLogItem{
					noProvider: {
						{
							Event: "PendingProviders",
							Args: map[addrs.Provider]getproviders.VersionConstraints{
								beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"),
							},
						},
						{
							Event: "ProvidersFetched",
							Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{
								beepProvider: nil,
							},
						},
					},
					beepProvider: {
						{
							Event:    "QueryPackagesBegin",
							Provider: beepProvider,
							Args: struct {
								Constraints string
								Locked      bool
							}{">= 1.0.0", true},
						},
						{
							Event:    "QueryPackagesSuccess",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageMeta",
							Provider: beepProvider,
							Args:     "1.0.0",
						},
						{
							Event:    "FetchPackageBegin",
							Provider: beepProvider,
							Args: struct {
								Version  string
								Location getproviders.PackageLocation
							}{"1.0.0", beepProviderDir},
						},
						{
							Event:    "ProvidersLockUpdated",
							Provider: beepProvider,
							Args: struct {
								Version string
								Local   []getproviders.Hash
								Signed  []getproviders.Hash
								Prior   []getproviders.Hash
							}{
								"1.0.0",
								[]getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="},
								nil,
								[]getproviders.Hash{"h1:does-not-match"},
							},
						},
						{
							Event:    "FetchPackageSuccess",
							Provider: beepProvider,
							Args: struct {
								Version    string
								LocalDir   string
								AuthResult string
							}{
								"1.0.0",
								filepath.Join(dir.BasePath(), "example.com/foo/beep/1.0.0/bleep_bloop"),
								"unauthenticated",
							},
						},
					},
				}
			},
		},
	}

	ctx := context.Background()

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			if test.Check == nil && test.WantEvents == nil && test.WantErr == "" {
				t.Fatalf("invalid test: must set at least one of Check, WantEvents, or WantErr")
			}

			outputDir := NewDirWithPlatform(tmpDir(t), fakePlatform)
			source := test.Source
			if source == nil {
				source = getproviders.NewMockSource(nil, nil)
			}
			inst := NewInstaller(outputDir, source)
			if test.Prepare != nil {
				test.Prepare(t, inst, outputDir)
			} /* boop */

			locks, lockDiags := depsfile.LoadLocksFromBytes([]byte(test.LockFile), "test.lock.hcl")
			if lockDiags.HasErrors() {
				t.Fatalf("invalid lock file: %s", lockDiags.Err().Error())
			}

			providerEvents := make(map[addrs.Provider][]*testInstallerEventLogItem)
			eventsCh := make(chan *testInstallerEventLogItem)
			var newLocks *depsfile.Locks
			var instErr error
			go func(ch chan *testInstallerEventLogItem) {
				events := installerLogEventsForTests(ch)
				ctx := events.OnContext(ctx)
				newLocks, instErr = inst.EnsureProviderVersions(ctx, locks, test.Reqs, test.Mode)
				close(eventsCh) // exits the event loop below
			}(eventsCh)
			for evt := range eventsCh {
				// We do the event collection in the main goroutine, rather than
				// running the installer itself in the main goroutine, so that
				// we can safely t.Log in here without violating the testing.T
				// usage rules.
				if evt.Provider == (addrs.Provider{}) {
					t.Logf("%s(%s)", evt.Event, spew.Sdump(evt.Args))
				} else {
					t.Logf("%s: %s(%s)", evt.Provider, evt.Event, spew.Sdump(evt.Args))
				}
				providerEvents[evt.Provider] = append(providerEvents[evt.Provider], evt)
			}

			if test.WantErr != "" {
				if instErr == nil {
					t.Errorf("succeeded; want error\nwant: %s", test.WantErr)
				} else if got, want := instErr.Error(), test.WantErr; !strings.Contains(got, want) {
					t.Errorf("wrong error\ngot: %s\nwant substring: %s", got, want)
				}
			} else if instErr != nil {
				t.Errorf("unexpected error\ngot: %s", instErr.Error())
			}

			if test.Check != nil {
				test.Check(t, outputDir, newLocks)
			}

			if test.WantEvents != nil {
				wantEvents := test.WantEvents(inst, outputDir)
				if diff := cmp.Diff(wantEvents, providerEvents); diff != "" {
					t.Errorf("wrong installer events\n%s", diff)
				}
			}
		})
	}
}

func TestEnsureProviderVersions_local_source(t *testing.T) {
	// create filesystem source using the test provider cache dir
	source := getproviders.NewFilesystemMirrorSource("testdata/cachedir")

	// create a temporary workdir
	tmpDirPath := t.TempDir()

	// set up the installer using the temporary directory and filesystem source
	platform := getproviders.Platform{OS: "linux", Arch: "amd64"}
	dir := NewDirWithPlatform(tmpDirPath, platform)
	installer := NewInstaller(dir, source)

	tests := map[string]struct {
		provider string
		version  string
		wantHash getproviders.Hash // getproviders.NilHash if not expected to be installed
		err      string
	}{
		"install-unpacked": {
			provider: "null",
			version:  "2.0.0",
			wantHash: getproviders.HashScheme1.New("qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g="),
		},
		"invalid-zip-file": {
			provider: "null",
			version:  "2.1.0",
			wantHash: getproviders.NilHash,
			err:      "zip: not a valid zip file",
		},
		"version-constraint-unmet": {
			provider: "null",
			version:  "2.2.0",
			wantHash: getproviders.NilHash,
			err:      "no available releases match the given constraints 2.2.0",
		},
		"missing-executable": {
			provider: "missing/executable",
			version:  "2.0.0",
			wantHash: getproviders.NilHash, // installation fails for a provider with no executable
			err:      "provider binary not found: could not find executable file starting with terraform-provider-executable",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			ctx := context.TODO()

			provider := addrs.MustParseProviderSourceString(test.provider)
			versionConstraint := getproviders.MustParseVersionConstraints(test.version)
			version := getproviders.MustParseVersion(test.version)
			reqs := getproviders.Requirements{
				provider: versionConstraint,
			}

			newLocks, err := installer.EnsureProviderVersions(ctx, depsfile.NewLocks(), reqs, InstallNewProvidersOnly)
			gotProviderlocks := newLocks.AllProviders()
			wantProviderLocks := map[addrs.Provider]*depsfile.ProviderLock{
				provider: depsfile.NewProviderLock(
					provider,
					version,
					getproviders.MustParseVersionConstraints("= 2.0.0"),
					[]getproviders.Hash{
						test.wantHash,
					},
				),
			}
			if test.wantHash == getproviders.NilHash {
				wantProviderLocks = map[addrs.Provider]*depsfile.ProviderLock{}
			}

			if diff := cmp.Diff(wantProviderLocks, gotProviderlocks, depsfile.ProviderLockComparer); diff != "" {
				t.Errorf("wrong selected\n%s", diff)
			}

			if test.err == "" && err == nil {
				return
			}

			switch err := err.(type) {
			case InstallerError:
				providerError, ok := err.ProviderErrors[provider]
				if !ok {
					t.Fatalf("did not get error for provider %s", provider)
				}

				if got := providerError.Error(); got != test.err {
					t.Fatalf("wrong result\ngot:  %s\nwant: %s\n", got, test.err)
				}
			default:
				t.Fatalf("wrong error type. Expected InstallerError, got %T", err)
			}
		})
	}
}

// This test only verifies protocol errors and does not try for successfull
// installation (at the time of writing, the test files aren't signed so the
// signature verification fails); that's left to the e2e tests.
func TestEnsureProviderVersions_protocol_errors(t *testing.T) {
	source, _, close := testRegistrySource(t)
	defer close()

	// create a temporary workdir
	tmpDirPath := t.TempDir()

	version0 := getproviders.MustParseVersionConstraints("0.1.0") // supports protocol version 1.0
	version1 := getproviders.MustParseVersion("1.2.0")            // this is the expected result in tests with a match
	version2 := getproviders.MustParseVersionConstraints("2.0")   // supports protocol version 99

	// set up the installer using the temporary directory and mock source
	platform := getproviders.Platform{OS: "gameboy", Arch: "lr35902"}
	dir := NewDirWithPlatform(tmpDirPath, platform)
	installer := NewInstaller(dir, source)

	tests := map[string]struct {
		provider     addrs.Provider
		inputVersion getproviders.VersionConstraints
		wantVersion  getproviders.Version
	}{
		"too old": {
			addrs.MustParseProviderSourceString("example.com/awesomesauce/happycloud"),
			version0,
			version1,
		},
		"too new": {
			addrs.MustParseProviderSourceString("example.com/awesomesauce/happycloud"),
			version2,
			version1,
		},
		"unsupported": {
			addrs.MustParseProviderSourceString("example.com/weaksauce/unsupported-protocol"),
			version0,
			getproviders.UnspecifiedVersion,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			reqs := getproviders.Requirements{
				test.provider: test.inputVersion,
			}
			ctx := context.TODO()
			_, err := installer.EnsureProviderVersions(ctx, depsfile.NewLocks(), reqs, InstallNewProvidersOnly)

			switch err := err.(type) {
			case nil:
				t.Fatalf("expected error, got success")
			case InstallerError:
				providerError, ok := err.ProviderErrors[test.provider]
				if !ok {
					t.Fatalf("did not get error for provider %s", test.provider)
				}

				switch providerError := providerError.(type) {
				case getproviders.ErrProtocolNotSupported:
					if !providerError.Suggestion.Same(test.wantVersion) {
						t.Fatalf("wrong result\ngot:  %s\nwant: %s\n", providerError.Suggestion, test.wantVersion)
					}
				default:
					t.Fatalf("wrong error type. Expected ErrProtocolNotSupported, got %T", err)
				}
			default:
				t.Fatalf("wrong error type. Expected InstallerError, got %T", err)
			}
		})
	}
}

// testServices starts up a local HTTP server running a fake provider registry
// service and returns a service discovery object pre-configured to consider
// the host "example.com" to be served by the fake registry service.
//
// The returned discovery object also knows the hostname "not.example.com"
// which does not have a provider registry at all and "too-new.example.com"
// which has a "providers.v99" service that is inoperable but could be useful
// to test the error reporting for detecting an unsupported protocol version.
// It also knows fails.example.com but it refers to an endpoint that doesn't
// correctly speak HTTP, to simulate a protocol error.
//
// The second return value is a function to call at the end of a test function
// to shut down the test server. After you call that function, the discovery
// object becomes useless.
func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) {
	server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))

	services = disco.New()
	services.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
		"providers.v1": server.URL + "/providers/v1/",
	})
	services.ForceHostServices(svchost.Hostname("not.example.com"), map[string]interface{}{})
	services.ForceHostServices(svchost.Hostname("too-new.example.com"), map[string]interface{}{
		// This service doesn't actually work; it's here only to be
		// detected as "too new" by the discovery logic.
		"providers.v99": server.URL + "/providers/v99/",
	})
	services.ForceHostServices(svchost.Hostname("fails.example.com"), map[string]interface{}{
		"providers.v1": server.URL + "/fails-immediately/",
	})

	// We'll also permit registry.terraform.io here just because it's our
	// default and has some unique features that are not allowed on any other
	// hostname. It behaves the same as example.com, which should be preferred
	// if you're not testing something specific to the default registry in order
	// to ensure that most things are hostname-agnostic.
	services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{
		"providers.v1": server.URL + "/providers/v1/",
	})

	return services, server.URL, func() {
		server.Close()
	}
}

// testRegistrySource is a wrapper around testServices that uses the created
// discovery object to produce a Source instance that is ready to use with the
// fake registry services.
//
// As with testServices, the second return value is a function to call at the end
// of your test in order to shut down the test server.
func testRegistrySource(t *testing.T) (source *getproviders.RegistrySource, baseURL string, cleanup func()) {
	services, baseURL, close := testServices(t)
	source = getproviders.NewRegistrySource(services)
	return source, baseURL, close
}

func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
	path := req.URL.EscapedPath()
	if strings.HasPrefix(path, "/fails-immediately/") {
		// Here we take over the socket and just close it immediately, to
		// simulate one possible way a server might not be an HTTP server.
		hijacker, ok := resp.(http.Hijacker)
		if !ok {
			// Not hijackable, so we'll just fail normally.
			// If this happens, tests relying on this will fail.
			resp.WriteHeader(500)
			resp.Write([]byte(`cannot hijack`))
			return
		}
		conn, _, err := hijacker.Hijack()
		if err != nil {
			resp.WriteHeader(500)
			resp.Write([]byte(`hijack failed`))
			return
		}
		conn.Close()
		return
	}

	if strings.HasPrefix(path, "/pkg/") {
		switch path {
		case "/pkg/awesomesauce/happycloud_1.2.0.zip":
			resp.Write([]byte("some zip file"))
		case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS":
			resp.Write([]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"))
		case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS.sig":
			resp.Write([]byte("GPG signature"))
		default:
			resp.WriteHeader(404)
			resp.Write([]byte("unknown package file download"))
		}
		return
	}

	if !strings.HasPrefix(path, "/providers/v1/") {
		resp.WriteHeader(404)
		resp.Write([]byte(`not a provider registry endpoint`))
		return
	}

	pathParts := strings.Split(path, "/")[3:]
	if len(pathParts) < 2 {
		resp.WriteHeader(404)
		resp.Write([]byte(`unexpected number of path parts`))
		return
	}
	log.Printf("[TRACE] fake provider registry request for %#v", pathParts)
	if len(pathParts) == 2 {
		switch pathParts[0] + "/" + pathParts[1] {

		case "-/legacy":
			// NOTE: This legacy lookup endpoint is specific to
			// registry.terraform.io and not expected to work on any other
			// registry host.
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			resp.Write([]byte(`{"namespace":"legacycorp"}`))

		default:
			resp.WriteHeader(404)
			resp.Write([]byte(`unknown namespace or provider type for direct lookup`))
		}
	}

	if len(pathParts) < 3 {
		resp.WriteHeader(404)
		resp.Write([]byte(`unexpected number of path parts`))
		return
	}

	if pathParts[2] == "versions" {
		if len(pathParts) != 3 {
			resp.WriteHeader(404)
			resp.Write([]byte(`extraneous path parts`))
			return
		}

		switch pathParts[0] + "/" + pathParts[1] {
		case "awesomesauce/happycloud":
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			// Note that these version numbers are intentionally misordered
			// so we can test that the client-side code places them in the
			// correct order (lowest precedence first).
			resp.Write([]byte(`{"versions":[{"version":"0.1.0","protocols":["1.0"]},{"version":"2.0.0","protocols":["99.0"]},{"version":"1.2.0","protocols":["5.0"]}, {"version":"1.0.0","protocols":["5.0"]}]}`))
		case "weaksauce/unsupported-protocol":
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			resp.Write([]byte(`{"versions":[{"version":"0.1.0","protocols":["0.1"]}]}`))
		case "weaksauce/no-versions":
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			resp.Write([]byte(`{"versions":[]}`))
		default:
			resp.WriteHeader(404)
			resp.Write([]byte(`unknown namespace or provider type`))
		}
		return
	}

	if len(pathParts) == 6 && pathParts[3] == "download" {
		switch pathParts[0] + "/" + pathParts[1] {
		case "awesomesauce/happycloud":
			if pathParts[4] == "nonexist" {
				resp.WriteHeader(404)
				resp.Write([]byte(`unsupported OS`))
				return
			}
			version := pathParts[2]
			body := map[string]interface{}{
				"protocols":             []string{"99.0"},
				"os":                    pathParts[4],
				"arch":                  pathParts[5],
				"filename":              "happycloud_" + version + ".zip",
				"shasum":                "000000000000000000000000000000000000000000000000000000000000f00d",
				"download_url":          "/pkg/awesomesauce/happycloud_" + version + ".zip",
				"shasums_url":           "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS",
				"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS.sig",
				"signing_keys": map[string]interface{}{
					"gpg_public_keys": []map[string]interface{}{
						{
							"ascii_armor": getproviders.HashicorpPublicKey,
						},
					},
				},
			}
			enc, err := json.Marshal(body)
			if err != nil {
				resp.WriteHeader(500)
				resp.Write([]byte("failed to encode body"))
			}
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			resp.Write(enc)
		case "weaksauce/unsupported-protocol":
			var protocols []string
			version := pathParts[2]
			switch version {
			case "0.1.0":
				protocols = []string{"1.0"}
			case "2.0.0":
				protocols = []string{"99.0"}
			default:
				protocols = []string{"5.0"}
			}

			body := map[string]interface{}{
				"protocols":             protocols,
				"os":                    pathParts[4],
				"arch":                  pathParts[5],
				"filename":              "happycloud_" + version + ".zip",
				"shasum":                "000000000000000000000000000000000000000000000000000000000000f00d",
				"download_url":          "/pkg/awesomesauce/happycloud_" + version + ".zip",
				"shasums_url":           "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS",
				"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + version + "_SHA256SUMS.sig",
				"signing_keys": map[string]interface{}{
					"gpg_public_keys": []map[string]interface{}{
						{
							"ascii_armor": getproviders.HashicorpPublicKey,
						},
					},
				},
			}
			enc, err := json.Marshal(body)
			if err != nil {
				resp.WriteHeader(500)
				resp.Write([]byte("failed to encode body"))
			}
			resp.Header().Set("Content-Type", "application/json")
			resp.WriteHeader(200)
			resp.Write(enc)
		default:
			resp.WriteHeader(404)
			resp.Write([]byte(`unknown namespace/provider/version/architecture`))
		}
		return
	}

	resp.WriteHeader(404)
	resp.Write([]byte(`unrecognized path scheme`))
}

// In order to be able to compare the recorded temp dir paths, we need to
// normalize the path to match what the installer would report.
func tmpDir(t *testing.T) string {
	unlinked, err := filepath.EvalSymlinks(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	return filepath.Clean(unlinked)
}
