package log

import (
	"bytes"
	"errors"
	"io"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"testing"

	"github.com/thrasher-corp/gocryptotrader/common/convert"
)

func TestMain(m *testing.M) {
	err := setupTestLoggers()
	if err != nil {
		log.Fatal("cannot set up test loggers", err)
	}
	tempDir, err := ioutil.TempDir(os.TempDir(), "")
	if err != nil {
		log.Fatal("Cannot create temporary file", err)
	}
	log.Println("temp dir created at:", tempDir)
	LogPath = tempDir
	r := m.Run()
	err = os.Remove(tempDir)
	if err != nil {
		log.Println("failed to remove temp file:", tempDir)
	}
	os.Exit(r)
}

func setupTestLoggers() error {
	logTest := Config{
		Enabled: convert.BoolPtr(true),
		SubLoggerConfig: SubLoggerConfig{
			Output: "console",
			Level:  "INFO|WARN|DEBUG|ERROR",
		},
		AdvancedSettings: advancedSettings{
			ShowLogSystemName: convert.BoolPtr(true),
			Spacer:            " | ",
			TimeStampFormat:   timestampFormat,
			Headers: headers{
				Info:  "[INFO]",
				Warn:  "[WARN]",
				Debug: "[DEBUG]",
				Error: "[ERROR]",
			},
		},
		SubLoggers: []SubLoggerConfig{
			{
				Name:   "lOg",
				Level:  "INFO|DEBUG|WARN|ERROR",
				Output: "stdout",
			}},
	}
	RWM.Lock()
	GlobalLogConfig = &logTest
	RWM.Unlock()
	if err := SetupGlobalLogger(); err != nil {
		return err
	}
	return SetupSubLoggers(logTest.SubLoggers)
}

func SetupDisabled() error {
	logTest := Config{
		Enabled: convert.BoolPtr(false),
	}
	RWM.Lock()
	GlobalLogConfig = &logTest
	RWM.Unlock()

	if err := SetupGlobalLogger(); err != nil {
		return err
	}
	return SetupSubLoggers(logTest.SubLoggers)
}

func BenchmarkInfo(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		Info(Global, "Hello this is an info benchmark")
	}
}

func TestAddWriter(t *testing.T) {
	t.Parallel()
	_, err := MultiWriter(ioutil.Discard, ioutil.Discard)
	if !errors.Is(err, errWriterAlreadyLoaded) {
		t.Fatalf("received: '%v' but expected: '%v'", err, errWriterAlreadyLoaded)
	}

	mw, err := MultiWriter()
	if !errors.Is(err, nil) {
		t.Fatalf("received: '%v' but expected: '%v'", err, nil)
	}
	err = mw.Add(ioutil.Discard)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Add(os.Stdin)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Add(os.Stdout)
	if err != nil {
		t.Fatal(err)
	}

	if total := len(mw.writers); total != 3 {
		t.Errorf("expected m.Writers to be 3 %v", total)
	}
}

func TestRemoveWriter(t *testing.T) {
	t.Parallel()
	mw, err := MultiWriter()
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Add(ioutil.Discard)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Add(os.Stdin)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Add(os.Stdout)
	if err != nil {
		t.Fatal(err)
	}
	total := len(mw.writers)
	err = mw.Remove(os.Stdin)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Remove(os.Stdout)
	if err != nil {
		t.Fatal(err)
	}
	err = mw.Remove(&bytes.Buffer{})
	if !errors.Is(err, errWriterNotFound) {
		t.Fatalf("received: '%v' but expected: '%v'", err, errWriterNotFound)
	}

	if len(mw.writers) != total-2 {
		t.Errorf("expected m.Writers to be %v got %v", total-2, len(mw.writers))
	}
}

type WriteShorter struct{}

func (w *WriteShorter) Write(p []byte) (int, error) {
	return 1, nil
}

type WriteError struct{}

func (w *WriteError) Write(p []byte) (int, error) {
	return 0, errWriteError
}

var errWriteError = errors.New("write error")

func TestMultiWriterWrite(t *testing.T) {
	t.Parallel()
	mw, err := MultiWriter(ioutil.Discard, &bytes.Buffer{})
	if err != nil {
		t.Fatal(err)
	}

	payload := "woooooooooooooooooooooooooooooooooooow"
	l, err := mw.Write([]byte(payload))
	if err != nil {
		t.Fatal(err)
	}
	if l != len(payload) {
		t.Fatal("unexpected return")
	}

	mw, err = MultiWriter(&WriteShorter{}, ioutil.Discard)
	if err != nil {
		t.Fatal(err)
	}
	_, err = mw.Write([]byte(payload))
	if !errors.Is(err, io.ErrShortWrite) {
		t.Fatalf("received: '%v' but expected: '%v'", err, io.ErrShortWrite)
	}

	mw, err = MultiWriter(&WriteError{}, ioutil.Discard)
	if err != nil {
		t.Fatal(err)
	}
	_, err = mw.Write([]byte(payload))
	if !errors.Is(err, errWriteError) {
		t.Fatalf("received: '%v' but expected: '%v'", err, errWriteError)
	}
}

