package gotest

import (
	"fmt"
	"testing"
	"time"

	"github.com/jstemmer/go-junit-report/v2/gtr"
	"github.com/jstemmer/go-junit-report/v2/parser/gotest/internal/collector"

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

func TestReport(t *testing.T) {
	events := []Event{
		{Type: "run_test", Name: "TestOne"},
		{Type: "output", Data: "\tHello"},
		{Type: "end_test", Name: "TestOne", Result: "PASS", Duration: 1 * time.Millisecond},
		{Type: "status", Result: "PASS"},
		{Type: "run_test", Name: "TestSkip"},
		{Type: "end_test", Name: "TestSkip", Result: "SKIP", Duration: 1 * time.Millisecond},
		{Type: "summary", Result: "ok", Name: "package/name", Duration: 1 * time.Millisecond},
		{Type: "run_test", Name: "TestOne"},
		{Type: "output", Data: "\tfile_test.go:10: error"},
		{Type: "end_test", Name: "TestOne", Result: "FAIL", Duration: 1 * time.Millisecond},
		{Type: "status", Result: "FAIL"},
		{Type: "summary", Result: "FAIL", Name: "package/name2", Duration: 1 * time.Millisecond},
		{Type: "output", Data: "goarch: amd64"},
		{Type: "run_benchmark", Name: "BenchmarkOne"},
		{Type: "benchmark", Name: "BenchmarkOne", NsPerOp: 100},
		{Type: "end_benchmark", Name: "BenchmarkOne", Result: "BENCH"},
		{Type: "run_benchmark", Name: "BenchmarkTwo"},
		{Type: "benchmark", Name: "BenchmarkTwo"},
		{Type: "end_benchmark", Name: "BenchmarkTwo", Result: "FAIL"},
		{Type: "status", Result: "PASS"},
		{Type: "summary", Result: "ok", Name: "package/name3", Duration: 1234 * time.Millisecond},
		{Type: "build_output", Name: "package/failing1"},
		{Type: "output", Data: "error message"},
		{Type: "summary", Result: "FAIL", Name: "package/failing1", Data: "[build failed]"},
	}
	want := gtr.Report{
		Packages: []gtr.Package{
			{
				Name:      "package/name",
				Duration:  1 * time.Millisecond,
				Timestamp: testTimestamp,
				Tests: []gtr.Test{
					{
						ID:       1,
						Name:     "TestOne",
						Duration: 1 * time.Millisecond,
						Result:   gtr.Pass,
						Output: []string{
							"\tHello", // TODO: strip tabs?
						},
						Data: map[string]interface{}{},
					},
					{
						ID:       2,
						Name:     "TestSkip",
						Duration: 1 * time.Millisecond,
						Result:   gtr.Skip,
						Data:     map[string]interface{}{},
					},
				},
			},
			{
				Name:      "package/name2",
				Duration:  1 * time.Millisecond,
				Timestamp: testTimestamp,
				Tests: []gtr.Test{
					{
						ID:       3,
						Name:     "TestOne",
						Duration: 1 * time.Millisecond,
						Result:   gtr.Fail,
						Output: []string{
							"\tfile_test.go:10: error",
						},
						Data: map[string]interface{}{},
					},
				},
			},
			{
				Name:      "package/name3",
				Duration:  1234 * time.Millisecond,
				Timestamp: testTimestamp,
				Tests: []gtr.Test{
					{
						ID:     4,
						Name:   "BenchmarkOne",
						Result: gtr.Pass,
						Data:   map[string]interface{}{key: Benchmark{NsPerOp: 100}},
					},
					{
						ID:     5,
						Name:   "BenchmarkTwo",
						Result: gtr.Fail,
						Data:   map[string]interface{}{},
					},
				},
				Output: []string{"goarch: amd64"},
			},
			{
				Name:      "package/failing1",
				Timestamp: testTimestamp,
				BuildError: gtr.Error{
					ID:     6,
					Name:   "package/failing1",
					Cause:  "[build failed]",
					Output: []string{"error message"},
				},
			},
		},
	}

	rb := newReportBuilder()
	rb.timestampFunc = testTimestampFunc
	for _, ev := range events {
		rb.ProcessEvent(ev)
	}
	got := rb.Build()
	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("Incorrect report created, diff (-want, +got):\n%v", diff)
	}
}

