package asserter

import (
	"context"
	"crypto/sha256"
	"fmt"
	"gardening/src/lib/filesystem"
	"gardening/src/lib/operating_system"
	"gardening/src/lib/spy"
	"gardening/sup/lib/reporter"
	"os"
	"sort"
	"strings"
)

type FilesystemAsserter struct {
	Reporter   *reporter.Reporter
	Asserter   *Asserter
	Spy        *spy.Spy
	Filesystem *filesystem.Filesystem
}

func (this *FilesystemAsserter) AssertFirstEditedFileContent(ctx context.Context, report *spy.Report, reference string) {
	filesWritten := this.Reporter.FindFilesWritten(ctx, report)
	if len(filesWritten) == 0 {
		this.Asserter.EqualIntegers(ctx, 1, len(filesWritten))
	}
	content := this.Filesystem.MustReadFile(ctx, filesWritten[0])
	this.Asserter.EqualStringReference(ctx, reference, content)
}

func (this *FilesystemAsserter) AssertFilesChangesCount(ctx context.Context, report *spy.Report, created int, updated int, deleted int) {
	createdCount := 0
	updatedCount := 0
	deletedCount := 0

	records := this.Reporter.FindRecordsSetByKinds(ctx, report, []string{"write-file", "remove"})
	for _, record := range records {
		if record.Kind == "write-file" {
			d := record.Data.(*operating_system.WriteFileDataSpyRecord)
			if d.Created == true {
				createdCount++
			}
			if d.Updated == true {
				updatedCount++
			}
		}
		if record.Kind == "remove" {
			d := record.Data.(*operating_system.DeleteFileDataSpyRecord)
			if d.Deleted == true {
				deletedCount++
			}
		}
	}

	expected := fmt.Sprintf("Created: %d, Updated: %d, Deleted %d", created, updated, deleted)
	actual := fmt.Sprintf("Created: %d, Updated: %d, Deleted %d", createdCount, updatedCount, deletedCount)

	if expected != actual {
		this.Asserter.MakeReport(ctx, &ExpectationReport{
			Message:  "Files changes count is not equals",
			Expected: expected,
			Actual:   actual,
			Kind:     KindEqualStrings,
		})
	}
}

func (this *FilesystemAsserter) AssertDirsDifferences(ctx context.Context, reference string, d1 string, d2 string) {
	referenceFiles := this.collectDirFiles(ctx, d1)
	actualFiles := this.collectDirFiles(ctx, d2)

	differences := this.computeDifferences(referenceFiles, actualFiles)

	this.Asserter.EqualStringReference(ctx, reference, differences)
}

func (this *FilesystemAsserter) computeDifferences(reference map[string]string, actual map[string]string) string {
	var differences []string

	// Files only in reference
	for path, hash := range reference {
		if actualHash, exists := actual[path]; !exists {
			differences = append(differences, fmt.Sprintf("- %s", path))
		} else if actualHash != hash {
			differences = append(differences, fmt.Sprintf("* %s", path))
		}
	}

	// Files only in actual
	for path, _ := range actual {
		if _, exists := reference[path]; !exists {
			differences = append(differences, fmt.Sprintf("+ %s", path))
		}
	}

	sort.Strings(differences)

	return strings.Join(differences, "\n")
}

func (this *FilesystemAsserter) collectDirFiles(ctx context.Context, dir string) map[string]string {
	files := make(map[string]string)
	err := this.Filesystem.Walk(ctx, dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			content := this.Filesystem.MustReadFile(ctx, path)
			hash := sha256.Sum256([]byte(content))
			p := strings.TrimPrefix(path, dir)
			files[p] = fmt.Sprintf("%x", hash)
		}
		return nil
	})
	if err != nil {
		panic("FilesystemAsserter.collectDirFiles failed to walk: " + err.Error())
	}
	return files
}
