// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package testing

import (
	"bytes"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

	"github.com/go-test/deep"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

var update = flag.Bool("update", false, "update .golden files")
var force = flag.Bool("force", false, "overwrite .golden files even if they already exist")

// FixtureTest is a test that is based on fixture and golden files. This is very convenient to test a largely variable surface with many variants.
type FixtureTest struct {
	T        *testing.T
	Path     string
	GoldPath func(path string) string
	Test     FixtureTestFunc
	Fixture  func() interface{}
	Gold     func() interface{}
}

// FixtureTestFunc implements the actual fixture test
type FixtureTestFunc func(t *testing.T, fixture interface{}) interface{}

// Run executes the fixture test - do not forget to call this one
func (ft *FixtureTest) Run() {
	t := ft.T

	fixtures, err := filepath.Glob(ft.Path)
	if err != nil {
		t.Error("cannot list test fixtures: ", err)
		return
	}

	for _, fn := range fixtures {
		t.Run(fn, func(t *testing.T) {
			fd, err := os.ReadFile(fn)
			if err != nil {
				t.Errorf("cannot read %s: %v", fn, err)
				return
			}

			fixture := ft.Fixture()
			if typ := reflect.TypeOf(fixture); typ.Kind() != reflect.Ptr {
				t.Error("Fixture() did not return a pointer")
				return
			}
			if msg, ok := fixture.(proto.Message); ok {
				err = protojson.Unmarshal(fd, msg)
				if err != nil {
					t.Errorf("cannot unmarshal %s: %v", fn, err)
					return
				}
			} else {
				err = json.Unmarshal(fd, fixture)
				if err != nil {
					t.Errorf("cannot unmarshal %s: %v", fn, err)
					return
				}
			}

			result := ft.Test(t, fixture)
			if result == nil {
				// Test routine is expected to complain using t.Errorf
				t.Logf("test routine for %s returned nil - continuing", fn)
				return
			}
			if typ := reflect.TypeOf(result); typ.Kind() != reflect.Ptr {
				t.Error("Test() did not return a pointer")
				return
			}

			actual, err := json.MarshalIndent(result, "", "    ")
			if err != nil {
				t.Errorf("cannot marshal status for %s: %v", fn, err)
				return
			}

			goldenFilePath := fmt.Sprintf("%s.golden", strings.TrimSuffix(fn, filepath.Ext(fn)))
			if ft.GoldPath != nil {
				goldenFilePath = ft.GoldPath(fn)
			}
			if *update {
				if _, err := os.Stat(goldenFilePath); *force || errors.Is(err, fs.ErrNotExist) {
					err = os.WriteFile(goldenFilePath, actual, 0600)
					if err != nil {
						t.Errorf("cannot write gold standard %s: %v", goldenFilePath, err)
						return
					}

					t.Logf("Wrote new gold standard in %s", goldenFilePath)
				} else {
					t.Logf("Did not overwrite gold standard in %s", goldenFilePath)
				}
			}

			expected, err := os.ReadFile(goldenFilePath)
			if err != nil {
				t.Errorf("cannot read golden file %s: %v", goldenFilePath, err)
				return
			}
			expected = bytes.TrimSpace(expected)

			if !bytes.Equal(actual, expected) {
				expectedResult := ft.Gold()
				if typ := reflect.TypeOf(expectedResult); typ.Kind() != reflect.Ptr {
					t.Error("Gold() did not return a pointer")
					return
				}

				err = json.Unmarshal(expected, expectedResult)
				if err != nil {
					t.Errorf("cannot unmarshal JSON %s: %v", goldenFilePath, err)
					return
				}

				diff := deep.Equal(expectedResult, result)
				if len(diff) > 0 {
					t.Errorf("fixture %s: %v", fn, diff)
				}

				return
			}
		})
	}
}