func TestBuildReportMultiplePackages(t *testing.T) {
	events := []Event{
		{Package: "package/name1", Type: "run_test", Name: "TestOne"},
		{Package: "package/name2", Type: "run_test", Name: "TestOne"},
		{Package: "package/name1", Type: "output", Data: "\tHello"},
		{Package: "package/name1", Type: "end_test", Name: "TestOne", Result: "PASS", Duration: 1 * time.Millisecond},
		{Package: "package/name2", Type: "output", Data: "\tfile_test.go:10: error"},
		{Package: "package/name2", Type: "end_test", Name: "TestOne", Result: "FAIL", Duration: 1 * time.Millisecond},
		{Package: "package/name2", Type: "status", Result: "FAIL"},
		{Package: "package/name2", Type: "summary", Result: "FAIL", Name: "package/name2", Duration: 1 * time.Millisecond},
		{Package: "package/name1", Type: "status", Result: "PASS"},
		{Package: "package/name1", Type: "summary", Result: "ok", Name: "package/name1", Duration: 1 * time.Millisecond},
	}

	want := gtr.Report{
		Packages: []gtr.Package{
			{
				Name:      "package/name2",
				Duration:  1 * time.Millisecond,
				Timestamp: testTimestamp,
				Tests: []gtr.Test{
					{
						ID:       2,
						Name:     "TestOne",
						Duration: 1 * time.Millisecond,
						Result:   gtr.Fail,
						Output:   []string{"\tfile_test.go:10: error"},
						Data:     make(map[string]interface{}),
					},
				},
			},
			{
				Name:      "package/name1",
				Duration:  1 * time.Millisecond,
				Timestamp: testTimestamp,
				Tests: []gtr.Test{
					{
						ID:       1,
						Name:     "TestOne",
						Duration: 1 * time.Millisecond,
						Result:   gtr.Pass,
						Output:   []string{"\tHello"},
						Data:     make(map[string]interface{}),
					},
				},
			},
		},
	}

	rb := newReportBuilder()
	rb.timestampFunc = testTimestampFunc
	for _, ev := range events {
		rb.ProcessEvent(ev)
	}
	got := rb.Build()
	if diff := cmp.Diff(want, got); diff != "" {
		t.Errorf("Incorrect report created, diff (-want, +got):\n%v", diff)
	}
}

func TestSubtestModes(t *testing.T) {
	events := []Event{
		{Type: "run_test", Name: "TestParent"},
		{Type: "output", Data: "TestParent before"},
		{Type: "run_test", Name: "TestParent/Subtest#1"},
		{Type: "output", Data: "Subtest#1 output"},
		{Type: "run_test", Name: "TestParent/Subtest#2"},
		{Type: "output", Data: "Subtest#2 output"},
		{Type: "cont_test", Name: "TestParent"},
		{Type: "output", Data: "TestParent after"},
		{Type: "end_test", Name: "TestParent", Result: "PASS", Duration: 1 * time.Millisecond},
		{Type: "end_test", Name: "TestParent/Subtest#1", Result: "FAIL", Duration: 2 * time.Millisecond},
		{Type: "end_test", Name: "TestParent/Subtest#2", Result: "PASS", Duration: 3 * time.Millisecond},
		{Type: "output", Data: "output"},
		{Type: "summary", Result: "FAIL", Name: "package/name", Duration: 1 * time.Millisecond},
	}

	tests := []struct {
		name string
		mode SubtestMode
		want gtr.Report
	}{
		{
			name: "ignore subtest parent results",
			mode: IgnoreParentResults,
			want: gtr.Report{
				Packages: []gtr.Package{
					{
						Name:      "package/name",
						Duration:  1 * time.Millisecond,
						Timestamp: testTimestamp,
						Tests: []gtr.Test{
							{
								ID:       1,
								Name:     "TestParent",
								Duration: 1 * time.Millisecond,
								Result:   gtr.Pass,
								Output:   []string{"TestParent before", "TestParent after"},
								Data:     map[string]interface{}{},
							},
							{
								ID:       2,
								Name:     "TestParent/Subtest#1",
								Duration: 2 * time.Millisecond,
								Result:   gtr.Fail,
								Output:   []string{"Subtest#1 output"},
								Data:     map[string]interface{}{},
							},
							{
								ID:       3,
								Name:     "TestParent/Subtest#2",
								Duration: 3 * time.Millisecond,
								Result:   gtr.Pass,
								Output:   []string{"Subtest#2 output"},
								Data:     map[string]interface{}{},
							},
						},
						Output: []string{"output"},
					},
				},
			},
		},
		{
			name: "exclude subtest parents",
			mode: ExcludeParents,
			want: gtr.Report{
				Packages: []gtr.Package{
					{
						Name:      "package/name",
						Duration:  1 * time.Millisecond,
						Timestamp: testTimestamp,
						Tests: []gtr.Test{
							{
								ID:       2,
								Name:     "TestParent/Subtest#1",
								Duration: 2 * time.Millisecond,
								Result:   gtr.Fail,
								Output:   []string{"Subtest#1 output"},
								Data:     map[string]interface{}{},
							},
							{
								ID:       3,
								Name:     "TestParent/Subtest#2",
								Duration: 3 * time.Millisecond,
								Result:   gtr.Pass,
								Output:   []string{"Subtest#2 output"},
								Data:     map[string]interface{}{},
							},
						},
						Output: []string{"TestParent before", "TestParent after", "output"},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			rb := newReportBuilder()
			rb.timestampFunc = testTimestampFunc
			rb.subtestMode = test.mode
			for _, ev := range events {
				rb.ProcessEvent(ev)
			}
			got := rb.Build()
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("Invalid report created from events, diff (-want, +got):\n%v", diff)
			}
		})
	}
}

func TestGroupBenchmarksByName(t *testing.T) {
	output := collector.New()
	for i := 1; i <= 4; i++ {
		output.AppendToID(i, fmt.Sprintf("output-%d", i))
	}

	tests := []struct {
		name string
		in   []gtr.Test
		want []gtr.Test
	}{
		{"nil", nil, nil},
		{
			"one failing benchmark",
			[]gtr.Test{{ID: 1, Name: "BenchmarkFailed", Result: gtr.Fail, Data: map[string]interface{}{}}},
			[]gtr.Test{{ID: 1, Name: "BenchmarkFailed", Result: gtr.Fail, Output: []string{"output-1"}, Data: map[string]interface{}{}}},
		},
		{
			"four passing benchmarks",
			[]gtr.Test{
				{ID: 1, Name: "BenchmarkOne", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 10, MBPerSec: 400, BytesPerOp: 1, AllocsPerOp: 2}}},
				{ID: 2, Name: "BenchmarkOne", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 20, MBPerSec: 300, BytesPerOp: 1, AllocsPerOp: 4}}},
				{ID: 3, Name: "BenchmarkOne", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 30, MBPerSec: 200, BytesPerOp: 1, AllocsPerOp: 8}}},
				{ID: 4, Name: "BenchmarkOne", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 40, MBPerSec: 100, BytesPerOp: 5, AllocsPerOp: 2}}},
			},
			[]gtr.Test{
				{ID: 1, Name: "BenchmarkOne", Result: gtr.Pass, Output: []string{"output-1", "output-2", "output-3", "output-4"}, Data: map[string]interface{}{key: Benchmark{NsPerOp: 25, MBPerSec: 250, BytesPerOp: 2, AllocsPerOp: 4}}},
			},
		},
		{
			"four mixed result benchmarks",
			[]gtr.Test{
				{ID: 1, Name: "BenchmarkMixed", Result: gtr.Unknown},
				{ID: 2, Name: "BenchmarkMixed", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 10, MBPerSec: 400, BytesPerOp: 1, AllocsPerOp: 2}}},
				{ID: 3, Name: "BenchmarkMixed", Result: gtr.Pass, Data: map[string]interface{}{key: Benchmark{NsPerOp: 40, MBPerSec: 100, BytesPerOp: 3, AllocsPerOp: 4}}},
				{ID: 4, Name: "BenchmarkMixed", Result: gtr.Fail},
			},
			[]gtr.Test{
				{ID: 1, Name: "BenchmarkMixed", Result: gtr.Fail, Output: []string{"output-1", "output-2", "output-3", "output-4"}, Data: map[string]interface{}{key: Benchmark{NsPerOp: 25, MBPerSec: 250, BytesPerOp: 2, AllocsPerOp: 3}}},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			got := groupBenchmarksByName(test.in, output)
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("groupBenchmarksByName result incorrect, diff (-want, +got):\n%s\n", diff)
			}
		})
	}
}

