//go:build mocks

package getproviders_test

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	"github.com/gruntwork-io/terragrunt/internal/tf/getproviders"
	"github.com/gruntwork-io/terragrunt/internal/tf/getproviders/mocks"
	"github.com/gruntwork-io/terragrunt/test/helpers"
	"github.com/gruntwork-io/terragrunt/test/helpers/logger"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/mock/gomock"
)

func mockProviderWithConstraints(t *testing.T, ctrl *gomock.Controller, address, ver, constraints string) getproviders.Provider {
	t.Helper()

	packageDir := helpers.TmpDirWOSymlinks(t)
	file, err := os.Create(filepath.Join(packageDir, "terraform-provider-v"+ver))
	require.NoError(t, err)
	_, err = fmt.Fprintf(file, "mock-provider-content-%s-%s", address, ver)
	require.NoError(t, err)
	err = file.Close()
	require.NoError(t, err)

	var document string

	var documentSB strings.Builder

	for i := 0; i < 2; i++ {
		packageName := fmt.Sprintf("%s-%s-%d", address, ver, i)
		hasher := sha256.New()
		_, err := hasher.Write([]byte(packageName))
		require.NoError(t, err)

		sha := hex.EncodeToString(hasher.Sum(nil))
		fmt.Fprintf(&documentSB, "%s %s\n", sha, packageName)
	}

	document += documentSB.String()

	provider := mocks.NewMockProvider(ctrl)
	provider.EXPECT().Address().Return(address).AnyTimes()
	provider.EXPECT().Version().Return(ver).AnyTimes()
	provider.EXPECT().Constraints().Return(constraints).AnyTimes()
	provider.EXPECT().PackageDir().Return(packageDir).AnyTimes()
	provider.EXPECT().Logger().Return(logger.CreateLogger()).AnyTimes()
	provider.EXPECT().DocumentSHA256Sums(gomock.Any()).Return([]byte(document), nil).AnyTimes()
	provider.EXPECT().RegistryHashes().Return(nil).AnyTimes()

	return provider
}

