// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build python || all

package ints

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/pulumi/pulumi/pkg/v3/testing/integration"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
	ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
	"github.com/pulumi/pulumi/sdk/v3/python"
)

func boolPointer(b bool) *bool {
	return &b
}

// TestEmptyPython simply tests that we can run an empty Python project.
//
//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestEmptyPython(t *testing.T) {
	integration.ProgramTest(t, &integration.ProgramTestOptions{
		Dir: filepath.Join("empty", "python"),
		Dependencies: []string{
			filepath.Join("..", "..", "sdk", "python", "env", "src"),
		},
		Quick: true,
	})
}

//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestStackReferencePython(t *testing.T) {
	t.Skip("Temporarily skipping test - pulumi/pulumi#14765")
	opts := &integration.ProgramTestOptions{
		RequireService: true,

		Dir: filepath.Join("stack_reference", "python"),
		Dependencies: []string{
			filepath.Join("..", "..", "sdk", "python", "env", "src"),
		},
		Quick: true,
		EditDirs: []integration.EditDir{
			{
				Dir:      filepath.Join("stack_reference", "python", "step1"),
				Additive: true,
			},
			{
				Dir:      filepath.Join("stack_reference", "python", "step2"),
				Additive: true,
			},
		},
	}
	integration.ProgramTest(t, opts)
}

// Tests dynamic provider in Python.
//
//nolint:paralleltest // ProgramTest calls t.Parallel()
func TestDynamicPython(t *testing.T) {
	var randomVal string
	integration.ProgramTest(t, &integration.ProgramTestOptions{
		Dir: filepath.Join("dynamic", "python"),
		Dependencies: []string{
			filepath.Join("..", "..", "sdk", "python", "env", "src"),
		},
		ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
			randomVal = stack.Outputs["random_val"].(string)
		},
		EditDirs: []integration.EditDir{{
			Dir:      filepath.Join("dynamic", "python", "step1"),
			Additive: true,
			ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
				assert.Equal(t, randomVal, stack.Outputs["random_val"].(string))

				// Regression testing the workaround for https://github.com/pulumi/pulumi/issues/8265
				// Ensure the __provider input and output was marked secret
				assertIsSecret := func(v interface{}) {
					switch v := v.(type) {
					case string:
						assert.Fail(t, "__provider was not a secret")
					case map[string]interface{}:
						assert.Equal(t, resource.SecretSig, v[resource.SigKey])
					}
				}

				dynRes := stack.Deployment.Resources[2]
				assertIsSecret(dynRes.Inputs["__provider"])
				assertIsSecret(dynRes.Outputs["__provider"])

				// Ensure there are no diagnostic events other than debug.
				for _, event := range stack.Events {
					if event.DiagnosticEvent != nil {
						assert.Equal(t, "debug", event.DiagnosticEvent.Severity,
							"unexpected diagnostic event: %#v", event.DiagnosticEvent)
					}
				}
			},
		}},
		UseSharedVirtualEnv: boolPointer(false),
	})
}

// Test remote component construction in Python.
func TestConstructPython(t *testing.T) {
	t.Parallel()

	testDir := "construct_component"
	runComponentSetup(t, testDir)

	tests := []struct {
		componentDir          string
		expectedResourceCount int
		env                   []string
	}{
		{
			componentDir:          "testcomponent",
			expectedResourceCount: 9,
			// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
			// Until we've addressed this, set PULUMI_TEST_YARN_LINK_PULUMI, which tells the integration test
			// module to run `yarn install && yarn link @pulumi/pulumi` in the Go program's directory, allowing
			// the Node.js dynamic provider plugin to load.
			// When the underlying issue has been fixed, the use of this environment variable inside the integration
			// test module should be removed.
			env: []string{"PULUMI_TEST_YARN_LINK_PULUMI=true"},
		},
		{
			componentDir:          "testcomponent-python",
			expectedResourceCount: 9,
		},
		{
			componentDir:          "testcomponent-go",
			expectedResourceCount: 8, // One less because no dynamic provider.
		},
	}

	//nolint:paralleltest // ProgramTest calls t.Parallel()
	for _, test := range tests {
		test := test
		t.Run(test.componentDir, func(t *testing.T) {
			localProviders := []integration.LocalDependency{
				{Package: "testcomponent", Path: filepath.Join(testDir, test.componentDir)},
			}
			integration.ProgramTest(t,
				optsForConstructPython(t, test.expectedResourceCount, localProviders, test.env...))
		})
	}
}

