package main

import (
	"encoding/xml"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/jstemmer/go-junit-report/v2/pkg/junit"
	"github.com/jstemmer/go-junit-report/v2/pkg/parser/gotest"

	"github.com/google/go-cmp/cmp"
)

var matchTest = flag.String("match", "", "only test testdata matching this pattern")

type TestCase struct {
	name        string
	reportName  string
	noXMLHeader bool
	packageName string
}

var testCases = []TestCase{
	{
		name:       "01-pass.txt",
		reportName: "01-report.xml",
	},
	{
		name:       "02-fail.txt",
		reportName: "02-report.xml",
	},
	{
		name:       "03-skip.txt",
		reportName: "03-report.xml",
	},
	{
		name:       "04-go_1_4.txt",
		reportName: "04-report.xml",
	},
	{
		name:        "05-no_xml_header.txt",
		reportName:  "05-report.xml",
		noXMLHeader: true,
	},
	{
		name:        "06-mixed.txt",
		reportName:  "06-report.xml",
		noXMLHeader: true,
	},
	{
		name:        "07-compiled_test.txt",
		reportName:  "07-report.xml",
		packageName: "test/package",
	},
	{
		name:       "08-parallel.txt",
		reportName: "08-report.xml",
	},
	{
		name:       "09-coverage.txt",
		reportName: "09-report.xml",
	},
	{
		name:       "10-multipkg-coverage.txt",
		reportName: "10-report.xml",
	},
	{
		name:       "11-go_1_5.txt",
		reportName: "11-report.xml",
	},
	{
		name:       "12-go_1_7.txt",
		reportName: "12-report.xml",
	},
	{
		name:       "13-syntax-error.txt",
		reportName: "13-report.xml",
	},
	{
		name:       "14-panic.txt",
		reportName: "14-report.xml",
	},
	{
		name:       "15-empty.txt",
		reportName: "15-report.xml",
	},
	{
		name:       "16-repeated-names.txt",
		reportName: "16-report.xml",
	},
	{
		name:       "17-race.txt",
		reportName: "17-report.xml",
	},
	{
		name:       "18-coverpkg.txt",
		reportName: "18-report.xml",
	},
	{
		name:       "19-pass.txt",
		reportName: "19-report.xml",
	},
	{
		name:       "20-parallel.txt",
		reportName: "20-report.xml",
	},
	{
		name:       "21-cached.txt",
		reportName: "21-report.xml",
	},
	{
		name:       "22-bench.txt",
		reportName: "22-report.xml",
	},
	{
		name:       "23-benchmem.txt",
		reportName: "23-report.xml",
	},
	{
		name:       "24-benchtests.txt",
		reportName: "24-report.xml",
	},
	{
		name:       "25-benchcount.txt",
		reportName: "25-report.xml",
	},
	{
		name:       "26-testbenchmultiple.txt",
		reportName: "26-report.xml",
	},
	{
		name:       "27-benchdecimal.txt",
		reportName: "27-report.xml",
	},
	{
		name:       "28-bench-1cpu.txt",
		reportName: "28-report.xml",
	},
	{
		name:       "29-bench-16cpu.txt",
		reportName: "29-report.xml",
	},
	{
		// generated by running go test on https://gist.github.com/liggitt/09a021ccec988b19917e0c2d60a18ee9
		name:       "30-stdout.txt",
		reportName: "30-report.xml",
	},
	{
		name:       "31-syntax-error-test-binary.txt",
		reportName: "31-report.xml",
	},
	{
		name:       "32-failed-summary.txt",
		reportName: "32-report.xml",
	},
	{
		name:       "33-bench-mb.txt",
		reportName: "33-report.xml",
	},
	{
		name:       "34-notest.txt",
		reportName: "34-report.xml",
	},
}

func TestNewOutput(t *testing.T) {
	matchRegex := compileMatch(t)
	for _, testCase := range testCases {
		if !matchRegex.MatchString(testCase.name) {
			continue
		}

		t.Run(testCase.name, func(t *testing.T) {
			testReport(testCase.name, testCase.reportName, testCase.packageName, t)
		})
	}
}

func testReport(input, reportFile, packageName string, t *testing.T) {
	file, err := os.Open("testdata/" + input)
	if err != nil {
		t.Fatal(err)
	}
	defer file.Close()

	parser := gotest.New(
		gotest.PackageName(packageName),
		gotest.TimestampFunc(func() time.Time {
			return time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
		}),
	)

	report, err := parser.Parse(file)
	if err != nil {
		t.Fatal(err)
	}

	if *printEvents {
		for _, event := range parser.Events() {
			t.Logf("Event: %+v", event)
		}
	}

	actual := junit.CreateFromReport(report, "hostname")

	expectedXML, err := loadTestReport(reportFile, "")
	if err != nil {
		t.Fatal(err)
	}

	var expected junit.Testsuites
	if err := xml.Unmarshal([]byte(expectedXML), &expected); err != nil {
		t.Fatal(err)
	}

	expected = modifyForBackwardsCompat(expected)
	if diff := cmp.Diff(actual, expected); diff != "" {
		t.Errorf("Unexpected report diff (-got, +want):\n%v", diff)
	}
}

func modifyForBackwardsCompat(testsuites junit.Testsuites) junit.Testsuites {
	testsuites.XMLName.Local = ""
	for i, suite := range testsuites.Suites {
		for j := range suite.Testcases {
			testsuites.Suites[i].Testcases[j].Classname = suite.Name
		}

		if suite.Properties != nil {
			if covIdx, covProp := getProperty("coverage.statements.pct", *suite.Properties); covIdx > -1 {
				pct, _ := strconv.ParseFloat(covProp.Value, 64)
				(*testsuites.Suites[i].Properties)[covIdx].Value = fmt.Sprintf("%.2f", pct)
			}
			testsuites.Suites[i].Properties = dropProperty("go.version", suite.Properties)
		}
	}
	return testsuites
}

func dropProperty(name string, properties *[]junit.Property) *[]junit.Property {
	if properties == nil {
		return nil
	}
	var props []junit.Property
	for _, prop := range *properties {
		if prop.Name != name {
			props = append(props, prop)
		}
	}
	if len(props) == 0 {
		return nil
	}
	return &props
}

func getProperty(name string, properties []junit.Property) (int, junit.Property) {
	for i, prop := range properties {
		if prop.Name == name {
			return i, prop
		}
	}
	return -1, junit.Property{}
}

func loadTestReport(name, goVersion string) (string, error) {
	contents, err := ioutil.ReadFile("testdata/" + name)
	if err != nil {
		return "", err
	}

	if goVersion == "" {
		// if goVersion is not specified, default to runtime version
		goVersion = runtime.Version()
	}

	// replace value="1.0" With actual version
	report := strings.Replace(string(contents), `value="1.0"`, fmt.Sprintf(`value="%s"`, goVersion), -1)

	return report, nil
}

func compileMatch(t *testing.T) *regexp.Regexp {
	rx, err := regexp.Compile(*matchTest)
	if err != nil {
		t.Fatalf("Error compiling -match flag %q: %v", *matchTest, err)
	}
	return rx
}