package dotnet

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

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

	"github.com/pulumi/pulumi/pkg/v3/codegen"
	"github.com/pulumi/pulumi/pkg/v3/codegen/testing/test"
	"github.com/pulumi/pulumi/pkg/v3/testing/integration"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/executable"
)

func Check(t *testing.T, path string, dependencies codegen.StringSet, pulumiSDKPath string) {
	var err error
	dir := filepath.Dir(path)

	ex, err := executable.FindExecutable("dotnet")
	require.NoError(t, err, "Failed to find dotnet executable")

	// We create a new cs-project each time the test is run.
	projectFile := filepath.Join(dir, filepath.Base(dir)+".csproj")
	programFile := filepath.Join(dir, "Program.cs")
	if err = os.Remove(projectFile); !os.IsNotExist(err) {
		require.NoError(t, err)
	}
	if err = os.Remove(programFile); !os.IsNotExist(err) {
		require.NoError(t, err)
	}
	err = integration.RunCommand(t, "create dotnet project",
		[]string{ex, "new", "console", "-f", "net6.0"}, dir, &integration.ProgramTestOptions{})
	require.NoError(t, err, "Failed to create C# project")

	// Remove Program.cs again generated from "dotnet new console"
	// because the generated C# program already has an entry point
	if err = os.Remove(programFile); !os.IsNotExist(err) {
		require.NoError(t, err)
	}

	// Add dependencies
	pkgs := dotnetDependencies(dependencies)
	if len(pkgs) != 0 {
		for _, pkg := range pkgs {
			pkg.install(t, ex, dir)
		}
		dep{"Pulumi", test.PulumiDotnetSDKVersion}.install(t, ex, dir)
	} else {
		// We would like this regardless of other dependencies, but dotnet
		// packages do not play well with package references.
		if pulumiSDKPath != "" {
			err = integration.RunCommand(t, "add sdk ref",
				[]string{ex, "add", "reference", pulumiSDKPath},
				dir, &integration.ProgramTestOptions{})
			require.NoError(t, err, "Failed to dotnet sdk package reference")
		} else {
			dep{"Pulumi", test.PulumiDotnetSDKVersion}.install(t, ex, dir)
		}
	}

	// Clean up build result
	defer func() {
		err = os.RemoveAll(filepath.Join(dir, "bin"))
		assert.NoError(t, err, "Failed to remove bin result")
		err = os.RemoveAll(filepath.Join(dir, "obj"))
		assert.NoError(t, err, "Failed to remove obj result")
	}()
	TypeCheck(t, path, dependencies, pulumiSDKPath)
}

func TypeCheck(t *testing.T, path string, dependencies codegen.StringSet, pulumiSDKPath string) {
	var err error
	dir := filepath.Dir(path)

	ex, err := executable.FindExecutable("dotnet")
	require.NoError(t, err)

	err = integration.RunCommand(t, "dotnet build",
		[]string{ex, "build", "--nologo"}, dir, &integration.ProgramTestOptions{})
	require.NoError(t, err, "Failed to build dotnet project")
}

type dep struct {
	Name    string
	Version string
}

func (pkg dep) install(t *testing.T, ex, dir string) {
	args := []string{ex, "add", "package", pkg.Name}
	if pkg.Version != "" {
		args = append(args, "--version", pkg.Version)
	}
	err := integration.RunCommand(t, "Add package",
		args, dir, &integration.ProgramTestOptions{})
	require.NoError(t, err, "Failed to add dependency %q %q", pkg.Name, pkg.Version)
}

// Converts from the hcl2 dependency format to the dotnet format.
//
// Example:
//
//	"aws" => {"Pulumi.Aws", 4.21.1}
//	"azure" => {"Pulumi.Azure", 4.21.1}
func dotnetDependencies(deps codegen.StringSet) []dep {
	result := make([]dep, len(deps))
	for i, d := range deps.SortedValues() {
		switch d {
		case "aws":
			result[i] = dep{"Pulumi.Aws", test.AwsSchema}
		case "azure-native":
			result[i] = dep{"Pulumi.AzureNative", test.AzureNativeSchema}
		case "azure":
			// TODO: update constant in test.AzureSchema to v5.x
			// because it has output-versioned function invokes
			result[i] = dep{"Pulumi.Azure", "5.12.0"}
		case "kubernetes":
			result[i] = dep{"Pulumi.Kubernetes", test.KubernetesSchema}
		case "random":
			result[i] = dep{"Pulumi.Random", test.RandomSchema}
		case "aws-static-website":
			result[i] = dep{"Pulumi.AwsStaticWebsite", test.AwsStaticWebsiteSchema}
		case "aws-native":
			result[i] = dep{"Pulumi.AwsNative", test.AwsNativeSchema}
		default:
			result[i] = dep{"Pulumi." + Title(d), ""}
		}
	}
	return result
}

func GenerateProgramBatchTest(t *testing.T, testCases []test.ProgramTest) {
	test.TestProgramCodegen(t,
		test.ProgramCodegenOptions{
			Language:   "dotnet",
			Extension:  "cs",
			OutputFile: "Program.cs",
			Check: func(t *testing.T, path string, dependencies codegen.StringSet) {
				Check(t, path, dependencies, "")
			},
			GenProgram: GenerateProgram,
			TestCases:  testCases,
		},
	)
}