func optsForConstructPython(
	t *testing.T, expectedResourceCount int, localProviders []integration.LocalDependency, env ...string,
) *integration.ProgramTestOptions {
	return &integration.ProgramTestOptions{
		Env: env,
		Dir: filepath.Join("construct_component", "python"),
		Dependencies: []string{
			filepath.Join("..", "..", "sdk", "python", "env", "src"),
		},
		LocalProviders: localProviders,
		Secrets: map[string]string{
			"secret": "this super secret is encrypted",
		},
		Quick:               true,
		UseSharedVirtualEnv: boolPointer(false),
		ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
			assert.NotNil(t, stackInfo.Deployment)
			if assert.Equal(t, expectedResourceCount, len(stackInfo.Deployment.Resources)) {
				stackRes := stackInfo.Deployment.Resources[0]
				assert.NotNil(t, stackRes)
				assert.Equal(t, resource.RootStackType, stackRes.Type)
				assert.Equal(t, "", string(stackRes.Parent))

				// Check that dependencies flow correctly between the originating program and the remote component
				// plugin.
				urns := make(map[string]resource.URN)
				for _, res := range stackInfo.Deployment.Resources[1:] {
					assert.NotNil(t, res)

					urns[res.URN.Name()] = res.URN
					switch res.URN.Name() {
					case "child-a":
						for _, deps := range res.PropertyDependencies {
							assert.Empty(t, deps)
						}
					case "child-b":
						expected := []resource.URN{urns["a"]}
						assert.ElementsMatch(t, expected, res.Dependencies)
						assert.ElementsMatch(t, expected, res.PropertyDependencies["echo"])
					case "child-c":
						expected := []resource.URN{urns["a"], urns["child-a"]}
						assert.ElementsMatch(t, expected, res.Dependencies)
						assert.ElementsMatch(t, expected, res.PropertyDependencies["echo"])
					case "a", "b", "c":
						secretPropValue, ok := res.Outputs["secret"].(map[string]interface{})
						assert.Truef(t, ok, "secret output was not serialized as a secret")
						assert.Equal(t, resource.SecretSig, secretPropValue[resource.SigKey].(string))
					}
				}
			}
		},
	}
}

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

	const testDir = "construct_component_configure_provider"
	runComponentSetup(t, testDir)
	pulumiRoot, err := filepath.Abs("../..")
	require.NoError(t, err)
	pulumiPySDK := filepath.Join("..", "..", "sdk", "python", "env", "src")
	componentSDK := filepath.Join(pulumiRoot, "pkg/codegen/testing/test/testdata/methods-return-plain-resource/python")
	opts := testConstructComponentConfigureProviderCommonOptions()
	opts = opts.With(integration.ProgramTestOptions{
		Dir:          filepath.Join(testDir, "python"),
		Dependencies: []string{pulumiPySDK, componentSDK},
		NoParallel:   true,
	})
	integration.ProgramTest(t, &opts)
}

// Regresses https://github.com/pulumi/pulumi/issues/6471
func TestAutomaticVenvCreation(t *testing.T) {
	t.Parallel()

	// Do not use integration.ProgramTest to avoid automatic venv
	// handling by test harness; we actually are testing venv
	// handling by the pulumi CLI itself.

	check := func(t *testing.T, venvPathTemplate string, dir string) {
		e := ptesting.NewEnvironment(t)
		defer func() {
			if !t.Failed() {
				e.DeleteEnvironment()
			}
		}()

		venvPath := strings.ReplaceAll(venvPathTemplate, "${root}", e.RootPath)
		t.Logf("venvPath = %s (IsAbs = %v)", venvPath, filepath.IsAbs(venvPath))

		e.ImportDirectory(dir)

		// replace "virtualenv: venv" with "virtualenv: ${venvPath}" in Pulumi.yaml
		pulumiYaml := filepath.Join(e.RootPath, "Pulumi.yaml")

		oldYaml, err := os.ReadFile(pulumiYaml)
		if err != nil {
			t.Error(err)
			return
		}
		newYaml := []byte(strings.ReplaceAll(string(oldYaml),
			"virtualenv: venv",
			fmt.Sprintf("virtualenv: >-\n      %s", venvPath)))

		if err := os.WriteFile(pulumiYaml, newYaml, 0o600); err != nil {
			t.Error(err)
			return
		}

		t.Logf("Wrote Pulumi.yaml:\n%s\n", string(newYaml))

		// Make a subdir and change to it to ensure paths aren't just relative to the working directory.
		subdir := filepath.Join(e.RootPath, "subdir")
		err = os.Mkdir(subdir, 0o755)
		require.NoError(t, err)
		e.CWD = subdir

		e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
		e.RunCommand("pulumi", "stack", "init", "teststack")
		e.RunCommand("pulumi", "preview")

		var absVenvPath string
		if filepath.IsAbs(venvPath) {
			absVenvPath = venvPath
		} else {
			absVenvPath = filepath.Join(e.RootPath, venvPath)
		}

		if !python.IsVirtualEnv(absVenvPath) {
			t.Errorf("Expected a virtual environment to be created at %s but it is not there",
				absVenvPath)
		}
	}

	t.Run("RelativePath", func(t *testing.T) {
		t.Parallel()
		check(t, "venv", filepath.Join("python", "venv"))
	})

	t.Run("AbsolutePath", func(t *testing.T) {
		t.Parallel()
		check(t, filepath.Join("${root}", "absvenv"), filepath.Join("python", "venv"))
	})

	t.Run("RelativePathWithMain", func(t *testing.T) {
		t.Parallel()
		check(t, "venv", filepath.Join("python", "venv-with-main"))
	})

	t.Run("AbsolutePathWithMain", func(t *testing.T) {
		t.Parallel()
		check(t, filepath.Join("${root}", "absvenv"), filepath.Join("python", "venv-with-main"))
	})

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

		e := ptesting.NewEnvironment(t)
		defer func() {
			if !t.Failed() {
				e.DeleteEnvironment()
			}
		}()

		dir := filepath.Join("python", "venv")
		e.ImportDirectory(dir)

		e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())
		e.RunCommand("pulumi", "stack", "init", "teststack")
		stdout, stderr, _ := e.GetCommandResults("pulumi", "preview")
		// pulumi/pulumi#9175
		// Ensures this error message doesn't show up for uninitialized
		// virtualenv
		//     `Failed to resolve python version command: ` +
		//     `fork/exec <path>/venv/bin/python: ` +
		//     `no such file or directory`
		assert.NotContains(t, stdout, "fork/exec")
		assert.NotContains(t, stderr, "fork/exec")
	})
}