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

package rpcapi

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"strings"
	"testing"

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages"
)

func TestPackagesServer_ProviderPackageVersions(t *testing.T) {

	tcs := map[string]struct {
		source           string
		expectedVersions []string
		expectedWarnings []string
		sourceFn         providerSourceFn
	}{
		"single_version": {
			source:           "hashicorp/foo",
			expectedVersions: []string{"0.1.0"},
			sourceFn: func(_ *disco.Disco) getproviders.Source {
				packages := []getproviders.PackageMeta{
					{
						Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
						Version:  versions.MustParseVersion("0.1.0"),
					},
				}
				return getproviders.NewMockSource(packages, nil)
			},
		},
		"multiple_versions": {
			source:           "hashicorp/foo",
			expectedVersions: []string{"0.1.0", "0.2.0"},
			sourceFn: func(_ *disco.Disco) getproviders.Source {
				packages := []getproviders.PackageMeta{
					{
						Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
						Version:  versions.MustParseVersion("0.1.0"),
					},
					{
						Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
						Version:  versions.MustParseVersion("0.2.0"),
					},
				}
				return getproviders.NewMockSource(packages, nil)
			},
		},
		"with_warnings": {
			source:           "hashicorp/foo",
			expectedVersions: []string{"0.1.0"},
			expectedWarnings: []string{"- warning one", "- warning two"},
			sourceFn: func(_ *disco.Disco) getproviders.Source {
				packages := []getproviders.PackageMeta{
					{
						Provider: addrs.MustParseProviderSourceString("hashicorp/foo"),
						Version:  versions.MustParseVersion("0.1.0"),
					},
				}
				warnings := map[addrs.Provider]getproviders.Warnings{
					addrs.MustParseProviderSourceString("hashicorp/foo"): {
						"warning one",
						"warning two",
					},
				}
				return getproviders.NewMockSource(packages, warnings)
			},
		},
	}
	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			service := &packagesServer{
				providerSourceFn: tc.sourceFn,
			}

			response, err := service.ProviderPackageVersions(context.Background(), &packages.ProviderPackageVersions_Request{
				SourceAddr: tc.source,
			})
			if err != nil {
				t.Fatal(err)
			}

			if len(tc.expectedWarnings) > 0 {
				for _, diag := range response.Diagnostics {
					if diag.Severity == terraform1.Diagnostic_WARNING && diag.Summary == "Additional provider information from registry" {
						expected := fmt.Sprintf("The remote registry returned warnings for %s:\n%s", tc.source, strings.Join(tc.expectedWarnings, "\n"))
						if diff := cmp.Diff(expected, diag.Detail); len(diff) > 0 {
							t.Error(diff)
						}
					}
				}

				// We're expecting only one diagnostic with the warnings.
				if len(response.Diagnostics) > 1 {
					for _, diag := range response.Diagnostics {
						t.Errorf("unexpected diagnostics: %s", diag.Detail)
					}
					return
				}
			} else {
				// Otherwise we're expecting no diagnostics.
				if len(response.Diagnostics) > 0 {
					for _, diag := range response.Diagnostics {
						t.Errorf("unexpected diagnostics: %s", diag.Detail)
					}
					return
				}
			}

			if diff := cmp.Diff(tc.expectedVersions, response.Versions); len(diff) > 0 {
				t.Error(diff)
			}
		})
	}

}

