package getproviders

import (
	"archive/zip"
	"context"
	"crypto/sha256"
	"fmt"
	"io"
	"os"
	"testing"

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

// MockSource is an in-memory-only, statically-configured source intended for
// use only in unit tests of other subsystems that consume provider sources.
//
// The MockSource also tracks calls to it in case a calling test wishes to
// assert that particular calls were made.
//
// This should not be used outside of unit test code.
type MockSource struct {
	packages []PackageMeta
	warnings map[addrs.Provider]Warnings
	calls    [][]interface{}
}

var _ Source = (*MockSource)(nil)

// NewMockSource creates and returns a MockSource with the given packages.
//
// The given packages don't necessarily need to refer to objects that actually
// exist on disk or over the network, unless the calling test is planning to
// use (directly or indirectly) the results for further provider installation
// actions.
func NewMockSource(packages []PackageMeta, warns map[addrs.Provider]Warnings) *MockSource {
	return &MockSource{
		packages: packages,
		warnings: warns,
	}
}

// AvailableVersions returns all of the versions of the given provider that
// are available in the fixed set of packages that were passed to
// NewMockSource when creating the receiving source.
func (s *MockSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
	s.calls = append(s.calls, []interface{}{"AvailableVersions", provider})
	var ret VersionList
	for _, pkg := range s.packages {
		if pkg.Provider == provider {
			ret = append(ret, pkg.Version)
		}
	}
	var warns []string
	if s.warnings != nil {
		if warnings, ok := s.warnings[provider]; ok {
			warns = warnings
		}
	}
	if len(ret) == 0 {
		// In this case, we'll behave like a registry that doesn't know about
		// this provider at all, rather than just returning an empty result.
		return nil, warns, ErrRegistryProviderNotKnown{provider}
	}
	ret.Sort()
	return ret, warns, nil
}

// PackageMeta returns the first package from the list given to NewMockSource
// when creating the receiver that has the given provider, version, and
// target platform.
//
// If none of the packages match, it returns ErrPlatformNotSupported to
// simulate the situation where a provider release isn't available for a
// particular platform.
//
// Note that if the list of packages passed to NewMockSource contains more
// than one with the same provider, version, and target this function will
// always return the first one in the list, which may not match the behavior
// of other sources in an equivalent situation because it's a degenerate case
// with undefined results.
func (s *MockSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
	s.calls = append(s.calls, []interface{}{"PackageMeta", provider, version, target})

	for _, pkg := range s.packages {
		if pkg.Provider != provider {
			continue
		}
		if pkg.Version != version {
			// (We're using strict equality rather than precedence here,
			// because this is an exact version specification. The caller
			// should consider precedence when selecting a version in the
			// AvailableVersions response, and pass the exact selected
			// version here.)
			continue
		}
		if pkg.TargetPlatform != target {
			continue
		}
		return pkg, nil
	}

	// If we fall out here then nothing matched at all, so we'll treat that
	// as "platform not supported" for consistency with RegistrySource.
	return PackageMeta{}, ErrPlatformNotSupported{
		Provider: provider,
		Version:  version,
		Platform: target,
	}
}

// CallLog returns a list of calls to other methods of the receiever that have
// been called since it was created, in case a calling test wishes to verify
// a particular sequence of operations.
//
// The result is a slice of slices where the first element of each inner slice
// is the name of the method that was called, and then any subsequent elements
// are positional arguments passed to that method.
//
// Callers are forbidden from modifying any objects accessible via the returned
// value.
func (s *MockSource) CallLog() [][]interface{} {
	return s.calls
}

// FakePackageMeta constructs and returns a PackageMeta that carries the given
// metadata but has fake location information that is likely to fail if
// attempting to install from it.
func FakePackageMeta(provider addrs.Provider, version Version, protocols VersionList, target Platform) PackageMeta {
	return PackageMeta{
		Provider:         provider,
		Version:          version,
		ProtocolVersions: protocols,
		TargetPlatform:   target,

		// Some fake but somewhat-realistic-looking other metadata. This
		// points nowhere, so will fail if attempting to actually use it.
		Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
		Location: PackageHTTPURL(fmt.Sprintf("https://fake.invalid/terraform-provider-%s_%s.zip", provider.Type, version.String())),
	}
}

// FakeInstallablePackageMeta constructs and returns a PackageMeta that points
// to a temporary archive file that could actually be installed in principle.
//
// Installing it will not produce a working provider though: just a fake file
// posing as an executable. The filename for the executable defaults to the
// standard terraform-provider-NAME_X.Y.Z format, but can be overridden with
// the execFilename argument.
//
// Cleanup functions are handled internally using t.Cleanup.
func FakeInstallablePackageMeta(t *testing.T, provider addrs.Provider, version Version, protocols VersionList, target Platform, execFilename string) (PackageMeta, error) {
	t.Helper()

	f, checksum, err := CreateFakeFileWithChecksumForProvider(t, provider, version, target, execFilename)
	if err != nil {
		return PackageMeta{}, err
	}

	meta := PackageMeta{
		Provider:         provider,
		Version:          version,
		ProtocolVersions: protocols,
		TargetPlatform:   target,

		Location: PackageLocalArchive(f.Name()),

		// This is a fake filename that mimics what a real registry might
		// indicate as a good filename for this package, in case some caller
		// intends to use it to name a local copy of the temporary file.
		// (At the time of writing, no caller actually does that, but who
		// knows what the future holds?)
		Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),

		Authentication: NewArchiveChecksumAuthentication(target, checksum),
	}
	return meta, nil
}

