// Copyright 2021 Molecula Corp. All rights reserved.
package test

import (
	"log"
	"os"
	"sync"
	"testing"

	"github.com/featurebasedb/featurebase/v3/testhook"
)

// glue lets us make a thing which isn't a testing.TB, but can be passed
// around to the functions that take testing.TB.

// DirCleaner represents the subset of the testing.TB interface
// we care about, allowing us to take objects which behave like
// that without importing all of testing to get them.
type DirCleaner interface {
	Helper()
	Errorf(string, ...interface{})
	Fatalf(string, ...interface{})
	Fatal(...interface{})
	Logf(string, ...interface{})
	Name() string
	TempDir() string
	Cleanup(func())
	Skip(...interface{})
}

// Verify that a TB is a DirCleaner
var _ DirCleaner = testing.TB(nil)
var _ DirCleaner = &wholeTestRunWrapper{}

// wholeTestRun is a thing that's shaped a bit like testing.TB,
// but it can be used across all the tests, running its cleanup functions
// at the very end of the testing process. this lets us create clusters
// using the same code and logic we would for per-test things, except
// substituting this, and then have a single global post-test-hook run
// their cleanup.
type wholeTestRun struct {
	setup        sync.Once
	mu           sync.Mutex
	tempDirs     []string
	cleanupFuncs []func()
}

// wholeTestRunWrapper is a test-specific thing that can refer to the
// global shared state, but also forwards everything *except* TempDir,
// Cleanup, and Logf to the tb it's created with.
type wholeTestRunWrapper struct {
	testing.TB
}

// globalT is a system-wide wholeTestRun. the first time it's used, for any
// reason, it registers a cleanup with testhook, which will run at the end
// of the TestMain stuff, but before any previously-registered cleanup
// functions (such as the auditor stuff) so it can ensure that everything's
// been deleted. Basically this exists to let us call `tb.TempDir` on
// things and get a directory which outlives the current test.
var globalT wholeTestRun

// NewWholeTestRun produces a wholeTestRunWrapper around TB, which overrides
// a couple of the TB's methods to get whole-test-friendly behaviors.
func NewWholeTestRun(tb testing.TB) *wholeTestRunWrapper {
	return &wholeTestRunWrapper{TB: tb}
}

func (w *wholeTestRun) Teardown() {
	w.mu.Lock()
	defer w.mu.Unlock()
	for _, fn := range w.cleanupFuncs {
		fn()
	}
	for _, path := range w.tempDirs {
		// disregard errors because we don't care that much about them;
		// we assume RemoveAll probably works unless something's wrong.
		// see the comments on the testing package's internal removeAll,
		// which suggests the errors only happen on Windows, which we
		// don't support.
		_ = os.RemoveAll(path)
	}
}

func (w *wholeTestRun) Setup() {
	w.setup.Do(func() {
		testhook.RegisterPostTestHook(func() error {
			w.Teardown()
			return nil
		})
	})
}

// We provide a Logf here because the wholeTestRunWrapper could
// be set as a tb-replacement for something which lasts past the
// test that created it. That shouldn't happen, probably.
func (w *wholeTestRunWrapper) Logf(msg string, args ...interface{}) {
	log.Printf(msg, args...)
}

func (w *wholeTestRun) TempDir(tb DirCleaner) string {
	w.Setup()
	w.mu.Lock()
	defer w.mu.Unlock()
	path, err := os.MkdirTemp("", "test-temp-")
	if err != nil {
		tb.Fatalf("creating temp dir: %v", err)
	}
	globalT.tempDirs = append(globalT.tempDirs, path)
	return path
}

func (w *wholeTestRun) Cleanup(fn func()) {
	w.Setup()
	w.mu.Lock()
	defer w.mu.Unlock()
	w.cleanupFuncs = append(w.cleanupFuncs, fn)
}

func (w *wholeTestRunWrapper) TempDir() string {
	return globalT.TempDir(w.TB)
}

func (w *wholeTestRunWrapper) Cleanup(fn func()) {
	globalT.Cleanup(fn)
}
