package logrus_test

import (
	"bytes"
	"encoding/json"
	"fmt"
	"sync"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	. "github.com/sirupsen/logrus"
	"github.com/sirupsen/logrus/hooks/test"
	. "github.com/sirupsen/logrus/internal/testutils"
)

type TestHook struct {
	Fired bool
}

func (hook *TestHook) Fire(entry *Entry) error {
	hook.Fired = true
	return nil
}

func (hook *TestHook) Levels() []Level {
	return []Level{
		TraceLevel,
		DebugLevel,
		InfoLevel,
		WarnLevel,
		ErrorLevel,
		FatalLevel,
		PanicLevel,
	}
}

func TestHookFires(t *testing.T) {
	hook := new(TestHook)

	LogAndAssertJSON(t, func(log *Logger) {
		log.Hooks.Add(hook)
		assert.Equal(t, hook.Fired, false)

		log.Print("test")
	}, func(fields Fields) {
		assert.Equal(t, hook.Fired, true)
	})
}

type ModifyHook struct {
}

func (hook *ModifyHook) Fire(entry *Entry) error {
	entry.Data["wow"] = "whale"
	return nil
}

func (hook *ModifyHook) Levels() []Level {
	return []Level{
		TraceLevel,
		DebugLevel,
		InfoLevel,
		WarnLevel,
		ErrorLevel,
		FatalLevel,
		PanicLevel,
	}
}

func TestHookCanModifyEntry(t *testing.T) {
	hook := new(ModifyHook)

	LogAndAssertJSON(t, func(log *Logger) {
		log.Hooks.Add(hook)
		log.WithField("wow", "elephant").Print("test")
	}, func(fields Fields) {
		assert.Equal(t, fields["wow"], "whale")
	})
}

func TestCanFireMultipleHooks(t *testing.T) {
	hook1 := new(ModifyHook)
	hook2 := new(TestHook)

	LogAndAssertJSON(t, func(log *Logger) {
		log.Hooks.Add(hook1)
		log.Hooks.Add(hook2)

		log.WithField("wow", "elephant").Print("test")
	}, func(fields Fields) {
		assert.Equal(t, fields["wow"], "whale")
		assert.Equal(t, hook2.Fired, true)
	})
}

type SingleLevelModifyHook struct {
	ModifyHook
}

func (h *SingleLevelModifyHook) Levels() []Level {
	return []Level{InfoLevel}
}

func TestHookEntryIsPristine(t *testing.T) {
	l := New()
	b := &bytes.Buffer{}
	l.Formatter = &JSONFormatter{}
	l.Out = b
	l.AddHook(&SingleLevelModifyHook{})

	l.Error("error message")
	data := map[string]string{}
	err := json.Unmarshal(b.Bytes(), &data)
	require.NoError(t, err)
	_, ok := data["wow"]
	require.False(t, ok)
	b.Reset()

	l.Info("error message")
	data = map[string]string{}
	err = json.Unmarshal(b.Bytes(), &data)
	require.NoError(t, err)
	_, ok = data["wow"]
	require.True(t, ok)
	b.Reset()

	l.Error("error message")
	data = map[string]string{}
	err = json.Unmarshal(b.Bytes(), &data)
	require.NoError(t, err)
	_, ok = data["wow"]
	require.False(t, ok)
	b.Reset()
}

type ErrorHook struct {
	Fired bool
}

func (hook *ErrorHook) Fire(entry *Entry) error {
	hook.Fired = true
	return nil
}

func (hook *ErrorHook) Levels() []Level {
	return []Level{
		ErrorLevel,
	}
}

func TestErrorHookShouldntFireOnInfo(t *testing.T) {
	hook := new(ErrorHook)

	LogAndAssertJSON(t, func(log *Logger) {
		log.Hooks.Add(hook)
		log.Info("test")
	}, func(fields Fields) {
		assert.Equal(t, hook.Fired, false)
	})
}

func TestErrorHookShouldFireOnError(t *testing.T) {
	hook := new(ErrorHook)

	LogAndAssertJSON(t, func(log *Logger) {
		log.Hooks.Add(hook)
		log.Error("test")
	}, func(fields Fields) {
		assert.Equal(t, hook.Fired, true)
	})
}

func TestAddHookRace(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(2)
	hook := new(ErrorHook)
	LogAndAssertJSON(t, func(log *Logger) {
		go func() {
			defer wg.Done()
			log.AddHook(hook)
		}()
		go func() {
			defer wg.Done()
			log.Error("test")
		}()
		wg.Wait()
	}, func(fields Fields) {
		// the line may have been logged
		// before the hook was added, so we can't
		// actually assert on the hook
	})
}

func TestAddHookRace2(t *testing.T) {
	t.Parallel()

	for i := 0; i < 3; i++ {
		testname := fmt.Sprintf("Test %d", i)
		t.Run(testname, func(t *testing.T) {
			t.Parallel()

			_ = test.NewGlobal()
			Info(testname)
		})
	}
}

type HookCallFunc struct {
	F func()
}

func (h *HookCallFunc) Levels() []Level {
	return AllLevels
}

func (h *HookCallFunc) Fire(e *Entry) error {
	h.F()
	return nil
}

func TestHookFireOrder(t *testing.T) {
	checkers := []string{}
	h := LevelHooks{}
	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "first hook") }})
	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "second hook") }})
	h.Add(&HookCallFunc{F: func() { checkers = append(checkers, "third hook") }})

	if err := h.Fire(InfoLevel, &Entry{}); err != nil {
		t.Error("unexpected error:", err)
	}
	require.Equal(t, []string{"first hook", "second hook", "third hook"}, checkers)
}
