/*
 *
 * Copyright 2020 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package grpctest

import (
	"errors"
	"fmt"
	"os"
	"path"
	"regexp"
	"runtime"
	"strconv"
	"sync"
	"testing"
	"time"

	"google.golang.org/grpc/grpclog"
)

// tLogr serves as the grpclog logger and is the interface through which
// expected errors are declared in tests.
var tLogr *tLogger

const callingFrame = 4

type logType int

func (l logType) String() string {
	switch l {
	case infoLog:
		return "INFO"
	case warningLog:
		return "WARNING"
	case errorLog:
		return "ERROR"
	case fatalLog:
		return "FATAL"
	}
	return "UNKNOWN"
}

const (
	infoLog logType = iota
	warningLog
	errorLog
	fatalLog
)

type tLogger struct {
	v           int
	initialized bool

	mu    sync.Mutex
	t     *testing.T
	start time.Time
	logs  map[logType]map[*regexp.Regexp]int
}

func init() {
	vLevel := 0 // Default verbosity level

	if vLevelEnv, found := os.LookupEnv("GRPC_GO_LOG_VERBOSITY_LEVEL"); found {
		// If found, attempt to convert. If conversion is successful, update vLevel.
		// If conversion fails, log a warning, but vLevel remains its default of 0.
		if val, err := strconv.Atoi(vLevelEnv); err == nil {
			vLevel = val
		} else {
			// Log the error if the environment variable is not a valid integer.
			fmt.Printf("Warning: GRPC_GO_LOG_VERBOSITY_LEVEL environment variable '%s' is not a valid integer. "+
				"Using default verbosity level 0. Error: %v\n", vLevelEnv, err)
		}
	}
	// Initialize tLogr with the determined verbosity level.
	logsMap := map[logType]map[*regexp.Regexp]int{
		errorLog:   {},
		warningLog: {},
	}
	tLogr = &tLogger{logs: logsMap, v: vLevel}
}

// getCallingPrefix returns the <file:line> at the given depth from the stack.
func getCallingPrefix(depth int) (string, error) {
	_, file, line, ok := runtime.Caller(depth)
	if !ok {
		return "", errors.New("frame request out-of-bounds")
	}
	return fmt.Sprintf("%s:%d", path.Base(file), line), nil
}

// log logs the message with the specified parameters to the tLogger.
func (tl *tLogger) log(ltype logType, depth int, format string, args ...any) {
	tl.mu.Lock()
	defer tl.mu.Unlock()
	prefix, err := getCallingPrefix(callingFrame + depth)
	if err != nil {
		tl.t.Error(err)
		return
	}
	args = append([]any{ltype.String() + " " + prefix}, args...)
	args = append(args, fmt.Sprintf(" (t=+%s)", time.Since(tl.start)))

	if format == "" {
		switch ltype {
		case errorLog:
			// fmt.Sprintln is used rather than fmt.Sprint because tl.Log uses fmt.Sprintln behavior.
			if tl.expected(fmt.Sprintln(args...), errorLog) {
				tl.t.Log(args...)
			} else {
				tl.t.Error(args...)
			}
		case warningLog:
			tl.expected(fmt.Sprintln(args...), warningLog)
			tl.t.Log(args...)
		case fatalLog:
			panic(fmt.Sprint(args...))
		default:
			tl.t.Log(args...)
		}
	} else {
		// Add formatting directives for the callingPrefix and timeSuffix.
		format = "%v " + format + "%s"
		switch ltype {
		case errorLog:
			if tl.expected(fmt.Sprintf(format, args...), errorLog) {
				tl.t.Logf(format, args...)
			} else {
				tl.t.Errorf(format, args...)
			}
		case warningLog:
			tl.expected(fmt.Sprintln(args...), warningLog)
			tl.t.Log(args...)
		case fatalLog:
			panic(fmt.Sprintf(format, args...))
		default:
			tl.t.Logf(format, args...)
		}
	}
}

// update updates the testing.T that the testing logger logs to. Should be done
// before every test. It also initializes the tLogger if it has not already.
func (tl *tLogger) update(t *testing.T) {
	tl.mu.Lock()
	defer tl.mu.Unlock()
	if !tl.initialized {
		grpclog.SetLoggerV2(tl)
		tl.initialized = true
	}
	tl.t = t
	tl.start = time.Now()
	tl.logs[errorLog] = map[*regexp.Regexp]int{}
	tl.logs[warningLog] = map[*regexp.Regexp]int{}
}

// ExpectError declares an error to be expected. For the next test, the first
// error log matching the expression (using FindString) will not cause the test
// to fail. "For the next test" includes all the time until the next call to
// Update(). Note that if an expected error is not encountered, this will cause
// the test to fail.
func ExpectError(expr string) {
	expectLogsN(expr, 1, errorLog)
}

// ExpectErrorN declares an error to be expected n times.
func ExpectErrorN(expr string, n int) {
	expectLogsN(expr, n, errorLog)
}

// ExpectWarning declares a warning to be expected.
func ExpectWarning(expr string) {
	expectLogsN(expr, 1, warningLog)
}

func expectLogsN(expr string, n int, logType logType) {
	tLogr.mu.Lock()
	defer tLogr.mu.Unlock()
	re, err := regexp.Compile(expr)
	if err != nil {
		tLogr.t.Error(err)
		return
	}
	tLogr.logs[logType][re] += n
}

// endTest checks if expected errors were not encountered.
func (tl *tLogger) endTest(t *testing.T) {
	tl.mu.Lock()
	defer tl.mu.Unlock()
	for re, count := range tl.logs[errorLog] {
		if count > 0 {
			t.Errorf("Expected error '%v' not encountered", re.String())
		}
	}
	for re, count := range tl.logs[warningLog] {
		if count > 0 {
			t.Errorf("Expected warning '%v' not encountered", re.String())
		}
	}
	tl.logs[errorLog] = map[*regexp.Regexp]int{}
	tl.logs[warningLog] = map[*regexp.Regexp]int{}
}

// expected determines if the log string of the particular type is protected or
// not.
func (tl *tLogger) expected(s string, logType logType) bool {
	for re, count := range tl.logs[logType] {
		if re.FindStringIndex(s) != nil {
			tl.logs[logType][re]--
			if count <= 1 {
				delete(tl.logs[logType], re)
			}
			return true
		}
	}
	return false
}

func (tl *tLogger) Info(args ...any) {
	tl.log(infoLog, 0, "", args...)
}

func (tl *tLogger) Infoln(args ...any) {
	tl.log(infoLog, 0, "", args...)
}

func (tl *tLogger) Infof(format string, args ...any) {
	tl.log(infoLog, 0, format, args...)
}

func (tl *tLogger) InfoDepth(depth int, args ...any) {
	tl.log(infoLog, depth, "", args...)
}

func (tl *tLogger) Warning(args ...any) {
	tl.log(warningLog, 0, "", args...)
}

func (tl *tLogger) Warningln(args ...any) {
	tl.log(warningLog, 0, "", args...)
}

func (tl *tLogger) Warningf(format string, args ...any) {
	tl.log(warningLog, 0, format, args...)
}

func (tl *tLogger) WarningDepth(depth int, args ...any) {
	tl.log(warningLog, depth, "", args...)
}

func (tl *tLogger) Error(args ...any) {
	tl.log(errorLog, 0, "", args...)
}

func (tl *tLogger) Errorln(args ...any) {
	tl.log(errorLog, 0, "", args...)
}

func (tl *tLogger) Errorf(format string, args ...any) {
	tl.log(errorLog, 0, format, args...)
}

func (tl *tLogger) ErrorDepth(depth int, args ...any) {
	tl.log(errorLog, depth, "", args...)
}

func (tl *tLogger) Fatal(args ...any) {
	tl.log(fatalLog, 0, "", args...)
}

func (tl *tLogger) Fatalln(args ...any) {
	tl.log(fatalLog, 0, "", args...)
}

func (tl *tLogger) Fatalf(format string, args ...any) {
	tl.log(fatalLog, 0, format, args...)
}

func (tl *tLogger) FatalDepth(depth int, args ...any) {
	tl.log(fatalLog, depth, "", args...)
}

func (tl *tLogger) V(l int) bool {
	return l <= tl.v
}
