// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// TODO: verify that the output of Marshal{Text,JSON} is suitably escaped.

package slog

import (
	"bytes"
	"strings"
	"testing"
	"time"

	"golang.org/x/exp/slog/internal/buffer"
)

func TestDefaultHandle(t *testing.T) {
	preAttrs := []Attr{Int("pre", 0)}
	attrs := []Attr{Int("a", 1), String("b", "two")}
	for _, test := range []struct {
		name  string
		with  func(Handler) Handler
		attrs []Attr
		want  string
	}{
		{
			name: "no attrs",
			want: "INFO message",
		},
		{
			name:  "attrs",
			attrs: attrs,
			want:  "INFO message a=1 b=two",
		},
		{
			name:  "preformatted",
			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs) },
			attrs: attrs,
			want:  "INFO message pre=0 a=1 b=two",
		},
		{
			name: "groups",
			attrs: []Attr{
				Int("a", 1),
				Group("g",
					Int("b", 2),
					Group("h", Int("c", 3)),
					Int("d", 4)),
				Int("e", 5),
			},
			want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
		},
		{
			name:  "group",
			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
			attrs: attrs,
			want:  "INFO message pre=0 s.a=1 s.b=two",
		},
		{
			name: "preformatted groups",
			with: func(h Handler) Handler {
				return h.WithAttrs([]Attr{Int("p1", 1)}).
					WithGroup("s1").
					WithAttrs([]Attr{Int("p2", 2)}).
					WithGroup("s2")
			},
			attrs: attrs,
			want:  "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
		},
		{
			name: "two with-groups",
			with: func(h Handler) Handler {
				return h.WithAttrs([]Attr{Int("p1", 1)}).
					WithGroup("s1").
					WithGroup("s2")
			},
			attrs: attrs,
			want:  "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			var got string
			var h Handler = newDefaultHandler(func(_ int, s string) error {
				got = s
				return nil
			})
			if test.with != nil {
				h = test.with(h)
			}
			r := NewRecord(time.Time{}, InfoLevel, "message", 0, nil)
			r.AddAttrs(test.attrs...)
			if err := h.Handle(r); err != nil {
				t.Fatal(err)
			}
			if got != test.want {
				t.Errorf("\ngot  %s\nwant %s", got, test.want)
			}
		})
	}
}

