package testutil

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"testing"
	"text/template"

	"github.com/go-test/deep"
	"github.com/sergi/go-diff/diffmatchpatch"
	"gopkg.in/yaml.v2"
)

// TestDataDiffer holds configuration for generating test diff
type TestDataDiffer struct {
	PrettyDiff     bool
	UpdateFixtures bool
	RejectPath     string
}

// DiffTestYAML compares a YAML structure to a fixture on the filestystem.
func (td *TestDataDiffer) DiffTestYAML(path string, actualYAML string) error {
	expectedYAML := ReadTestdata(path)
	return td.diffTestYAML(path, actualYAML, expectedYAML)
}

// DiffTestYAMLTemplate compares a YAML structure to a parameterized fixture on the filestystem.
func (td *TestDataDiffer) DiffTestYAMLTemplate(path string, actualYAML string, params any) error {
	file := filepath.Join("testdata", path)
	t, err := template.New(path).ParseFiles(file)
	if err != nil {
		return fmt.Errorf("Failed to read YAML template from %s: %w", path, err)
	}
	var buf bytes.Buffer
	err = t.Execute(&buf, params)
	if err != nil {
		return fmt.Errorf("Failed to build YAML from template %s: %w", path, err)
	}
	return td.diffTestYAML(path, actualYAML, buf.String())
}

func (td *TestDataDiffer) diffTestYAML(path, actualYAML, expectedYAML string) error {
	actual, err := unmarshalYAML([]byte(actualYAML))
	if err != nil {
		return fmt.Errorf("Failed to unmarshal generated YAML: %w", err)
	}
	expected, err := unmarshalYAML([]byte(expectedYAML))
	if err != nil {
		return fmt.Errorf("Failed to unmarshal expected YAML: %w", err)
	}
	diff := deep.Equal(expected, actual)
	if diff == nil {
		return nil
	}
	td.storeActual(path, []byte(actualYAML))
	e := fmt.Sprintf("YAML mismatches %s:", path)
	for _, d := range diff {
		e += fmt.Sprintf("\n	%s", d)
	}
	return errors.New(e)
}

// DiffTestdata generates the diff for actual w.r.the file in path
func (td *TestDataDiffer) DiffTestdata(t *testing.T, path, actual string) {
	t.Helper()
	expected := ReadTestdata(path)
	if actual == expected {
		return
	}
	dmp := diffmatchpatch.New()
	diffs := dmp.DiffMain(expected, actual, true)
	diffs = dmp.DiffCleanupSemantic(diffs)
	var diff string
	if td.PrettyDiff {
		diff = dmp.DiffPrettyText(diffs)
	} else {
		diff = dmp.PatchToText(dmp.PatchMake(diffs))
	}
	t.Errorf("mismatch: %s\n%s", path, diff)

	td.storeActual(path, []byte(actual))
}

func (td *TestDataDiffer) storeActual(path string, actual []byte) {
	if td.UpdateFixtures {
		writeTestdata(path, actual)
	}

	if td.RejectPath != "" {
		writeRejects(path, actual, td.RejectPath)
	}
}

// ReadTestdata reads a file and returns the contents of that file as a string.
func ReadTestdata(fileName string) string {
	file, err := os.Open(filepath.Join("testdata", fileName))
	if err != nil {
		panic(fmt.Sprintf("Failed to open expected output file: %v", err))
	}

	fixture, err := io.ReadAll(file)
	if err != nil {
		panic(fmt.Sprintf("Failed to read expected output file: %v", err))
	}

	return string(fixture)
}

func unmarshalYAML(data []byte) ([]interface{}, error) {
	objs := make([]interface{}, 0)
	rd := bytes.NewReader(data)
	decoder := yaml.NewDecoder(rd)
	for {
		var obj interface{}
		if err := decoder.Decode(&obj); err != nil {
			if errors.Is(err, io.EOF) {
				return objs, nil
			}
			return nil, err
		}

		objs = append(objs, obj)
	}
}

func writeTestdata(fileName string, data []byte) {
	p := filepath.Join("testdata", fileName)
	if err := os.WriteFile(p, data, 0600); err != nil {
		panic(err)
	}
}

func writeRejects(origFileName string, data []byte, rejectPath string) {
	p := filepath.Join(rejectPath, origFileName+".rej")
	if err := os.WriteFile(p, data, 0600); err != nil {
		panic(err)
	}
}
