package compile

import (
	"os"
	"path/filepath"
	stdslices "slices"
	"strconv"
	"testing"

	"github.com/alecthomas/assert/v2"
	"github.com/alecthomas/types/optional"

	"github.com/block/ftl/common/builderrors"
	"github.com/block/ftl/common/schema"
	"github.com/block/ftl/common/slices"
	"github.com/block/ftl/common/strcase"
	goschema "github.com/block/ftl/go-runtime/schema"
	"github.com/block/ftl/go-runtime/schema/common"
	"github.com/block/ftl/internal/watch"
)

func TestImportAliases(t *testing.T) {
	actual, err := schema.ParseModuleString("", `
	module typealias {
		typealias FooBar1 Any
		+typemap go "github.com/one1/foo/bar/package.Type"

		typealias FooBar2 Any
		+typemap go "github.com/two2/foo/bar/package.Type"

		typealias Unique Any
		+typemap go "github.com/two2/foo/bar/unique.Type"

		typealias UniqueDir Any
		+typemap go "github.com/some/pkg.uniquedir.Type"

		typealias NonUniqueDir Any
		+typemap go "github.com/example/path/to/pkg.last.Type"

		typealias ConflictsWithDir Any
		+typemap go "github.com/last.Type"

		// import aliases can't have a number as the first character
		typealias StartsWithANumber1 Any
		+typemap go "github.com/11/numeric.Type"

		typealias StartsWithANumber2 Any
		+typemap go "github.com/22/numeric.Type"

		// two different directories with the same import path, first one wins
		typealias SamePackageDiffDir1 Any
		+typemap go "github.com/same.dir1.Type"

		typealias SamePackageDiffDir2 Any
		+typemap go "github.com/same.dir2.Type"

		// two aliases that are part of the same external package
		typealias TwoAliasesWithOnePkg1 Any
		+typemap go "github.com/two/aliaseswithonepkg.Type1"

		typealias TwoAliasesWithOnePkg2 Any
		+typemap go "github.com/two/aliaseswithonepkg.Type2"

		// references ftl/moduleclash, which is also the name of an external library
		export data ExampleData {
			something moduleclash.ExampleType
		}

		typealias ClashesWithModuleImport Any
		+typemap go "github.com/ftlmoduleclash.Type2"
	}
	`)
	assert.NoError(t, err)
	imports := imports(actual, false)
	assert.Equal(t, map[string]string{
		"github.com/one1/foo/bar/package":  "one1_foo_bar_package",
		"github.com/two2/foo/bar/package":  "two2_foo_bar_package",
		"github.com/two2/foo/bar/unique":   "unique",
		"github.com/some/pkg":              "uniquedir",
		"github.com/example/path/to/pkg":   "pkg_last",
		"github.com/last":                  "github_com_last",
		"github.com/11/numeric":            "_11_numeric",
		"github.com/22/numeric":            "_22_numeric",
		"github.com/same":                  "dir1",
		"github.com/two/aliaseswithonepkg": "aliaseswithonepkg",
		"ftl/moduleclash":                  "ftlmoduleclash",
		"github.com/ftlmoduleclash":        "github_com_ftlmoduleclash",
	}, imports)
}

func TestUpdateGoModuleValidatesModuleName(t *testing.T) {
	dir := t.TempDir()
	goModPath := filepath.Join(dir, "go.mod")

	// Matching module name
	err := os.WriteFile(goModPath, []byte(`module ftl/mymodule

go 1.24.0
`), 0600)
	assert.NoError(t, err)

	_, _, err = updateGoModule(goModPath, "mymodule", optional.None[watch.ModifyFilesTransaction]())
	assert.NoError(t, err)

	// Mismatched module name
	err = os.WriteFile(goModPath, []byte(`module ftl/wrongname

go 1.24.0
`), 0600)
	assert.NoError(t, err)

	_, _, err = updateGoModule(goModPath, "mymodule", optional.None[watch.ModifyFilesTransaction]())
	assert.Contains(t, err.Error(), "module name mismatch: expected 'ftl/mymodule' but got 'ftl/wrongname'")
}

func TestCylicVerbs(t *testing.T) {
	module, err := schema.ParseModuleString("", `
module cyclic {
  verb a(Unit) Unit
    +calls cyclic.b

  verb b(Unit) Unit
    +calls cyclic.c

  verb c(Unit) Unit
    +calls cyclic.a
}
	`)
	assert.NoError(t, err)
	verbDecls := stdslices.Collect(slices.FilterVariants[*schema.Verb](module.Decls))
	_, err = buildMainDeploymentContext(&schema.Schema{Realms: []*schema.Realm{{Name: "foo"}}}, goschema.Result{
		Module: module,
		VerbResourceParamOrder: map[*schema.Verb][]common.VerbResourceParam{
			verbDecls[0]: {},
			verbDecls[1]: {},
			verbDecls[2]: {},
		},
		NativeNames: goschema.NativeNames{
			verbDecls[0]: "ftl/cyclic." + strcase.ToUpperCamel(module.Decls[0].GetName()),
			verbDecls[1]: "ftl/cyclic." + strcase.ToUpperCamel(module.Decls[1].GetName()),
			verbDecls[2]: "ftl/cyclic." + strcase.ToUpperCamel(module.Decls[2].GetName()),
		},
	},
		"", "projectname", nil, nil)

	assert.EqualError(t, err, "cyclic references are not allowed: cyclic.a refers to cyclic.b refers to cyclic.c refers to cyclic.a")
}

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

	for i, tt := range []struct {
		input    string
		expected []builderrors.Error
	}{
		{
			input: "random text which is just an error",
			expected: []builderrors.Error{
				{
					Msg: "random text which is just an error",
				},
			},
		},
		{
			input: `
# ftl/time
../../../time.go:30:6: Internal redeclared in this block
	../../../time.go:25:6: other declaration of Internal
../../../../../../example.go:1:2: An error I made up which refers to ../../../../../../another.go and another/../../../path.go
`,
			expected: []builderrors.Error{
				{
					Msg: "Internal redeclared in this block: time.go:25:6: other declaration of Internal",
					Pos: optional.Some(builderrors.Position{
						Filename:    "time.go",
						Line:        30,
						StartColumn: 6,
						EndColumn:   6,
					}),
				},
				{
					Msg: "An error I made up which refers to ../../../another.go and another/../../../path.go",
					Pos: optional.Some(builderrors.Position{
						Filename:    "../../../example.go",
						Line:        1,
						StartColumn: 2,
						EndColumn:   2,
					}),
				},
			},
		},
	} {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			t.Parallel()
			// Add standard params to expected errs for convenience
			expected := slices.Map(tt.expected, func(e builderrors.Error) builderrors.Error {
				e.Type = builderrors.COMPILER
				e.Level = builderrors.ERROR
				return e
			})
			actual := buildErrsFromCompilerErr(tt.input)
			assert.Equal(t, expected, actual)
		})
	}
}