func mockProviderUpdateLock(t *testing.T, ctrl *gomock.Controller, address, version string) getproviders.Provider {
	t.Helper()

	packageDir := helpers.TmpDirWOSymlinks(t)
	file, err := os.Create(filepath.Join(packageDir, "terraform-provider-v"+version))
	require.NoError(t, err)
	_, err = fmt.Fprintf(file, "mock-provider-content-%s-%s", address, version)
	require.NoError(t, err)
	err = file.Close()
	require.NoError(t, err)

	var document string

	var documentSB strings.Builder

	for i := 0; i < 2; i++ {
		packageName := fmt.Sprintf("%s-%s-%d", address, version, i)
		hasher := sha256.New()
		_, err := hasher.Write([]byte(packageName))
		require.NoError(t, err)

		sha := hex.EncodeToString(hasher.Sum(nil))
		fmt.Fprintf(&documentSB, "%s %s\n", sha, packageName)
	}

	document += documentSB.String()

	provider := mocks.NewMockProvider(ctrl)
	provider.EXPECT().Address().Return(address).AnyTimes()
	provider.EXPECT().Version().Return(version).AnyTimes()
	provider.EXPECT().Constraints().Return("").AnyTimes()
	provider.EXPECT().PackageDir().Return(packageDir).AnyTimes()
	provider.EXPECT().Logger().Return(logger.CreateLogger()).AnyTimes()
	provider.EXPECT().DocumentSHA256Sums(gomock.Any()).Return([]byte(document), nil).AnyTimes()
	provider.EXPECT().RegistryHashes().Return(nil).AnyTimes()

	return provider
}

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

	testCases := []struct {
		initialLockfile  string
		expectedLockfile string
		providers        []getproviders.Provider
	}{
		{
			providers:       []getproviders.Provider{},
			initialLockfile: ``,
			expectedLockfile: `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.37.0"
  constraints = "5.37.0"
  hashes = [
    "h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=",
    "zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398",
    "zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f",
  ]
}
`,
		},
		{
			providers: []getproviders.Provider{},
			initialLockfile: `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.37.0"
  constraints = "5.37.0"
  hashes = [
    "h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=",
    "zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398",
    "zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f",
  ]
}

provider "registry.terraform.io/hashicorp/azurerm" {
  version     = "3.101.0"
  constraints = "3.101.0"
  hashes = [
    "h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=",
    "zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d",
    "zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369",
  ]
}
`,
			expectedLockfile: `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.36.0"
  constraints = "5.36.0"
  hashes = [
    "h1:RpTjHdEAYqidB9hFPs68dIhkeIE1c/ZH9fEBdddf0Ik=",
    "zh:8721239b83a06212fb2f474d2acddfa2659a224ef66c77e28e1efe2290a30fa7",
    "zh:ed83a9620eab99e091b9f786f20f03fddb50cba030839fe0529bd518bfd67f8d",
  ]
}

provider "registry.terraform.io/hashicorp/azurerm" {
  version     = "3.101.0"
  constraints = "3.101.0"
  hashes = [
    "h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=",
    "zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d",
    "zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369",
  ]
}

provider "registry.terraform.io/hashicorp/template" {
  version     = "2.2.0"
  constraints = "2.2.0"
  hashes = [
    "h1:kvJsWhTmFya0WW8jAfY40fDtYhWQ6mOwPQC2ncDNjZs=",
    "zh:02d170f0a0f453155686baf35c10b5a7a230ef20ca49f6e26de1c2691ac70a59",
    "zh:d88ec10849d5a1d9d1db458847bbc62049f0282a2139e5176d645b75a0346992",
  ]
}
`,
		},
		{
			providers: []getproviders.Provider{},
			initialLockfile: `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.36.0"
  constraints = ">= 5.36.0"
  hashes = [
    "h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=",
    "zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398",
    "zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f",
  ]
}

provider "registry.terraform.io/hashicorp/template" {
  version     = "2.1.0"
  constraints = "<= 2.1.0"
  hashes = [
    "h1:vxE/PD8SWl6Lmh5zRvIW1Y559xfUyuV2T/VeQLXi7f0=",
    "zh:6fc271665ac28c3fee773b9dc2b8066280ba35b7e9a14a6148194a240c43f42a",
    "zh:c19f719c9f7ce6d7449fe9c020100faed0705303c7f95beeef81dfd1e4a2004b",
  ]
}
`,
			expectedLockfile: `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.37.0"
  constraints = ">= 5.36.0"
  hashes = [
    "h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=",
    "zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398",
    "zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f",
  ]
}

provider "registry.terraform.io/hashicorp/template" {
  version     = "2.2.0"
  constraints = "2.2.0"
  hashes = [
    "h1:kvJsWhTmFya0WW8jAfY40fDtYhWQ6mOwPQC2ncDNjZs=",
    "zh:02d170f0a0f453155686baf35c10b5a7a230ef20ca49f6e26de1c2691ac70a59",
    "zh:d88ec10849d5a1d9d1db458847bbc62049f0282a2139e5176d645b75a0346992",
  ]
}
`,
		},
	}

	for i, tc := range testCases {
		t.Run(fmt.Sprintf("testCase-%d", i), func(t *testing.T) {
			t.Parallel()

			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			switch i {
			case 0:
				tc.providers = []getproviders.Provider{
					mockProviderUpdateLock(t, ctrl, "registry.terraform.io/hashicorp/aws", "5.37.0"),
				}
			case 1:
				tc.providers = []getproviders.Provider{
					mockProviderUpdateLock(t, ctrl, "registry.terraform.io/hashicorp/aws", "5.36.0"),
					mockProviderUpdateLock(t, ctrl, "registry.terraform.io/hashicorp/template", "2.2.0"),
				}
			case 2:
				tc.providers = []getproviders.Provider{
					mockProviderUpdateLock(t, ctrl, "registry.terraform.io/hashicorp/aws", "5.37.0"),
					mockProviderUpdateLock(t, ctrl, "registry.terraform.io/hashicorp/template", "2.2.0"),
				}
			}

			workingDir := helpers.TmpDirWOSymlinks(t)
			lockfilePath := filepath.Join(workingDir, ".terraform.lock.hcl")

			if tc.initialLockfile != "" {
				file, err := os.Create(lockfilePath)
				require.NoError(t, err)
				_, err = file.WriteString(tc.initialLockfile)
				require.NoError(t, err)
				err = file.Close()
				require.NoError(t, err)
			}

			err := getproviders.UpdateLockfile(t.Context(), workingDir, tc.providers)
			require.NoError(t, err)

			actualLockfile, err := os.ReadFile(lockfilePath)
			require.NoError(t, err)

			assert.Equal(t, tc.expectedLockfile, string(actualLockfile))
		})
	}
}

