package symbolyze

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"sync"
	"testing"
)

// buildSimple calls make in the testdata-directory in order to build the
// test-binary `simple` that is linked against python3.7.  If this fails, make
// necessary adjustments to the Makefile and/or your environment and try again.
func buildSimple() error {
	cmd := exec.Command("make", "-s")
	cmd.Dir = "testdata"
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	return cmd.Run()
}

// TestSimpeGDB runs the program testdata/simple multiple times and uses gdb to
// extract the address of the symbol _PyRuntime for each pid.  It compares
// those with the results from Scanner.Run().
func TestSimpleGDB(t *testing.T) {
	ourResults := map[int]uint64{}
	gdbResults := map[int]uint64{}

	err := buildSimple()
	if err != nil {
		t.Fatal(err)
	}

	scanner := NewScanner("_PyRuntime", "*python3*")
	var m sync.Mutex
	scanner.OnFound(func(pid int, offset uint64) error {
		t.Logf("scanner setting pid %d to offset %x", pid, offset)
		m.Lock()
		ourResults[pid] = offset
		m.Unlock()
		return nil
	})

	for i := 0; i < 5; i++ {
		simple := exec.Command("testdata/simple")
		if err := simple.Start(); err != nil {
			t.Fatal(err)
		}

		defer func() {
			simple.Process.Kill()
			simple.Wait()
		}()

		t.Logf("Asking Gdb about PID %d", simple.Process.Pid)

		offset, err := extractOffsetWithGdb(simple.Process.Pid, t)
		if err != nil {
			t.Fatalf("extractOffsetWithGdb: %v", err)
		}

		t.Logf("Found offset at 0x%x", offset)
		gdbResults[simple.Process.Pid] = offset
	}

	if err = scanner.Run(); err != nil {
		t.Logf("Scanner failure: %v", err)
	}

	for pid, off := range gdbResults {
		if off2, ok := ourResults[pid]; !ok {
			t.Fatalf("Scanner has no offset for pid %d", pid)
		} else if off != off2 {
			t.Fatalf("Scanner has found offset %d while gdb has found offset %d for pid %d", off2, off, pid)
		}
	}
}

// searchRX should match the output of gdb when it prints the address of the
// symbol
var searchRX = regexp.MustCompile(`Symbol "_PyRuntime" is static storage at address 0x([0-9a-fA-F]+)`)

// extractOffsetWithGdb calls gdb in batch-mode on a pid and looks up the
// address of the symbol _PyRuntime by calling `info address _PyRuntime`.  If
// found, it extracts the address from the gdb-output.
func extractOffsetWithGdb(pid int, t *testing.T) (offset uint64, err error) {
	cmd := exec.Command("gdb",
		"--batch",
		"-p", strconv.Itoa(pid),
		"-ex", "info address _PyRuntime",
		"-ex", "quit")

	output, err := cmd.StdoutPipe()
	if err != nil {
		return 0, err
	}

	if err := cmd.Start(); err != nil {
		return 0, err
	}

	defer cmd.Wait()

	scanner := bufio.NewScanner(output)
	for scanner.Scan() {
		line := scanner.Text()
		// t.Logf("[gdb]: %v", line)
		match := searchRX.FindStringSubmatch(line)
		if len(match) > 1 {
			return strconv.ParseUint(match[1], 16, 64)
		}
	}

	return 0, fmt.Errorf("Symbol not found with gdb.\n" +
		"Maybe /proc/sys/kernel/yama/ptrace_scope must be set to 0? Or try to run the test as root!")
}