// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package swig

import (
	"cmd/internal/quoted"
	"internal/testenv"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"testing"
)

func TestStdio(t *testing.T) {
	testenv.MustHaveCGO(t)
	mustHaveSwig(t)
	run(t, "testdata/stdio", false)
}

func TestCall(t *testing.T) {
	testenv.MustHaveCGO(t)
	mustHaveSwig(t)
	mustHaveCxx(t)
	run(t, "testdata/callback", false, "Call")
	t.Run("lto", func(t *testing.T) { run(t, "testdata/callback", true, "Call") })
}

func TestCallback(t *testing.T) {
	testenv.MustHaveCGO(t)
	mustHaveSwig(t)
	mustHaveCxx(t)
	run(t, "testdata/callback", false, "Callback")
	t.Run("lto", func(t *testing.T) { run(t, "testdata/callback", true, "Callback") })
}

func run(t *testing.T, dir string, lto bool, args ...string) {
	runArgs := append([]string{"run", "."}, args...)
	cmd := exec.Command("go", runArgs...)
	cmd.Dir = dir
	if lto {
		const cflags = "-flto -Wno-lto-type-mismatch -Wno-unknown-warning-option"
		cmd.Env = append(cmd.Environ(),
			"CGO_CFLAGS="+cflags,
			"CGO_CXXFLAGS="+cflags,
			"CGO_LDFLAGS="+cflags)
	}
	out, err := cmd.CombinedOutput()
	if string(out) != "OK\n" {
		t.Errorf("%s", string(out))
	}
	if err != nil {
		t.Errorf("%s", err)
	}
}

func mustHaveCxx(t *testing.T) {
	// Ask the go tool for the CXX it's configured to use.
	cxx, err := exec.Command("go", "env", "CXX").CombinedOutput()
	if err != nil {
		t.Fatalf("go env CXX failed: %s", err)
	}
	args, err := quoted.Split(string(cxx))
	if err != nil {
		t.Skipf("could not parse 'go env CXX' output %q: %s", string(cxx), err)
	}
	if len(args) == 0 {
		t.Skip("no C++ compiler")
	}
	testenv.MustHaveExecPath(t, string(args[0]))
}

var (
	swigOnce sync.Once
	haveSwig bool
)

func mustHaveSwig(t *testing.T) {
	swigOnce.Do(func() {
		mustHaveSwigOnce(t)
		haveSwig = true
	})
	// The first call will skip t with a nice message. On later calls, we just skip.
	if !haveSwig {
		t.Skip("swig not found")
	}
}

func mustHaveSwigOnce(t *testing.T) {
	swig, err := exec.LookPath("swig")
	if err != nil {
		t.Skipf("swig not in PATH: %s", err)
	}

	// Check that swig was installed with Go support by checking
	// that a go directory exists inside the swiglib directory.
	// See https://golang.org/issue/23469.
	output, err := exec.Command(swig, "-go", "-swiglib").Output()
	if err != nil {
		t.Skip("swig is missing Go support")
	}
	swigDir := strings.TrimSpace(string(output))

	_, err = os.Stat(filepath.Join(swigDir, "go"))
	if err != nil {
		t.Skip("swig is missing Go support")
	}

	// Check that swig has a new enough version.
	// See https://golang.org/issue/22858.
	out, err := exec.Command(swig, "-version").CombinedOutput()
	if err != nil {
		t.Skipf("failed to get swig version:%s\n%s", err, string(out))
	}

	re := regexp.MustCompile(`[vV]ersion +(\d+)([.]\d+)?([.]\d+)?`)
	matches := re.FindSubmatch(out)
	if matches == nil {
		// Can't find version number; hope for the best.
		t.Logf("failed to find swig version, continuing")
		return
	}

	var parseError error
	atoi := func(s string) int {
		x, err := strconv.Atoi(s)
		if err != nil && parseError == nil {
			parseError = err
		}
		return x
	}
	var major, minor, patch int
	major = atoi(string(matches[1]))
	if len(matches[2]) > 0 {
		minor = atoi(string(matches[2][1:]))
	}
	if len(matches[3]) > 0 {
		patch = atoi(string(matches[3][1:]))
	}
	if parseError != nil {
		t.Logf("error parsing swig version %q, continuing anyway: %s", string(matches[0]), parseError)
		return
	}
	t.Logf("found swig version %d.%d.%d", major, minor, patch)
	if major < 3 || (major == 3 && minor == 0 && patch < 6) {
		t.Skip("test requires swig 3.0.6 or later")
	}
}
