package logrus_test

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"runtime"
	"strings"
	"testing"

	"github.com/sirupsen/logrus"
)

func formatJSONEntry(t *testing.T, formatter *logrus.JSONFormatter, fields logrus.Fields) map[string]any {
	t.Helper()

	var buf bytes.Buffer

	logger := logrus.New()
	logger.SetOutput(&buf)
	logger.SetFormatter(formatter)

	logger.WithFields(fields).Info("")

	entry := make(map[string]any)
	if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
		t.Fatal("Unable to unmarshal formatted entry: ", err)
	}
	return entry
}

func TestErrorNotLost(t *testing.T) {
	entry := formatJSONEntry(t, &logrus.JSONFormatter{}, logrus.Fields{
		"error": errors.New("wild walrus"),
	})

	if entry["error"] != "wild walrus" {
		t.Fatal("Error field not set")
	}
}

func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
	entry := formatJSONEntry(t, &logrus.JSONFormatter{}, logrus.Fields{
		"omg": errors.New("wild walrus"),
	})

	if entry["omg"] != "wild walrus" {
		t.Fatal("Error field not set")
	}
}

func TestFieldClashWithTime(t *testing.T) {
	entry := formatJSONEntry(t, &logrus.JSONFormatter{}, logrus.Fields{
		"time": "right now!",
	})

	if entry["fields.time"] != "right now!" {
		t.Fatal("fields.time not set to original time field")
	}

	timeVal, ok := entry["time"].(string)
	if !ok || timeVal == "" || timeVal == "right now!" {
		t.Fatal("time field not set to Entry time")
	}
}

func TestFieldClashWithMsg(t *testing.T) {
	entry := formatJSONEntry(t, &logrus.JSONFormatter{}, logrus.Fields{
		"msg": "something",
	})

	if entry["fields.msg"] != "something" {
		t.Fatal("fields.msg not set to original msg field")
	}
}

func TestFieldClashWithLevel(t *testing.T) {
	entry := formatJSONEntry(t, &logrus.JSONFormatter{}, logrus.Fields{
		"level": "something",
	})

	if entry["fields.level"] != "something" {
		t.Fatal("fields.level not set to original level field")
	}
}

func TestFieldClashWithRemappedFields(t *testing.T) {
	formatter := &logrus.JSONFormatter{
		FieldMap: logrus.FieldMap{
			logrus.FieldKeyTime:  "@timestamp",
			logrus.FieldKeyLevel: "@level",
			logrus.FieldKeyMsg:   "@message",
		},
	}
	entry := formatJSONEntry(t, formatter, logrus.Fields{
		"@timestamp": "@timestamp",
		"@level":     "@level",
		"@message":   "@message",
		"timestamp":  "timestamp",
		"level":      "level",
		"msg":        "msg",
	})

	for _, field := range []string{"timestamp", "level", "msg"} {
		if entry[field] != field {
			t.Errorf("Expected field %v to be untouched; got %v", field, entry[field])
		}

		remappedKey := fmt.Sprintf("fields.%s", field)
		if remapped, ok := entry[remappedKey]; ok {
			t.Errorf("Expected %s to be empty; got %v", remappedKey, remapped)
		}
	}

	for _, field := range []string{"@timestamp", "@level", "@message"} {
		if entry[field] == field {
			t.Errorf("Expected field %v to be mapped to an Entry value", field)
		}

		remappedKey := fmt.Sprintf("fields.%s", field)
		if remapped, ok := entry[remappedKey]; ok {
			if remapped != field {
				t.Errorf("Expected field %v to be copied to %s; got %v", field, remappedKey, remapped)
			}
		} else {
			t.Errorf("Expected field %v to be copied to %s; was absent", field, remappedKey)
		}
	}
}

