package asserter

// |@@| C

import (
	"context"
	"encoding/json"
	"fmt"
	"gardening/src/lib"
	"gardening/src/lib/config"
	"gardening/src/lib/dev_tool"
	"gardening/src/lib/jsoner"
	"gardening/src/lib/spy"
	"gardening/sup/lib/reporter"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"testing"
	"unicode"
)

type Asserter struct {
	DevTool  *dev_tool.DevTool
	Config   *config.Config
	Jsoner   *jsoner.Jsoner
	Reporter *reporter.Reporter
}

type ExpectationReport struct {
	Name         string      `json:"name"`
	Message      string      `json:"message"`
	Expected     string      `json:"expected"`
	Actual       string      `json:"actual"`
	DirectCaller string      `json:"direct_caller"`
	Kind         string      `json:"kind"`
	Data         interface{} `json:"data"`
	AcceptData   interface{} `json:"accept_data"`
	DevToolUrl   string      `json:"dev_tool_url"`
}

type AcceptDataEqualFileContent struct {
	FilePath string `json:"file_path"`
}

type AcceptDataEqualStructures struct {
	FilePath string `json:"file_path"`
}

type AcceptDataEqualReport struct {
	FilePath string `json:"file_path"`
}

type ExpectationReportData struct {
	Expected *spy.Report `json:"expected"`
	Actual   *spy.Report `json:"actual"`
}

const KindEqualFileContent = "equal-file-content"
const KindEqualReports = "equal-reports"
const KindEqualStructures = "equal-structures"
const KindEqualStrings = "equal-strings"
const KindNotEqualStrings = "not-equal-strings"
const KindEqualIntegers = "equal-integers"
const KindWithError = "with-error"
const KindWithoutError = "without-error"
const KindIsFalse = "is-false"
const KindIsNil = "is-nil"

const (
	Reset = "\033[0m"
	Red   = "\033[91"
)

const (
	EndFmt = "m"
)

type TestRecordDataEqualJsonReference struct {
	ExpectedFilePath string `json:"expected_file_path"`
}

func (this *Asserter) IsValid(ctx context.Context, bool bool) {
	if bool == true {
		return
	}
	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Is invalid",
		Expected: "true",
		Actual:   "false",
		Kind:     KindIsFalse,
		Data:     nil,
	})
}

func (this *Asserter) IsTrue(ctx context.Context, bool bool) {
	if bool == true {
		return
	}
	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Is false when it should be true",
		Expected: "true",
		Actual:   "false",
		Kind:     KindIsFalse,
		Data:     nil,
	})
}

func (this *Asserter) IsFalse(ctx context.Context, bool bool) {
	if bool == false {
		return
	}
	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Is true when it should be false",
		Expected: "false",
		Actual:   "true",
		Kind:     KindIsFalse,
		Data:     nil,
	})
}

func (this *Asserter) WithoutError(ctx context.Context, err error) {
	if err == nil {
		return
	}
	println(err.Error())
	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Has error",
		Expected: "",
		Actual:   err.Error(),
		Kind:     KindWithoutError,
		Data:     nil,
	})
}

func (this *Asserter) WithError(ctx context.Context, err error) {
	if err != nil {
		return
	}
	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Expect error, but none present",
		Expected: "presence",
		Actual:   "absence",
		Kind:     KindWithError,
		Data:     nil,
	})
}

func (this *Asserter) EqualJsonReference(ctx context.Context, referencePath string, actual json.RawMessage) {
	filePath := "./test_data/" + referencePath + ".json"
	expected, err := os.ReadFile(filePath)
	if err != nil {
		panic(err)
	}
	expectedS := string(expected)
	actualS := string(actual)
	absPath, err := filepath.Abs(filePath)
	if err != nil {
		panic(err)
	}
	if expectedS != actualS {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Json is not equals to the reference",
			Expected: expectedS,
			Actual:   actualS,
			Kind:     "equal-json-reference",
			Data: &TestRecordDataEqualJsonReference{
				ExpectedFilePath: absPath,
			},
		})
	}
}