func TestPackagesServer_FetchProviderPackage(t *testing.T) {
	providerHashes := providerHashes(t)

	tcs := map[string]struct {
		// source, version, platforms, and hashes are what we're going to pass
		// in as the request.
		source    string
		version   string
		platforms []string
		hashes    []string

		// platformLocations, and platformHashes are what we're going to use to
		// create our virtual provider metadata.
		platformLocations map[string]string
		platformHashes    map[string][]string

		// diagnostics are the expected diagnostics for each platform.
		diagnostics map[string][]string
	}{
		"single_version_and_platform": {
			source:    "hashicorp/foo",
			version:   "0.1.0",
			platforms: []string{"linux_amd64"},
			platformLocations: map[string]string{
				"linux_amd64": "terraform_provider_foo",
			},
		},
		"single_version_multiple_platforms": {
			source:    "hashicorp/foo",
			version:   "0.1.0",
			platforms: []string{"linux_amd64", "darwin_arm64"},
			platformLocations: map[string]string{
				"linux_amd64":  "terraform_provider_foo",
				"darwin_arm64": "terraform_provider_bar",
			},
		},
		"single_version_and_platform_with_hashes": {
			source:    "hashicorp/foo",
			version:   "0.1.0",
			platforms: []string{"linux_amd64"},
			platformLocations: map[string]string{
				"linux_amd64": "terraform_provider_foo",
			},
			platformHashes: map[string][]string{
				"linux_amd64": {
					"h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=",
				},
			},
		},
		"single_version_and_platform_with_hashes_clash": {
			source:    "hashicorp/foo",
			version:   "0.1.0",
			hashes:    []string{"h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="},
			platforms: []string{"linux_amd64"},
			platformLocations: map[string]string{
				"linux_amd64": "terraform_provider_foo",
			},
			platformHashes: map[string][]string{
				"linux_amd64": {
					"h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=",
				},
			},
			diagnostics: map[string][]string{
				"linux_amd64": {
					"the local package for registry.terraform.io/hashicorp/foo 0.1.0 doesn't match any of the checksums previously recorded in the dependency lock file",
				},
			},
		},
	}
	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			service := &packagesServer{
				providerSourceFn: func(_ *disco.Disco) getproviders.Source {
					var providers []getproviders.PackageMeta
					for _, p := range tc.platforms {
						platform := parsePlatform(t, p)

						var authentication getproviders.PackageAuthentication
						if len(tc.platformHashes) > 0 {
							authentication = getproviders.NewPackageHashAuthentication(platform, func() []getproviders.Hash {
								var hashes []getproviders.Hash
								for _, hash := range tc.platformHashes[p] {
									hashes = append(hashes, getproviders.Hash(hash))
								}
								return hashes
							}())
						}

						providers = append(providers, getproviders.PackageMeta{
							Provider:       addrs.MustParseProviderSourceString(tc.source),
							Version:        versions.MustParseVersion(tc.version),
							TargetPlatform: platform,
							Location:       getproviders.PackageLocalDir(path.Join("testdata", "providers", tc.platformLocations[p])),
							Authentication: authentication,
						})
					}

					return getproviders.NewMockSource(providers, nil)
				},
			}

			cacheDir := t.TempDir()
			response, err := service.FetchProviderPackage(context.Background(), &packages.FetchProviderPackage_Request{
				CacheDir:   cacheDir,
				SourceAddr: tc.source,
				Version:    tc.version,
				Platforms:  tc.platforms,
				Hashes:     tc.hashes,
			})
			if err != nil {
				t.Fatal(err)
			}

			if len(response.Diagnostics) > 0 {
				for _, diag := range response.Diagnostics {
					t.Errorf("unexpected diagnostics: %s", diag.Detail)
				}
				return
			}

			if len(response.Results) != len(tc.platforms) {
				t.Fatalf("wrong number of results")
			}

			for ix, platform := range tc.platforms {
				result := response.Results[ix]

				if tc.diagnostics != nil && len(tc.diagnostics[platform]) > 0 {
					if len(result.Diagnostics) != len(tc.diagnostics[platform]) {
						t.Fatalf("expected %d diagnostics for %s but found %d", len(tc.diagnostics[platform]), platform, len(result.Diagnostics))
					}
					for ix, expected := range tc.diagnostics[platform] {
						if !strings.Contains(result.Diagnostics[ix].Detail, expected) {
							t.Errorf("expected: %s\nactual:    %s", expected, result.Diagnostics[ix])
						}
					}

					return
				} else {
					if len(result.Diagnostics) > 0 {
						for _, diag := range result.Diagnostics {
							t.Errorf("unexpected diagnostics for %s: %s", platform, diag.Detail)
						}
						return
					}
				}

				if diff := cmp.Diff(providerHashes[tc.platformLocations[platform]], result.Provider.Hashes); len(diff) > 0 {
					t.Error(diff)
				}
			}
		})
	}
}

func providerHashes(t *testing.T) map[string][]string {
	var hashes map[string][]string

	data, err := os.ReadFile("testdata/providers/hashes.json")
	if err != nil {
		t.Fatal(err)
	}

	if err := json.Unmarshal(data, &hashes); err != nil {
		t.Fatal(err)
	}

	return hashes
}

func parsePlatform(t *testing.T, raw string) getproviders.Platform {
	platform, err := getproviders.ParsePlatform(raw)
	if err != nil {
		t.Fatal(err)
	}
	return platform
}