func TestFieldsInNestedDictionary(t *testing.T) {
	var buf bytes.Buffer

	logger := logrus.New()
	logger.SetOutput(&buf)
	logger.SetFormatter(&logrus.JSONFormatter{
		DataKey: "args",
	})

	logEntry := logger.WithFields(logrus.Fields{
		"level": "level",
		"test":  "test",
	})
	logEntry.Info("test")

	b := buf.Bytes()
	entry := make(map[string]any)
	err := json.Unmarshal(b, &entry)
	if err != nil {
		t.Fatal("Unable to unmarshal formatted entry: ", err)
	}

	args := entry["args"].(map[string]any)

	for _, field := range []string{"test", "level"} {
		if value, present := args[field]; !present || value != field {
			t.Errorf("Expected field %v to be present under 'args'; untouched", field)
		}
	}

	for _, field := range []string{"test", "fields.level"} {
		if _, present := entry[field]; present {
			t.Errorf("Expected field %v not to be present at top level", field)
		}
	}

	// with nested object, "level" shouldn't clash
	if entry["level"] != "info" {
		t.Errorf("Expected 'level' field to contain 'info'")
	}
}

func TestJSONEntryFieldValueError(t *testing.T) {
	t.Run("good value", func(t *testing.T) {
		var buf bytes.Buffer
		l := logrus.New()
		l.SetOutput(&buf)
		l.SetFormatter(&logrus.JSONFormatter{DisableTimestamp: true})

		l.WithField("ok", "ok").Info("test")

		var data map[string]any
		err := json.Unmarshal(buf.Bytes(), &data)
		if err != nil {
			t.Fatal("Unable to unmarshal formatted entry: ", err)
		}

		if _, ok := data[logrus.FieldKeyLogrusError]; ok {
			t.Errorf(`Unexpected "logrus_error" field in log entry: %v`, data)
		}
		if v, ok := data["ok"]; !ok || v != "ok" {
			t.Errorf(`Expected log entry to contain "ok"="ok": %v`, data)
		}
	})

	// If we dropped an unsupported field, the FieldKeyLogrusError should
	// contain a message that we did.
	t.Run("bad and good value", func(t *testing.T) {
		var buf bytes.Buffer
		l := logrus.New()
		l.SetOutput(&buf)
		l.SetFormatter(&logrus.JSONFormatter{DisableTimestamp: true})

		l.WithField("func", func() {}).WithField("ok", "ok").Info("test")

		var data map[string]any
		if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
			t.Fatal("Unable to unmarshal formatted entry: ", err)
		}

		if v, ok := data[logrus.FieldKeyLogrusError]; !ok || v == "" {
			t.Errorf(`Expected log entry to contain a "logrus_error" field: %v`, data)
		}
		if _, ok := data["func"]; ok {
			t.Errorf(`Expected "func" field to be removed from log entry: %v`, data)
		}
		if v, ok := data["ok"]; !ok || v != "ok" {
			t.Errorf(`Expected log entry to contain "ok=ok": %v`, data)
		}
	})

	// This is testing the current behavior; error is preserved, even if an
	// unsupported value was dropped and replaced with a supported value for
	// the same field.
	t.Run("replace bad value", func(t *testing.T) {
		var buf bytes.Buffer
		l := logrus.New()
		l.SetOutput(&buf)
		l.SetFormatter(&logrus.JSONFormatter{DisableTimestamp: true})

		l.WithField("func", func() {}).WithField("ok", "ok").WithField("func", "not-a-func").Info("test")

		var data map[string]any
		if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
			t.Fatal("Unable to unmarshal formatted entry: ", err)
		}

		if v, ok := data[logrus.FieldKeyLogrusError]; !ok || v == "" {
			t.Errorf(`Expected log entry to contain a "logrus_error" field: %v`, data)
		}
		if v, ok := data["func"]; !ok || v != "not-a-func" {
			t.Errorf(`Expected log entry to contain "func=not-a-func": %v`, data)
		}
		if v, ok := data["ok"]; !ok || v != "ok" {
			t.Errorf(`Expected log entry to contain "ok=ok": %v`, data)
		}
	})
}

func TestJSONEntryEndsWithNewline(t *testing.T) {
	var buf bytes.Buffer

	logger := logrus.New()
	logger.SetOutput(&buf)
	logger.SetFormatter(&logrus.JSONFormatter{})

	logger.WithField("level", "something").Info("")

	b := buf.Bytes()
	if b[len(b)-1] != '\n' {
		t.Fatal("Expected JSON log entry to end with a newline")
	}
}