// This is basically the same as FakePackageMeta, except that we'll use a PackageHTTPURL instead of a PackageLocalArchive when creating metadata for the provider.
// By doing so, we create a mock source that makes Terraform believe it's downloading the provider via HTTP, instead of from a local archive.
//
// The caller is responsible for calling the close callback to clean up the temporary file.
// The temporary file is only used to calculate checksums and isn't actually used to install the provider in the test.
func FakePackageMetaViaHTTP(t *testing.T, provider addrs.Provider, version Version, protocols VersionList, target Platform, locationBaseUrl string) (PackageMeta, error) {
	f, checksum, err := CreateFakeFileWithChecksumForProvider(t, provider, version, target, "")
	if err != nil {
		return PackageMeta{}, err
	}

	meta := PackageMeta{
		Provider:         provider,
		Version:          version,
		ProtocolVersions: protocols,
		TargetPlatform:   target,

		Location: PackageHTTPURL(
			fmt.Sprintf(
				"http://%[1]s/terraform-provider-%[2]s/%[3]s/terraform-provider-%[2]s_%[3]s_%[4]s.zip",
				locationBaseUrl,
				provider.Type,
				version.String(),
				target.String(),
			),
		),

		// This is a fake filename that mimics what a real registry might
		// indicate as a good filename for this package, in case some caller
		// intends to use it to name a local copy of the temporary file.
		// (At the time of writing, no caller actually does that, but who
		// knows what the future holds?)
		Filename: f.Name(),

		Authentication: NewArchiveChecksumAuthentication(target, checksum),
	}
	return meta, nil
}

// CreateFakeFileWithChecksumForProvider creates a temporary zip file containing a fake provider executable for a given combo of:
//   - provider address
//   - version
//   - target platform
//
// The function returns an os.File pointing to the temp file and a checksum of its contents. These are intended to be used to
// create mock provider sources.
//
// Cleanup functions (closing the file and deleting it) are handled internally using t.Cleanup.
func CreateFakeFileWithChecksumForProvider(t *testing.T, provider addrs.Provider, version Version, target Platform, execFilename string) (*os.File, [32]byte, error) {
	t.Helper()

	f, err := os.CreateTemp("", "terraform-getproviders-fake-package-")
	if err != nil {
		return nil, [32]byte{}, err
	}

	// Handle cleanup of the temporary file using t.Cleanup.
	close := func() {
		f.Close()
		os.Remove(f.Name())
	}
	t.Cleanup(close)

	if execFilename == "" {
		execFilename = fmt.Sprintf("terraform-provider-%s_%s", provider.Type, version.String())
		if target.OS == "windows" {
			// For a little more (technically unnecessary) realism...
			execFilename += ".exe"
		}
	}

	// Create mock zip file
	zw := zip.NewWriter(f)
	fw, err := zw.Create(execFilename)
	if err != nil {
		return nil, [32]byte{}, fmt.Errorf("failed to add %s to mock zip file: %s", execFilename, err)
	}
	fmt.Fprintf(fw, "This is a fake provider package for %s %s, not a real provider.\n", provider, version)
	err = zw.Close()
	if err != nil {
		return nil, [32]byte{}, fmt.Errorf("failed to close the mock zip file: %s", err)
	}

	// Compute the SHA256 checksum of the generated file, to allow package
	// authentication code to be exercised.
	f.Seek(0, io.SeekStart)
	h := sha256.New()
	io.Copy(h, f)
	checksum := [32]byte{}
	h.Sum(checksum[:0])

	return f, checksum, nil
}

func (s *MockSource) ForDisplay(provider addrs.Provider) string {
	return "mock source"
}
