//nolint:lll
package nodejs

import (
	"bytes"
	"encoding/json"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
	"github.com/pulumi/pulumi/pkg/v3/codegen/testing/test"
)

// For better CI test to job distribution, we split the test cases into three tests.

var genPkgBatchSize = len(test.PulumiPulumiSDKTests) / 3

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

	testGeneratePackageBatch(t, test.PulumiPulumiSDKTests[0:genPkgBatchSize])
}

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

	testGeneratePackageBatch(t, test.PulumiPulumiSDKTests[genPkgBatchSize:2*genPkgBatchSize])
}

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

	testGeneratePackageBatch(t, test.PulumiPulumiSDKTests[2*genPkgBatchSize:])
}

func testGeneratePackageBatch(t *testing.T, testCases []*test.SDKTest) {
	test.TestSDKCodegen(t, &test.SDKCodegenOptions{
		Language:   "nodejs",
		GenPackage: GeneratePackage,
		Checks: map[string]test.CodegenCheck{
			"nodejs/compile": func(t *testing.T, pwd string) {
				typeCheckGeneratedPackage(t, pwd, true)
			},
			"nodejs/test": testGeneratedPackage,
		},
		TestCases: testCases,
	})
}

// Runs unit tests against the generated code.
func testGeneratedPackage(t *testing.T, pwd string) {
	// Some tests have do not have mocha as a dependency.
	hasMocha := false
	for _, c := range getYarnCommands(t, pwd) {
		if c == "mocha" {
			hasMocha = true
			break
		}
	}

	// We are attempting to ensure that we don't write tests that are not run. The `nodejs-extras`
	// folder exists to mixin tests of the form `*.spec.ts`. We assume that if this folder is
	// present and contains `*.spec.ts` files, we want to run those tests.
	foundTests := false
	findTests := func(path string, _ os.DirEntry, _ error) error {
		if strings.HasSuffix(path, ".spec.ts") {
			foundTests = true
		}
		return nil
	}
	mixinFolder := filepath.Join(filepath.Dir(pwd), "nodejs-extras")
	if err := filepath.WalkDir(mixinFolder, findTests); !hasMocha && !os.IsNotExist(err) && foundTests {
		t.Errorf("%s has at least one nodejs-extras/**/*.spec.ts file , but does not have mocha as a dependency."+
			" Tests were not run. Please add mocha as a dependency in the schema or remove the *.spec.ts files.",
			pwd)
	}

	if hasMocha {
		// If mocha is a dev dependency but no test files exist, this will fail.
		test.RunCommand(t, "mocha", pwd,
			"yarn", "run", "mocha",
			"--require", "ts-node/register",
			"tests/**/*.spec.ts")
	} else {
		t.Logf("No mocha tests found for %s", pwd)
	}
}

// Get the commands runnable with yarn run
func getYarnCommands(t *testing.T, pwd string) []string {
	cmd := exec.Command("yarn", "run", "--json")
	cmd.Dir = pwd
	out, err := cmd.Output()
	if err != nil {
		t.Errorf("Got error determining valid commands: %s", err)
	}
	dec := json.NewDecoder(bytes.NewReader(out))
	parsed := []map[string]interface{}{}
	for {
		var m map[string]interface{}
		if err := dec.Decode(&m); err != nil {
			if err == io.EOF {
				break
			}
			t.FailNow()
		}
		parsed = append(parsed, m)
	}
	var cmds []string

	addProvidedCmds := func(c map[string]interface{}) {
		// If this fails, we want the test to fail. We don't want to accidentally skip tests.
		data := c["data"].(map[string]interface{})
		if data["type"] == "possibleCommands" {
			return
		}
		for _, cmd := range data["items"].([]interface{}) {
			cmds = append(cmds, cmd.(string))
		}
	}

	addBinaryCmds := func(c map[string]interface{}) {
		data := c["data"].(string)
		if !strings.HasPrefix(data, "Commands available from binary scripts:") {
			return
		}
		cmdList := data[strings.Index(data, ":")+1:]
		for _, cmd := range strings.Split(cmdList, ",") {
			cmds = append(cmds, strings.TrimSpace(cmd))
		}
	}

	for _, c := range parsed {
		switch c["type"] {
		case "list":
			addProvidedCmds(c)
		case "info":
			addBinaryCmds(c)
		}
	}
	t.Logf("Found yarn commands in %s: %v", pwd, cmds)
	return cmds
}

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

	test.TestTypeNameCodegen(t, "nodejs", func(pkg *schema.Package) test.TypeNameGeneratorFunc {
		modules, info, err := generateModuleContextMap("test", pkg, nil)
		require.NoError(t, err)

		pkg.Language["nodejs"] = info

		root, ok := modules[""]
		require.True(t, ok)

		// Parallel tests will use the TypeNameGeneratorFunc
		// from multiple goroutines, but root.typeString is
		// not safe. Mutex is needed to avoid panics on
		// concurrent map write.
		//
		// Note this problem is test-only since prod code
		// works on a single goroutine.

		var mutex sync.Mutex
		return func(t schema.Type) string {
			mutex.Lock()
			defer mutex.Unlock()
			return root.typeString(t, false, nil)
		}
	})
}

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

	tests := []struct {
		input    string
		expected string
	}{
		{
			input:    "hi",
			expected: "Hi",
		},
		{
			input:    "NothingChanges",
			expected: "NothingChanges",
		},
		{
			input:    "everything-changed",
			expected: "EverythingChanged",
		},
	}
	for _, tt := range tests {
		result := pascal(tt.input)
		require.Equal(t, tt.expected, result)
	}
}

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

	tests := []struct {
		name     string
		input    schema.Type
		expected bool
	}{
		{"string", schema.StringType, true},
		{"int", schema.IntType, false},
		{"Input[string]", &schema.InputType{ElementType: schema.StringType}, true},
		{"Input[int]", &schema.InputType{ElementType: schema.IntType}, false},
		{"StrictStringEnum", &schema.EnumType{ElementType: schema.StringType}, true},
		{"StrictIntEnum", &schema.EnumType{ElementType: schema.IntType}, false},
		{"RelaxedStringEnum", &schema.UnionType{
			ElementTypes: []schema.Type{&schema.EnumType{ElementType: schema.StringType}, schema.StringType},
		}, true},
		{"RelaxedIntEnum", &schema.UnionType{
			ElementTypes: []schema.Type{&schema.EnumType{ElementType: schema.IntType}, schema.IntType},
		}, false},
	}
	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			if got := isStringType(tt.input); got != tt.expected {
				t.Errorf("isStringType() = %v, want %v", got, tt.expected)
			}
		})
	}
}