package tfsdklog

import (
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"sync"
	"syscall"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/terraform-plugin-log/internal/logging"
	testing "github.com/mitchellh/go-testing-interface"
)

const (
	// envLog is the environment variable that users can set to control the
	// least-verbose level of logs that will be output during testing. If
	// this environment variable is set, it will default to off. This is
	// just the default; specific loggers and sub-loggers can set a lower
	// or higher verbosity level without a problem right now. In theory,
	// they should not be able to.
	//
	// Valid values are TRACE, DEBUG, INFO, WARN, ERROR, and OFF. A special
	// pseudo-value, JSON, will set the value to TRACE and output the
	// results in their JSON format.
	envLog = "TF_LOG"

	// envLogFile is the environment variable that controls where log
	// output is written during tests. By default, logs will be written to
	// standard error. Setting this environment variable to another file
	// path will write logs there instead during tests.
	envLogFile = "TF_LOG_PATH"

	// envAccLogFile is the environment variable that controls where log
	// output from the provider under test and the Terraform binary (and
	// other providers) will be written during tests. Setting this
	// environment variable to a file will combine all log output in that
	// file. If both this environment variable and TF_LOG_PATH are set,
	// this environment variable will take precedence.
	envAccLogFile = "TF_ACC_LOG_PATH"

	// envLogPathMask is the environment variable that controls per-test
	// logging output. It should be set to a fmt-compatible string, where a
	// single %s will be replaced with the test name, and the log output
	// for that test (and only that test) will be written to that file.
	// Setting this environment variable will override TF_LOG_PATH.
	// Only the logs for the provider under test are included.
	envLogPathMask = "TF_LOG_PATH_MASK"
)

// ValidLevels are the string representations of levels that can be set for
// loggers.
var ValidLevels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF"}

// Only show invalid log level message once across any number of level lookups.
var invalidLogLevelMessage sync.Once

// RegisterTestSink sets up a logging sink, for use with test frameworks and
// other cases where plugin logs don't get routed through Terraform. This
// applies the same filtering and file output behaviors that Terraform does.
//
// RegisterTestSink should only ever be called by test frameworks, providers
// should never call it.
//
// RegisterTestSink must be called prior to any loggers being setup or
// instantiated.
func RegisterTestSink(ctx context.Context, t testing.T) context.Context {
	logger, loggerOptions := newSink(t)

	ctx = logging.SetSink(ctx, logger)
	ctx = logging.SetSinkOptions(ctx, loggerOptions)

	return ctx
}

func newSink(t testing.T) (hclog.Logger, *hclog.LoggerOptions) {
	logOutput := io.Writer(os.Stderr)
	var json bool
	var logLevel hclog.Level
	var logFile string

	envLevel := strings.ToUpper(os.Getenv(envLog))

	// if TF_LOG_PATH is set, output logs there
	if logPath := os.Getenv(envLogFile); logPath != "" {
		logFile = logPath
	}

	// if TF_ACC_LOG_PATH is set, output logs there instead
	if logPath := os.Getenv(envAccLogFile); logPath != "" {
		logFile = logPath
		// helper/resource makes this default to TRACE, so we should,
		// too
		envLevel = "TRACE"
	}

	// if TF_LOG_PATH_MASK is set, use a test-name specific logging file,
	// instead
	if logPathMask := os.Getenv(envLogPathMask); logPathMask != "" {
		testName := strings.Replace(t.Name(), "/", "__", -1)
		logFile = fmt.Sprintf(logPathMask, testName)
	}

	if logFile != "" {
		f, err := os.OpenFile(logFile, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err)
		} else {
			logOutput = f
		}
	}

	// if TF_LOG is set, set the level
	if envLevel == "" {
		logLevel = hclog.Off
	} else if envLevel == "JSON" {
		logLevel = hclog.Trace
		json = true
	} else if isValidLogLevel(envLevel) {
		logLevel = hclog.LevelFromString(envLevel)
	} else {
		invalidLogLevelMessage.Do(func() {
			fmt.Fprintf(
				os.Stderr,
				"[WARN] Invalid log level: %q. Defaulting to level: OFF. Valid levels are: %+v\n",
				envLevel,
				ValidLevels,
			)
		})
	}

	loggerOptions := &hclog.LoggerOptions{
		Level:             logLevel,
		Output:            logOutput,
		IndependentLevels: true,
		JSONFormat:        json,
	}

	return hclog.New(loggerOptions), loggerOptions
}

func isValidLogLevel(level string) bool {
	for _, validLevel := range ValidLevels {
		if level == validLevel {
			return true
		}
	}

	return false
}