// Verify the common parts of TextHandler and JSONHandler.
func TestJSONAndTextHandlers(t *testing.T) {

	// ReplaceAttr functions

	// remove all Attrs
	removeAll := func(a Attr) Attr { return Attr{} }

	// remove the given keys
	removeKeys := func(keys ...string) func(a Attr) Attr {
		return func(a Attr) Attr {
			for _, k := range keys {
				if a.Key == k {
					return Attr{}
				}
			}
			return a
		}
	}

	attrs := []Attr{String("a", "one"), Int("b", 2), Any("", "ignore me")}
	preAttrs := []Attr{Int("pre", 3), String("x", "y")}

	for _, test := range []struct {
		name     string
		replace  func(Attr) Attr
		with     func(Handler) Handler
		preAttrs []Attr
		attrs    []Attr
		wantText string
		wantJSON string
	}{
		{
			name:     "basic",
			attrs:    attrs,
			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
		},
		{
			name:     "cap keys",
			replace:  upperCaseKey,
			attrs:    attrs,
			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
		},
		{
			name:     "remove all",
			replace:  removeAll,
			attrs:    attrs,
			wantText: "",
			wantJSON: `{}`,
		},
		{
			name:     "preformatted",
			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
			preAttrs: preAttrs,
			attrs:    attrs,
			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
		},
		{
			name:     "preformatted cap keys",
			replace:  upperCaseKey,
			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
			preAttrs: preAttrs,
			attrs:    attrs,
			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
		},
		{
			name:     "preformatted remove all",
			replace:  removeAll,
			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
			preAttrs: preAttrs,
			attrs:    attrs,
			wantText: "",
			wantJSON: "{}",
		},
		{
			name:     "remove built-in",
			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
			attrs:    attrs,
			wantText: "a=one b=2",
			wantJSON: `{"a":"one","b":2}`,
		},
		{
			name:     "preformatted remove built-in",
			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
			attrs:    attrs,
			wantText: "pre=3 x=y a=one b=2",
			wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
		},
		{
			name:    "groups",
			replace: removeKeys(TimeKey, LevelKey), // to simplify the result
			attrs: []Attr{
				Int("a", 1),
				Group("g",
					Int("b", 2),
					Group("h", Int("c", 3)),
					Int("d", 4)),
				Int("e", 5),
			},
			wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
			wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
		},
		{
			name:     "empty group",
			replace:  removeKeys(TimeKey, LevelKey),
			attrs:    []Attr{Group("g"), Group("h", Int("a", 1))},
			wantText: "msg=message h.a=1",
			wantJSON: `{"msg":"message","g":{},"h":{"a":1}}`,
		},
		{
			name:    "escapes",
			replace: removeKeys(TimeKey, LevelKey),
			attrs: []Attr{
				String("a b", "x\t\n\000y"),
				Group(" b.c=\"\\x2E\t",
					String("d=e", "f.g\""),
					Int("m.d", 1)), // dot is not escaped
			},
			wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
			wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
		},
		{
			name:    "LogValuer",
			replace: removeKeys(TimeKey, LevelKey),
			attrs: []Attr{
				Int("a", 1),
				Any("name", logValueName{"Ren", "Hoek"}),
				Int("b", 2),
			},
			wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
			wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
		},
		{
			name:     "with-group",
			replace:  removeKeys(TimeKey, LevelKey),
			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
			attrs:    attrs,
			wantText: "msg=message pre=3 x=y s.a=one s.b=2",
			wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
		},
		{
			name:    "preformatted with-groups",
			replace: removeKeys(TimeKey, LevelKey),
			with: func(h Handler) Handler {
				return h.WithAttrs([]Attr{Int("p1", 1)}).
					WithGroup("s1").
					WithAttrs([]Attr{Int("p2", 2)}).
					WithGroup("s2")
			},
			attrs:    attrs,
			wantText: "msg=message p1=1 s1.p2=2 s1.s2.a=one s1.s2.b=2",
			wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"a":"one","b":2}}}`,
		},
		{
			name:    "two with-groups",
			replace: removeKeys(TimeKey, LevelKey),
			with: func(h Handler) Handler {
				return h.WithAttrs([]Attr{Int("p1", 1)}).
					WithGroup("s1").
					WithGroup("s2")
			},
			attrs:    attrs,
			wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
			wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
		},
	} {
		r := NewRecord(testTime, InfoLevel, "message", 1, nil)
		r.AddAttrs(test.attrs...)
		var buf bytes.Buffer
		opts := HandlerOptions{ReplaceAttr: test.replace}
		t.Run(test.name, func(t *testing.T) {
			for _, handler := range []struct {
				name string
				h    Handler
				want string
			}{
				{"text", opts.NewTextHandler(&buf), test.wantText},
				{"json", opts.NewJSONHandler(&buf), test.wantJSON},
			} {
				t.Run(handler.name, func(t *testing.T) {
					h := handler.h
					if test.with != nil {
						h = test.with(h)
					}
					buf.Reset()
					if err := h.Handle(r); err != nil {
						t.Fatal(err)
					}
					got := strings.TrimSuffix(buf.String(), "\n")
					if got != handler.want {
						t.Errorf("\ngot  %s\nwant %s\n", got, handler.want)
					}
				})
			}
		})
	}
}

func upperCaseKey(a Attr) Attr {
	a.Key = strings.ToUpper(a.Key)
	return a
}

type logValueName struct {
	first, last string
}

func (n logValueName) LogValue() Value {
	return GroupValue(
		String("first", n.first),
		String("last", n.last))
}

func TestHandlerEnabled(t *testing.T) {
	atomicLevel := func(l Level) *AtomicLevel {
		var al AtomicLevel
		al.Set(l)
		return &al
	}

	for _, test := range []struct {
		leveler Leveler
		want    bool
	}{
		{nil, true},
		{WarnLevel, false},
		{&AtomicLevel{}, true}, // defaults to Info
		{atomicLevel(WarnLevel), false},
		{DebugLevel, true},
		{atomicLevel(DebugLevel), true},
	} {
		h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
		got := h.enabled(InfoLevel)
		if got != test.want {
			t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
		}
	}
}

func TestAppendSource(t *testing.T) {
	for _, test := range []struct {
		file               string
		wantText, wantJSON string
	}{
		{"a/b.go", "a/b.go:1", `"a/b.go:1"`},
		{"a b.go", `"a b.go:1"`, `"a b.go:1"`},
		{`C:\windows\b.go`, `C:\windows\b.go:1`, `"C:\\windows\\b.go:1"`},
	} {
		check := func(json bool, want string) {
			t.Helper()
			var buf []byte
			state := handleState{
				h:   &commonHandler{json: json},
				buf: (*buffer.Buffer)(&buf),
			}
			state.appendSource(test.file, 1)
			got := string(buf)
			if got != want {
				t.Errorf("%s, json=%t:\ngot  %s\nwant %s", test.file, json, got, want)
			}
		}
		check(false, test.wantText)
		check(true, test.wantJSON)
	}
}

const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"

func TestWriteTimeRFC3339(t *testing.T) {
	for _, tm := range []time.Time{
		time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
		time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
		time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
	} {
		want := tm.Format(rfc3339Millis)
		buf := buffer.New()
		defer buf.Free()
		writeTimeRFC3339Millis(buf, tm)
		got := buf.String()
		if got != want {
			t.Errorf("got %s, want %s", got, want)
		}
	}
}

func BenchmarkWriteTime(b *testing.B) {
	buf := buffer.New()
	defer buf.Free()
	tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		writeTimeRFC3339Millis(buf, tm)
		buf.Reset()
	}
}
