# Note that in this test we use "! bincmp" on plaintext output files,
# as a workaround for "cmp" not supporting "! cmp".
# TODO: now that obfuscation with -seed is deterministic,
# can we just rely on the regular "cmp" with fixed output files?

# TODO: consider setting these seeds globally,
# so we can reuse them across tests and make better use of the shared build cache.
env SEED1=OQg9kACEECQ
env SEED2=NruiDmVz6/s

# Check the binary with a given base64 encoded seed.
exec garble -seed=${SEED1} build
exec ./main$exe
cmp stderr main.stderr
binsubstr main$exe 'teststring' 'imported var value'
! binsubstr main$exe 'ImportedVar' ${SEED1}

[short] stop # the extra checks are relatively expensive

exec ./main$exe test/main/imported
cp stderr importedpkg-seed-static-1

# Also check that the binary is reproducible.
# No packages should be rebuilt either, thanks to the build cache.
cp main$exe main_seed1$exe
rm main$exe
exec garble -seed=${SEED1}= build -v
! stderr .
bincmp main$exe main_seed1$exe

exec ./main$exe test/main/imported
cmp stderr importedpkg-seed-static-1

# Even if we use the same seed, the same names in a different package
# should still be obfuscated in a different way.
exec ./main$exe test/main
cp stderr mainpkg-seed-static-1
! bincmp mainpkg-seed-static-1 importedpkg-seed-static-1

# Using different flags which affect the build, such as -literals or -tiny,
# should result in the same obfuscation as long as the seed is constant.
# TODO: also test that changing non-garble build parameters,
# such as GOARCH or -tags, still results in the same hashing via the seed.

exec garble -seed=${SEED1} -literals build
exec ./main$exe test/main/imported
cmp stderr importedpkg-seed-static-1

exec garble -seed=${SEED1} -tiny build
exec ./main$exe test/main/imported
cmp stderr importedpkg-seed-static-1

# Also check that a different seed leads to a different binary.
# We can't know if caching happens here, because of previous test runs.
cp main$exe main_seed2$exe
rm main$exe
exec garble -seed=${SEED2} build
! bincmp main$exe main_seed2$exe

exec ./main$exe test/main/imported
cp stderr importedpkg-seed-static-2
! bincmp importedpkg-seed-static-2 importedpkg-seed-static-1

# Use a random seed, which should always trigger a full build.
exec garble -seed=random build -v
stderr -count=1 '^-seed chosen at random: .+'
stderr -count=1 '^runtime$'
stderr -count=1 '^test/main$'
exec ./main$exe
cmp stderr main.stderr
binsubstr main$exe 'teststring' 'imported var value'
! binsubstr main$exe 'ImportedVar'

exec ./main$exe test/main/imported
cp stderr importedpkg-seed-random-1
! bincmp importedpkg-seed-random-1 importedpkg-seed-static-1

# Also check that the random binary is not reproducible.
cp main$exe main_random$exe
rm main$exe
exec garble -seed=random build -v
stderr .
! bincmp main$exe main_random$exe

exec ./main$exe test/main/imported
cp stderr importedpkg-seed-random-2
! bincmp importedpkg-seed-random-2 importedpkg-seed-random-1

# Finally, ensure that our runtime and reflect test code does what we think.
go build
exec ./main$exe
cmp stderr main.stderr
exec ./main$exe test/main
cmp stderr mainpkg.stderr
exec ./main$exe test/main/imported
cmp stderr importedpkg.stderr
-- go.mod --
module test/main

go 1.23
-- main.go --
package main

import (
	"os"

	"test/main/imported"
)

var teststringVar = "teststring"

func main() { mainFunc() }

func mainFunc() {
	if len(os.Args) > 1 {
		switch os.Args[1] {
		case "test/main":
			imported.PrintNames(NamedTypeValue, NamedFunc)
		case "test/main/imported":
			imported.PrintNames(imported.NamedType{}, imported.NamedFunc)
		default:
			panic("unknown package")
		}
	} else {
		println(teststringVar)
		println(imported.ImportedVar)

		// When we're obfuscating, check that the obfuscated name lengths vary.
		// With sixteen hashed names, and a range of 7 (between 6 and 12),
		// the chances of fourteen repeats are incredibly minuscule.
		// If that happens, then our randomness is clearly broken.
		if hashedNames[0] != "main.hashed_00" {
			var count [16]int
			for _, name := range hashedNames {
				name = name[len("main."):]
				if len(name) < 6 {
					panic("ended up with a hashed name that's too short: "+name)
				}
				if len(name) > 12 {
					panic("ended up with a hashed name that's too long: "+name)
				}
				count[len(name)]++
				if count[len(name)] >= 14 {
					for _, name := range hashedNames {
						println(name)
					}
					panic("six or more hashed names with the same length")
				}
			}
		}
	}
}

// A workaround to fool garble's reflect detection,
// because we want it to show us the obfuscated NamedType.
var NamedTypeValue any = NamedType{}

type NamedType struct {
	NamedField int
}

func NamedFunc() string {
	return imported.CallerFuncName()
}

var hashedNames = []string{
	hashed_00(), hashed_01(), hashed_02(), hashed_03(),
	hashed_04(), hashed_05(), hashed_06(), hashed_07(),
	hashed_08(), hashed_09(), hashed_10(), hashed_11(),
	hashed_12(), hashed_13(), hashed_14(), hashed_15(),
}

func hashed_00() string { return imported.CallerFuncName() }
func hashed_01() string { return imported.CallerFuncName() }
func hashed_02() string { return imported.CallerFuncName() }
func hashed_03() string { return imported.CallerFuncName() }
func hashed_04() string { return imported.CallerFuncName() }
func hashed_05() string { return imported.CallerFuncName() }
func hashed_06() string { return imported.CallerFuncName() }
func hashed_07() string { return imported.CallerFuncName() }
func hashed_08() string { return imported.CallerFuncName() }
func hashed_09() string { return imported.CallerFuncName() }
func hashed_10() string { return imported.CallerFuncName() }
func hashed_11() string { return imported.CallerFuncName() }
func hashed_12() string { return imported.CallerFuncName() }
func hashed_13() string { return imported.CallerFuncName() }
func hashed_14() string { return imported.CallerFuncName() }
func hashed_15() string { return imported.CallerFuncName() }

-- imported/imported.go --
package imported

import (
	"reflect"
	"runtime"
)

var ImportedVar = "imported var value"

type NamedType struct {
	NamedField int
}

func NamedFunc() string {
	return CallerFuncName()
}

func PrintNames(v any, fn func() string) {
	typ := reflect.TypeOf(v)
	println("path:", typ.PkgPath())
	println("type:", typ.Name())
	println("field:", typ.Field(0).Name)
	println("func: ", fn())
}

func CallerFuncName() string {
	pc, _, _, _ := runtime.Caller(1)
	fn := runtime.FuncForPC(pc)
	return fn.Name()
}
-- main.stderr --
teststring
imported var value
-- mainpkg.stderr --
path: main
type: NamedType
field: NamedField
func:  main.NamedFunc
-- importedpkg.stderr --
path: test/main/imported
type: NamedType
field: NamedField
func:  test/main/imported.NamedFunc