func (this *Asserter) EqualIntegers(ctx context.Context, expected int, actual int) {
	if expected != actual {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Integers are not equals",
			Expected: strconv.Itoa(expected),
			Actual:   strconv.Itoa(actual),
			Kind:     KindEqualIntegers,
		})
	}
}

func (this *Asserter) EqualStrings(ctx context.Context, expected string, actual string) {
	if expected != actual {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Strings are not equals",
			Expected: expected,
			Actual:   actual,
			Kind:     KindEqualStrings,
		})
	}
}

func (this *Asserter) NotEqualStrings(ctx context.Context, expected string, actual string) {
	if expected == actual {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Strings are equals",
			Expected: expected,
			Actual:   actual,
			Kind:     KindNotEqualStrings,
		})
	}
}

func (this *Asserter) IsNotNil(ctx context.Context, actual interface{}) {
	if actual == nil {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Is nil",
			Expected: "not nil",
			Actual:   "nil",
			Kind:     KindIsNil,
		})
	}
}

func (this *Asserter) EqualFileContentBetweenReferenceAndData(ctx context.Context, fileName string) {
	fileName = fileName + ".txt"
	testDir := this.testDir()
	referenceDir := filepath.Join(testDir, "references")
	dataDir := filepath.Join(testDir, "data")
	referencePath := filepath.Join(referenceDir, fileName)
	dataPath := filepath.Join(dataDir, fileName)
	b, err := os.ReadFile(referencePath)
	lib.Poe(err)
	b2, err := os.ReadFile(dataPath)
	lib.Poe(err)
	expected := string(b)
	actual := string(b2)
	if expected != actual {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Files content not equals",
			Expected: expected,
			Actual:   actual,
			Kind:     KindEqualFileContent,
			AcceptData: AcceptDataEqualFileContent{
				FilePath: referencePath,
			},
		})
	}
}

func (this *Asserter) EqualStringReference(ctx context.Context, reference string, actual string) {
	fileName := reference + ".txt"
	testDataDir := this.testDir()
	filesReportDataDir := filepath.Join(testDataDir, "references")
	err := os.MkdirAll(filesReportDataDir, 0777)
	lib.Poe(err)
	expectedFilePath := filepath.Join(filesReportDataDir, fileName)
	if _, err := os.Stat(expectedFilePath); err != nil {
		err = os.WriteFile(expectedFilePath, []byte(""), 0666)
		lib.Poe(err)
	}
	b, err := os.ReadFile(expectedFilePath)
	lib.Poe(err)
	expected := string(b)

	if expected != actual {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Files content not equals",
			Expected: expected,
			Actual:   actual,
			Kind:     KindEqualFileContent,
			AcceptData: AcceptDataEqualFileContent{
				FilePath: expectedFilePath,
			},
		})
	}
}

// Deprecated : Use EqualStringReference instead
func (this *Asserter) EqualFileContentReference(ctx context.Context, expectedFileName string, actual string) {
	this.EqualStringReference(ctx, expectedFileName, actual)
}

func (this *Asserter) EqualStructureReference(ctx context.Context, expectedReference string, actual interface{}) {
	testDataDir := strings.TrimSuffix(this.testFilePath(), "_test.go")
	filesReportDataDir := filepath.Join(testDataDir, "references")
	err := os.MkdirAll(filesReportDataDir, 0777)
	lib.Poe(err)
	expectedFilePath := filepath.Join(filesReportDataDir, expectedReference+".json")
	if _, err := os.Stat(expectedFilePath); err != nil {
		err = os.WriteFile(expectedFilePath, []byte(""), 0666)
		lib.Poe(err)
	}
	expectedJson, err := os.ReadFile(expectedFilePath)
	lib.Poe(err)
	actualJson := this.Jsoner.MustMarshalIndent(actual)

	expectedString := string(expectedJson)
	actualString := string(actualJson)

	if expectedString != actualString {
		this.MakeReport(ctx, &ExpectationReport{
			Message:  "Structures are not equals",
			Expected: expectedString,
			Actual:   actualString,
			Kind:     KindEqualStructures,
			AcceptData: AcceptDataEqualStructures{
				FilePath: expectedFilePath,
			},
		})
	}
}