func TestGetWriters(t *testing.T) {
	t.Parallel()
	_, err := getWriters(nil)
	if !errors.Is(err, errSubloggerConfigIsNil) {
		t.Fatalf("received: '%v' but expected: '%v'", err, errSubloggerConfigIsNil)
	}

	outputWriters := "stDout|stderr|filE"

	_, err = getWriters(&SubLoggerConfig{Output: outputWriters})
	if !errors.Is(err, nil) {
		t.Fatalf("received: '%v' but expected: '%v'", err, nil)
	}

	outputWriters = "stdout|stderr|file|noobs"
	_, err = getWriters(&SubLoggerConfig{Output: outputWriters})
	if !errors.Is(err, errUnhandledOutputWriter) {
		t.Fatalf("received: '%v' but expected: '%v'", err, errUnhandledOutputWriter)
	}
}

func TestGenDefaultSettings(t *testing.T) {
	t.Parallel()
	if cfg := GenDefaultSettings(); cfg.Enabled == nil {
		t.Fatal("unexpected items in struct")
	}
}

func TestLevel(t *testing.T) {
	t.Parallel()
	_, err := Level("LOG")
	if err != nil {
		t.Errorf("Failed to get log %s levels skipping", err)
	}

	_, err = Level("totallyinvalidlogger")
	if err == nil {
		t.Error("Expected error on invalid logger")
	}
}

func TestSetLevel(t *testing.T) {
	t.Parallel()
	newLevel, err := SetLevel("LOG", "ERROR")
	if err != nil {
		t.Skipf("Failed to get log %s levels skipping", err)
	}

	if newLevel.Info || newLevel.Debug || newLevel.Warn {
		t.Error("failed to set level correctly")
	}

	if !newLevel.Error {
		t.Error("failed to set level correctly")
	}

	_, err = SetLevel("abc12345556665", "ERROR")
	if err == nil {
		t.Error("SetLevel() Should return error on invalid logger")
	}
}

func TestCloseLogger(t *testing.T) {
	t.Parallel()
	if err := CloseLogger(); err != nil {
		t.Errorf("CloseLogger() failed %v", err)
	}
}

func TestConfigureSubLogger(t *testing.T) {
	t.Parallel()
	err := configureSubLogger("LOG", "INFO", os.Stdin)
	if err != nil {
		t.Skipf("configureSubLogger() returned unexpected error %v", err)
	}
	levels := Global.GetLevels()
	if (levels != Levels{Info: true, Debug: false}) {
		t.Error("configureSubLogger() incorrectly configure subLogger")
	}
	if Global.name != "LOG" {
		t.Error("configureSubLogger() Failed to uppercase name")
	}
}

func TestSplitLevel(t *testing.T) {
	t.Parallel()
	levelsInfoDebug := splitLevel("INFO|DEBUG")

	expected := Levels{
		Info:  true,
		Debug: true,
		Warn:  false,
		Error: false,
	}

	if levelsInfoDebug != expected {
		t.Errorf("splitLevel() returned invalid data expected: %+v got: %+v", expected, levelsInfoDebug)
	}
}

func BenchmarkInfoDisabled(b *testing.B) {
	if err := SetupDisabled(); err != nil {
		b.Fatal(err)
	}

	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		Info(Global, "Hello this is an info benchmark")
	}
}

func BenchmarkInfof(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		Infof(Global, "Hello this is an infof benchmark %v %v %v\n", n, 1, 2)
	}
}

func BenchmarkInfoln(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		Infoln(Global, "Hello this is an infoln benchmark")
	}
}

func TestNewLogEvent(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}
	RWM.Lock()
	err := logger.newLogEvent("out", "header", "SUBLOGGER", w)
	RWM.Unlock()
	if err != nil {
		t.Fatal(err)
	}

	if w.String() == "" {
		t.Error("newLogEvent() failed expected output got empty string")
	}

	RWM.Lock()
	err = logger.newLogEvent("out", "header", "SUBLOGGER", nil)
	RWM.Unlock()
	if err == nil {
		t.Error("Error expected with output is set to nil")
	}
}

func TestInfo(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}

	sl := registerNewSubLogger("TESTYMCTESTALOTINFO")
	sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
	sl.SetOutput(w)

	Info(sl, "Hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Infof(sl, "%s", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Infoln(sl, "hello", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	_, err := SetLevel("TESTYMCTESTALOTINFO", "")
	if err != nil {
		t.Fatal(err)
	}

	Info(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}

	Infoln(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}
}