func TestReportSpecialCases(t *testing.T) {
	tests := []struct {
		name   string
		events []Event
		want   gtr.Report
	}{
		{
			"failed-summary-only",
			[]Event{{Type: "summary", Result: "FAIL", Name: "package/name", Duration: 1 * time.Millisecond}},
			gtr.Report{
				Packages: []gtr.Package{
					{
						Name:      "package/name",
						Duration:  1 * time.Millisecond,
						Timestamp: testTimestamp,
						RunError: gtr.Error{
							Name: "package/name",
						},
					},
				},
			},
		},
		{
			"leftover-builderror",
			[]Event{
				{Type: "build_output", Name: "package/name"},
				{Type: "output", Data: "error message"},
			},
			gtr.Report{
				Packages: []gtr.Package{
					{
						Name:      "package/name",
						Timestamp: testTimestamp,
						BuildError: gtr.Error{
							ID:     1,
							Name:   "package/name",
							Output: []string{"error message"},
						},
					},
				},
			},
		},
		{
			"build error in package with _test suffix",
			[]Event{
				{Type: "build_output", Name: "package/name_test"},
				{Type: "summary", Name: "package/name", Result: "FAIL", Data: "[build failed]"},
			},
			gtr.Report{
				Packages: []gtr.Package{
					{
						Name:      "package/name",
						Timestamp: testTimestamp,
						BuildError: gtr.Error{
							ID:    1,
							Name:  "package/name_test",
							Cause: "[build failed]",
						},
					},
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			rb := newReportBuilder()
			rb.timestampFunc = testTimestampFunc
			for _, ev := range test.events {
				rb.ProcessEvent(ev)
			}
			got := rb.Build()
			if diff := cmp.Diff(test.want, got); diff != "" {
				t.Errorf("Incorrect report created, diff (-want, +got):\n%v\n", diff)
			}
		})
	}
}