func (this *Asserter) MakeReport(ctx context.Context, report *ExpectationReport) {
	_, filename, line, _ := runtime.Caller(2)
	directCaller := filename + ":" + strconv.Itoa(line)
	report.DirectCaller = directCaller
	localDirectCaller := strings.TrimPrefix(directCaller, this.Config.GetRootDir()+"/")

	t, ok := ctx.Value("t").(*testing.T)
	if !ok {
		panic("Recording assertion failure requires to have a ctx with a `t` key, containing a *testing.T")
	}

	report.Name = t.Name()

	b, err := json.Marshal(report)
	lib.Poe(err)

	_, path, _, _ := runtime.Caller(0)

	path = filepath.Dir(path)
	path += "/../../.."
	if err != nil {
		lib.Poe(err)
	}
	err = os.WriteFile(path+"/tmp/last-test-fail.json", b, 0644)
	lib.Poe(err)

	fmt.Println(Red + EndFmt + "    ➤ Error: " + Reset + report.Message)

	t.Log(report.Message)
	displayDifference := []string{KindEqualStrings, KindEqualIntegers, KindIsFalse}
	if slices.Contains(displayDifference, report.Kind) {
		t.Log("expected vs actual: " + report.Expected + " vs " + report.Actual)

	}
	t.Log("detail: ./tmp/last-test-fail.json")
	println("\tcaller: " + localDirectCaller) // Println and not Log to have the linking in IDE

	id := this.DevTool.SendExpectationReport(ctx, report)
	url := this.DevTool.GetExpectationUrl(id)
	println("\turl: " + url)
	println("")

	t.FailNow()
}

func (this *Asserter) EqualReported(ctx context.Context, report *spy.Report) bool {
	testDataDir := strings.TrimSuffix(this.testFilePath(), "_test.go")
	spyReportDataDir := filepath.Join(testDataDir, "spy_reports")
	err := os.MkdirAll(spyReportDataDir, 0777)
	lib.Poe(err)
	testingFunctionName := ctx.Value("t").(*testing.T).Name()

	storedReport := &spy.Report{}

	fileName := this.pascalToSnakeCase(strings.TrimPrefix(testingFunctionName, "Test")) + ".json"
	filePath := filepath.Join(spyReportDataDir, fileName)
	if _, err := os.Stat(filePath); err != nil {
		println("No existing report")
	} else {
		b, err := os.ReadFile(filePath)
		lib.Poe(err)
		err = json.Unmarshal(b, storedReport)
		lib.Poe(err)
	}

	if storedReport.Hash == report.Hash {
		return true
	}

	data := ExpectationReportData{
		Expected: storedReport,
		Actual:   report,
	}

	this.MakeReport(ctx, &ExpectationReport{
		Message:  "Reports are not equals",
		Expected: storedReport.Hash,
		Actual:   report.Hash,
		Kind:     KindEqualReports,
		Data:     data,
		AcceptData: AcceptDataEqualReport{
			FilePath: filePath,
		},
	})
	return false
}

func (this *Asserter) pascalToSnakeCase(s string) string {
	var result []rune
	for i, r := range s {
		if i > 0 && unicode.IsUpper(r) {
			result = append(result, '_')
		}
		result = append(result, unicode.ToLower(r))
	}
	return string(result)
}

func (this *Asserter) testDir() string {
	return strings.TrimSuffix(this.testFilePath(), "_test.go")
}

func (this *Asserter) testFilePath() string {
	for skip := 0; ; skip++ {
		_, file, _, ok := runtime.Caller(skip)
		if !ok {
			break
		}
		if strings.Count(file, "/tests/") > 0 && strings.HasSuffix(file, "_test.go") {
			return file
		}
	}
	panic("Unable to detect test file path")
}

func (this *Asserter) HasNotPanic() bool {
	return true
}