func TestDebug(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}

	sl := registerNewSubLogger("TESTYMCTESTALOTDEBUG")
	sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
	sl.SetOutput(w)

	Debug(sl, "Hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Debugf(sl, "%s", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Debugln(sl, "hello", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	_, err := SetLevel("TESTYMCTESTALOTDEBUG", "")
	if err != nil {
		t.Fatal(err)
	}

	Debug(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}

	Debugln(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}
}

func TestWarn(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}

	sl := registerNewSubLogger("TESTYMCTESTALOTWARN")
	sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
	sl.SetOutput(w)

	Warn(sl, "Hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Warnf(sl, "%s", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Warnln(sl, "hello", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	_, err := SetLevel("TESTYMCTESTALOTWARN", "")
	if err != nil {
		t.Fatal(err)
	}

	Warn(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}

	Warnln(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}
}

func TestError(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}

	sl := registerNewSubLogger("TESTYMCTESTALOTERROR")
	sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
	sl.SetOutput(w)

	Error(sl, "Hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Errorf(sl, "%s", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	Errorln(sl, "hello", "hello")

	if w.String() == "" {
		t.Error("expected Info() to write output to buffer")
	}
	w.Reset()

	_, err := SetLevel("TESTYMCTESTALOTERROR", "")
	if err != nil {
		t.Fatal(err)
	}

	Error(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}

	Errorln(sl, "HelloHello")

	if w.String() != "" {
		t.Error("Expected output buffer to be empty but wrote to output", w.String())
	}
}

func TestSubLoggerName(t *testing.T) {
	t.Parallel()
	w := &bytes.Buffer{}
	registerNewSubLogger("sublogger")
	RWM.Lock()
	err := logger.newLogEvent("out", "header", "SUBLOGGER", w)
	RWM.Unlock()
	if err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(w.String(), "SUBLOGGER") {
		t.Error("Expected SUBLOGGER in output")
	}

	RWM.Lock()
	logger.ShowLogSystemName = false
	RWM.Unlock()
	w.Reset()
	RWM.Lock()
	err = logger.newLogEvent("out", "header", "SUBLOGGER", w)
	RWM.Unlock()
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(w.String(), "SUBLOGGER") {
		t.Error("Unexpected SUBLOGGER in output")
	}
}

func TestNewSubLogger(t *testing.T) {
	t.Parallel()
	_, err := NewSubLogger("")
	if !errors.Is(err, errEmptyLoggerName) {
		t.Fatalf("received: %v but expected: %v", err, errEmptyLoggerName)
	}

	sl, err := NewSubLogger("TESTERINOS")
	if !errors.Is(err, nil) {
		t.Fatalf("received: %v but expected: %v", err, nil)
	}

	Debug(sl, "testerinos")

	_, err = NewSubLogger("TESTERINOS")
	if !errors.Is(err, errSubLoggerAlreadyregistered) {
		t.Fatalf("received: %v but expected: %v", err, errSubLoggerAlreadyregistered)
	}
}

func BenchmarkNewLogEvent(b *testing.B) {
	var bro bytes.Buffer
	l := Logger{Spacer: " "}
	for i := 0; i < b.N; i++ {
		_ = l.newLogEvent("somedata", "header", "sublog", &bro)
	}
}

func TestRotateWrite(t *testing.T) {
	t.Parallel()
	empty := Rotate{Rotate: convert.BoolPtr(true), FileName: "test.txt"}
	payload := make([]byte, defaultMaxSize*megabyte+1)
	_, err := empty.Write(payload)
	if !errors.Is(err, errExceedsMaxFileSize) {
		t.Fatalf("received: %v but expected: %v", err, errExceedsMaxFileSize)
	}

	empty.MaxSize = 1
	payload = make([]byte, 1*megabyte+1)
	_, err = empty.Write(payload)
	if !errors.Is(err, errExceedsMaxFileSize) {
		t.Fatalf("received: %v but expected: %v", err, errExceedsMaxFileSize)
	}

	// test write
	payload = make([]byte, 1*megabyte-1)
	_, err = empty.Write(payload)
	if !errors.Is(err, nil) {
		t.Fatalf("received: %v but expected: %v", err, nil)
	}

	// test rotate
	payload = make([]byte, 1*megabyte)
	_, err = empty.Write(payload)
	if !errors.Is(err, nil) {
		t.Fatalf("received: %v but expected: %v", err, nil)
	}
}

func TestOpenNew(t *testing.T) {
	t.Parallel()
	empty := Rotate{}
	err := empty.openNew()
	if !errors.Is(err, errFileNameIsEmpty) {
		t.Fatalf("received: %v but expected: %v", err, errFileNameIsEmpty)
	}

	empty.FileName = "wow.txt"
	err = empty.openNew()
	if !errors.Is(err, nil) {
		t.Fatalf("received: %v but expected: %v", err, nil)
	}
}
