package main

import (
	"bytes"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/tinygo-org/tinygo/compileopts"
	"github.com/tinygo-org/tinygo/diagnostics"
)

// Test the error messages of the TinyGo compiler.
func TestErrors(t *testing.T) {
	// TODO: nicely formatted error messages for:
	//   - duplicate symbols in ld.lld (currently only prints bitcode file)
	type errorTest struct {
		name   string
		target string
	}
	for _, tc := range []errorTest{
		{name: "cgo"},
		{name: "compiler"},
		{name: "interp"},
		{name: "invalidmain"},
		{name: "invalidname"},
		{name: "linker-flashoverflow", target: "cortex-m-qemu"},
		{name: "linker-ramoverflow", target: "cortex-m-qemu"},
		{name: "linker-undefined", target: "darwin/arm64"},
		{name: "linker-undefined", target: "linux/amd64"},
		//{name: "linker-undefined", target: "windows/amd64"}, // TODO: no source location
		{name: "linker-undefined", target: "cortex-m-qemu"},
		//{name: "linker-undefined", target: "wasip1"}, // TODO: no source location
		{name: "loader-importcycle"},
		{name: "loader-invaliddep"},
		{name: "loader-invalidpackage"},
		{name: "loader-nopackage"},
		{name: "optimizer"},
		{name: "syntax"},
		{name: "types"},
	} {
		name := tc.name
		if tc.target != "" {
			name += "#" + tc.target
		}
		target := tc.target
		if target == "" {
			target = "wasip1"
		}
		t.Run(name, func(t *testing.T) {
			options := optionsFromTarget(target, sema)
			testErrorMessages(t, "./testdata/errors/"+tc.name+".go", &options)
		})
	}
}

func testErrorMessages(t *testing.T, filename string, options *compileopts.Options) {
	t.Parallel()

	// Parse expected error messages.
	expected := readErrorMessages(t, filename)

	// Try to build a binary (this should fail with an error).
	tmpdir := t.TempDir()
	err := Build(filename, tmpdir+"/out", options)
	if err == nil {
		t.Fatal("expected to get a compiler error")
	}

	// Get the full ./testdata/errors directory.
	wd, absErr := filepath.Abs("testdata/errors")
	if absErr != nil {
		t.Fatal(absErr)
	}

	// Write error message out as plain text.
	var buf bytes.Buffer
	diagnostics.CreateDiagnostics(err).WriteTo(&buf, wd)
	actual := strings.TrimRight(buf.String(), "\n")

	// Check whether the error is as expected.
	if !matchErrors(t, expected, actual) {
		t.Errorf("expected error:\n%s\ngot:\n%s", indentText(expected, "> "), indentText(actual, "> "))
	}
}

func matchErrors(t *testing.T, pattern, actual string) bool {
	patternLines := strings.Split(pattern, "\n")
	actualLines := strings.Split(actual, "\n")
	if len(patternLines) != len(actualLines) {
		return false
	}
	for i, patternLine := range patternLines {
		indices := regexp.MustCompile(`\{\{.*?\}\}`).FindAllStringIndex(patternLine, -1)
		patternParts := []string{"^"}
		lastStop := 0
		for _, startstop := range indices {
			start := startstop[0]
			stop := startstop[1]
			patternParts = append(patternParts,
				regexp.QuoteMeta(patternLine[lastStop:start]),
				patternLine[start+2:stop-2])
			lastStop = stop
		}
		patternParts = append(patternParts, regexp.QuoteMeta(patternLine[lastStop:]), "$")
		pattern := strings.Join(patternParts, "")
		re, err := regexp.Compile(pattern)
		if err != nil {
			t.Fatalf("could not compile regexp for %#v: %v", patternLine, err)
		}
		if !re.MatchString(actualLines[i]) {
			return false
		}
	}
	return true
}

// Indent the given text with a given indentation string.
func indentText(text, indent string) string {
	return indent + strings.ReplaceAll(text, "\n", "\n"+indent)
}

// Read "// ERROR:" prefixed messages from the given file.
func readErrorMessages(t *testing.T, file string) string {
	data, err := os.ReadFile(file)
	if err != nil {
		t.Fatal("could not read input file:", err)
	}

	var errors []string
	for _, line := range strings.Split(string(data), "\n") {
		if strings.HasPrefix(line, "// ERROR: ") {
			errors = append(errors, strings.TrimRight(line[len("// ERROR: "):], "\r\n"))
		}
	}
	return strings.Join(errors, "\n")
}