// mockProviderWithRegistryHashes returns a mock Provider that exposes
// per-platform hashes matching the OpenTofu registry's `packages` response
// field. The locally-computed `h1:` hash for the unpacked package is still
// merged in alongside the registry-supplied hashes.
func mockProviderWithRegistryHashes(t *testing.T, ctrl *gomock.Controller, address, ver string, hashesByPlatform map[string][]getproviders.Hash) (getproviders.Provider, string) {
	t.Helper()

	packageDir := helpers.TmpDirWOSymlinks(t)
	file, err := os.Create(filepath.Join(packageDir, "terraform-provider-v"+ver))
	require.NoError(t, err)
	_, err = fmt.Fprintf(file, "mock-provider-content-%s-%s", address, ver)
	require.NoError(t, err)
	require.NoError(t, file.Close())

	provider := mocks.NewMockProvider(ctrl)
	provider.EXPECT().Address().Return(address).AnyTimes()
	provider.EXPECT().Version().Return(ver).AnyTimes()
	provider.EXPECT().Constraints().Return("").AnyTimes()
	provider.EXPECT().PackageDir().Return(packageDir).AnyTimes()
	provider.EXPECT().Logger().Return(logger.CreateLogger()).AnyTimes()
	provider.EXPECT().RegistryHashes().Return(hashesByPlatform).AnyTimes()

	return provider, packageDir
}

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

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	provider, packageDir := mockProviderWithRegistryHashes(t, ctrl,
		"registry.opentofu.org/hashicorp/aws", "5.37.0",
		map[string][]getproviders.Hash{
			"linux_amd64": {
				"zh:aaaa000000000000000000000000000000000000000000000000000000000001",
				"h1:LinuxAmd64HashOpentofuRegistryProvidedValueAAA=",
			},
			"darwin_arm64": {
				"zh:bbbb000000000000000000000000000000000000000000000000000000000002",
				"h1:DarwinArm64HashOpentofuRegistryProvidedValueBBB=",
			},
		},
	)

	localH1, err := getproviders.PackageHashV1(packageDir)
	require.NoError(t, err)

	workingDir := helpers.TmpDirWOSymlinks(t)
	lockfilePath := filepath.Join(workingDir, ".terraform.lock.hcl")

	err = getproviders.UpdateLockfile(t.Context(), workingDir, []getproviders.Provider{provider})
	require.NoError(t, err)

	actual, err := os.ReadFile(lockfilePath)
	require.NoError(t, err)

	sortedHashes := []string{
		localH1.String(),
		"h1:DarwinArm64HashOpentofuRegistryProvidedValueBBB=",
		"h1:LinuxAmd64HashOpentofuRegistryProvidedValueAAA=",
		"zh:aaaa000000000000000000000000000000000000000000000000000000000001",
		"zh:bbbb000000000000000000000000000000000000000000000000000000000002",
	}
	slices.Sort(sortedHashes)

	var hashesBlock strings.Builder
	for _, h := range sortedHashes {
		fmt.Fprintf(&hashesBlock, "    %q,\n", h)
	}

	expected := fmt.Sprintf(`
provider "registry.opentofu.org/hashicorp/aws" {
  version     = "5.37.0"
  constraints = "5.37.0"
  hashes = [
%s  ]
}
`, hashesBlock.String())

	assert.Equal(t, expected, string(actual))
}

// TestMockUpdateLockfilePreservesAggregatedConstraints verifies that when a lock file
// already has valid aggregated constraints from the full dependency tree (e.g.
// ">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0"), and the provider's module-only
// constraints differ (e.g. ">= 3.0.0, < 7.0.0"), the lock file constraints are
// preserved as-is because the version still satisfies them.
// This is the bug described in https://github.com/gruntwork-io/terragrunt/issues/5616
func TestMockUpdateLockfilePreservesAggregatedConstraints(t *testing.T) {
	t.Parallel()

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// Create a provider whose module-level constraints differ from the lock file's
	// aggregated constraints, but whose version satisfies the lock file constraints.
	provider := mockProviderWithConstraints(t, ctrl,
		"registry.terraform.io/hashicorp/aws", "5.37.0",
		">= 3.0.0, < 7.0.0", // module-only constraints (subset of lock file)
	)

	workingDir := helpers.TmpDirWOSymlinks(t)
	lockfilePath := filepath.Join(workingDir, ".terraform.lock.hcl")

	// Write a lock file with aggregated constraints from the full dependency tree.
	initialLockfile := `
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.37.0"
  constraints = ">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0"
  hashes = [
    "h1:existing-hash=",
  ]
}
`
	err := os.WriteFile(lockfilePath, []byte(initialLockfile), 0644)
	require.NoError(t, err)

	err = getproviders.UpdateLockfile(t.Context(), workingDir, []getproviders.Provider{provider})
	require.NoError(t, err)

	actualLockfile, err := os.ReadFile(lockfilePath)
	require.NoError(t, err)

	// The constraints line must be preserved as the original aggregated constraints,
	// NOT overwritten with the module-only constraints.
	assert.Contains(t, string(actualLockfile), `constraints = ">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0"`,
		"Lock file constraints should be preserved, not overwritten with module-only constraints")
	assert.NotContains(t, string(actualLockfile), `constraints = ">= 3.0.0, < 7.0.0"`,
		"Module-only constraints should not replace the aggregated constraints")
}
