package klogr

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

	"k8s.io/klog/v2"
	"k8s.io/klog/v2/internal/test/require"
	"k8s.io/klog/v2/test"

	"github.com/go-logr/logr"
)

const (
	formatDefault = "Default"
	formatNew     = "New"
)

func testOutput(t *testing.T, format string) {
	createLogger := func() logr.Logger {
		switch format {
		case formatNew:
			return New()
		case formatDefault:
			return NewWithOptions()
		default:
			return NewWithOptions(WithFormat(Format(format)))
		}
	}
	tests := map[string]struct {
		klogr              logr.Logger
		text               string
		keysAndValues      []interface{}
		err                error
		expectedOutput     string
		expectedKlogOutput string
	}{
		"should log with values passed to keysAndValues": {
			klogr:         createLogger().V(0),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue"},
			expectedOutput: `"msg"="test" "akey"="avalue"
`,
			expectedKlogOutput: `"test" akey="avalue"
`,
		},
		"should log with name and values passed to keysAndValues": {
			klogr:         createLogger().V(0).WithName("me"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue"},
			// Sorted by keys.
			expectedOutput: `"msg"="test" "akey"="avalue" "logger"="me"
`,
			// Not sorted by keys.
			expectedKlogOutput: `"test" logger="me" akey="avalue"
`,
		},
		"should log with multiple names and values passed to keysAndValues": {
			klogr:         createLogger().V(0).WithName("hello").WithName("world"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue"},
			// Sorted by keys.
			expectedOutput: `"msg"="test" "akey"="avalue" "logger"="hello.world"
`,
			// Not sorted by keys.
			expectedKlogOutput: `"test" logger="hello.world" akey="avalue"
`,
		},
		"de-duplicate keys with the same value": {
			klogr:         createLogger().V(0),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue"},
			expectedOutput: `"msg"="test" "akey"="avalue"
`,
			expectedKlogOutput: `"test" akey="avalue"
`,
		},
		"de-duplicate keys when the values are passed to Info": {
			klogr:         createLogger().V(0),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue", "akey", "avalue2"},
			expectedOutput: `"msg"="test" "akey"="avalue2"
`,
			expectedKlogOutput: `"test" akey="avalue2"
`,
		},
		"should only print the duplicate key that is passed to Info if one was passed to the logger": {
			klogr:         createLogger().WithValues("akey", "avalue"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue"},
			expectedOutput: `"msg"="test" "akey"="avalue"
`,
			expectedKlogOutput: `"test" akey="avalue"
`,
		},
		"should sort within logger and parameter key/value pairs in the default format and dump the logger pairs first": {
			klogr:         createLogger().WithValues("akey9", "avalue9", "akey8", "avalue8", "akey1", "avalue1"),
			text:          "test",
			keysAndValues: []interface{}{"akey5", "avalue5", "akey4", "avalue4"},
			expectedOutput: `"msg"="test" "akey1"="avalue1" "akey4"="avalue4" "akey5"="avalue5" "akey8"="avalue8" "akey9"="avalue9"
`,
			expectedKlogOutput: `"test" akey9="avalue9" akey8="avalue8" akey1="avalue1" akey5="avalue5" akey4="avalue4"
`,
		},
		"should only print the key passed to Info when one is already set on the logger": {
			klogr:         createLogger().WithValues("akey", "avalue"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue2"},
			expectedOutput: `"msg"="test" "akey"="avalue2"
`,
			expectedKlogOutput: `"test" akey="avalue2"
`,
		},
		"should correctly handle odd-numbers of KVs": {
			klogr:         createLogger(),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue", "akey2"},
			expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)"
`,
			expectedKlogOutput: `"test" akey="avalue" akey2="(MISSING)"
`,
		},
		"should correctly handle odd-numbers of KVs in WithValue": {
			klogr:         createLogger().WithValues("keyWithoutValue"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue", "akey2"},
			// klogr format sorts all key/value pairs.
			expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)" "keyWithoutValue"="(MISSING)"
`,
			expectedKlogOutput: `"test" keyWithoutValue="(MISSING)" akey="avalue" akey2="(MISSING)"
`,
		},
		"should correctly html characters": {
			klogr:         createLogger(),
			text:          "test",
			keysAndValues: []interface{}{"akey", "<&>"},
			expectedOutput: `"msg"="test" "akey"="<&>"
`,
			expectedKlogOutput: `"test" akey="<&>"
`,
		},
		"should correctly handle odd-numbers of KVs in both log values and Info args": {
			klogr:         createLogger().WithValues("basekey1", "basevar1", "basekey2"),
			text:          "test",
			keysAndValues: []interface{}{"akey", "avalue", "akey2"},
			// klogr format sorts all key/value pairs.
			expectedOutput: `"msg"="test" "akey"="avalue" "akey2"="(MISSING)" "basekey1"="basevar1" "basekey2"="(MISSING)"
`,
			expectedKlogOutput: `"test" basekey1="basevar1" basekey2="(MISSING)" akey="avalue" akey2="(MISSING)"
`,
		},
		"should correctly print regular error types": {
			klogr:         createLogger().V(0),
			text:          "test",
			keysAndValues: []interface{}{"err", errors.New("whoops")},
			expectedOutput: `"msg"="test" "err"="whoops"
`,
			expectedKlogOutput: `"test" err="whoops"
`,
		},
		"should use MarshalJSON in the default format if an error type implements it": {
			klogr:         createLogger().V(0),
			text:          "test",
			keysAndValues: []interface{}{"err", &customErrorJSON{"whoops"}},
			expectedOutput: `"msg"="test" "err"="WHOOPS"
`,
			expectedKlogOutput: `"test" err="whoops"
`,
		},
		"should correctly print regular error types when using logr.Error": {
			klogr: createLogger().V(0),
			text:  "test",
			err:   errors.New("whoops"),
			expectedOutput: `"msg"="test" "error"="whoops" 
`,
			expectedKlogOutput: `"test" err="whoops"
`,
		},
	}
	for n, test := range tests {
		t.Run(n, func(t *testing.T) {

			// hijack the klog output
			tmpWriteBuffer := bytes.NewBuffer(nil)
			klog.SetOutput(tmpWriteBuffer)

			if test.err != nil {
				test.klogr.Error(test.err, test.text, test.keysAndValues...)
			} else {
				test.klogr.Info(test.text, test.keysAndValues...)
			}

			// call Flush to ensure the text isn't still buffered
			klog.Flush()

			actual := tmpWriteBuffer.String()
			expectedOutput := test.expectedOutput
			if format == string(FormatKlog) || format == formatDefault {
				expectedOutput = test.expectedKlogOutput
			}
			if actual != expectedOutput {
				t.Errorf("Expected:\n%s\nActual:\n%s\n", expectedOutput, actual)
			}
		})
	}
}

func TestOutput(t *testing.T) {
	fs := test.InitKlog(t)
	require.NoError(t, fs.Set("skip_headers", "true"))

	formats := []string{
		formatNew,
		formatDefault,
		string(FormatSerialize),
		string(FormatKlog),
	}
	for _, format := range formats {
		t.Run(format, func(t *testing.T) {
			testOutput(t, format)
		})
	}
}

type customErrorJSON struct {
	s string
}

func (e *customErrorJSON) Error() string {
	return e.s
}

func (e *customErrorJSON) MarshalJSON() ([]byte, error) {
	return json.Marshal(strings.ToUpper(e.s))
}