func TestJSONMessageKey(t *testing.T) {
	formatter := &logrus.JSONFormatter{
		FieldMap: logrus.FieldMap{
			logrus.FieldKeyMsg: "message",
		},
	}

	b, err := formatter.Format(&logrus.Entry{Message: "oh hai"})
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, `"message":"oh hai"`) {
		t.Fatal("Expected JSON to format message key")
	}
}

func TestJSONLevelKey(t *testing.T) {
	formatter := &logrus.JSONFormatter{
		FieldMap: logrus.FieldMap{
			logrus.FieldKeyLevel: "somelevel",
		},
	}

	b, err := formatter.Format(logrus.WithField("level", "something"))
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, "somelevel") {
		t.Fatal("Expected JSON to format level key")
	}
}

func TestJSONTimeKey(t *testing.T) {
	formatter := &logrus.JSONFormatter{
		FieldMap: logrus.FieldMap{
			logrus.FieldKeyTime: "timeywimey",
		},
	}

	b, err := formatter.Format(logrus.WithField("level", "something"))
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, "timeywimey") {
		t.Fatal("Expected JSON to format time key")
	}
}

func TestFieldDoesNotClashWithCaller(t *testing.T) {
	var buf bytes.Buffer

	logger := logrus.New()
	logger.SetOutput(&buf)
	logger.SetFormatter(&logrus.JSONFormatter{})
	logger.SetReportCaller(false)

	logger.WithField("func", "howdy pardner").Info("")

	var entry map[string]any
	if err := json.Unmarshal(buf.Bytes(), &entry); err != nil {
		t.Fatal("Unable to unmarshal formatted entry: ", err)
	}

	if entry["func"] != "howdy pardner" {
		t.Fatal("func field replaced when ReportCaller=false")
	}
}

func TestFieldClashWithCaller(t *testing.T) {
	formatter := &logrus.JSONFormatter{}

	b, err := formatter.Format(&logrus.Entry{
		Data: logrus.Fields{
			"func": "howdy pardner",
		},
		Caller: &runtime.Frame{
			Function: "somefunc",
		},
	})
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}

	entry := make(map[string]any)
	if err := json.Unmarshal(b, &entry); err != nil {
		t.Fatal("Unable to unmarshal formatted entry: ", err)
	}

	if entry["fields.func"] != "howdy pardner" {
		t.Fatalf("fields.func not set to original func field when ReportCaller=true (got '%s')",
			entry["fields.func"])
	}

	if entry["func"] != "somefunc" {
		t.Fatalf("func not set as expected when ReportCaller=true (got '%s')",
			entry["func"])
	}
}

func TestJSONDisableTimestamp(t *testing.T) {
	formatter := &logrus.JSONFormatter{
		DisableTimestamp: true,
	}

	b, err := formatter.Format(logrus.WithField("level", "something"))
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if strings.Contains(s, logrus.FieldKeyTime) {
		t.Error("Did not prevent timestamp", s)
	}
}

func TestJSONEnableTimestamp(t *testing.T) {
	formatter := &logrus.JSONFormatter{}

	b, err := formatter.Format(logrus.WithField("level", "something"))
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, logrus.FieldKeyTime) {
		t.Error("Timestamp not present", s)
	}
}

func TestJSONDisableHTMLEscape(t *testing.T) {
	formatter := &logrus.JSONFormatter{DisableHTMLEscape: true}

	b, err := formatter.Format(&logrus.Entry{Message: "& < >"})
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, "& < >") {
		t.Error("Message should not be HTML escaped", s)
	}
}

func TestJSONEnableHTMLEscape(t *testing.T) {
	formatter := &logrus.JSONFormatter{}

	b, err := formatter.Format(&logrus.Entry{Message: "& < >"})
	if err != nil {
		t.Fatal("Unable to format entry: ", err)
	}
	s := string(b)
	if !strings.Contains(s, `\u0026 \u003c \u003e`) {
		t.Error("Message should be HTML escaped", s)
	}
}
