// 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.

package slog

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"internal/testenv"
	"io"
	"strings"
	"testing"
	"time"
)

var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)

func TestTextHandler(t *testing.T) {
	for _, test := range []struct {
		name             string
		attr             Attr
		wantKey, wantVal string
	}{
		{
			"unquoted",
			Int("a", 1),
			"a", "1",
		},
		{
			"quoted",
			String("x = y", `qu"o`),
			`"x = y"`, `"qu\"o"`,
		},
		{
			"String method",
			Any("name", name{"Ren", "Hoek"}),
			`name`, `"Hoek, Ren"`,
		},
		{
			"struct",
			Any("x", &struct{ A, b int }{A: 1, b: 2}),
			`x`, `"&{A:1 b:2}"`,
		},
		{
			"TextMarshaler",
			Any("t", text{"abc"}),
			`t`, `"text{\"abc\"}"`,
		},
		{
			"TextMarshaler error",
			Any("t", text{""}),
			`t`, `"!ERROR:text: empty string"`,
		},
		{
			"TextAppender",
			Any("t", textAppend{"abc"}),
			`t`, `"textAppend{\"abc\"}"`,
		},
		{
			"TextAppender error",
			Any("t", textAppend{""}),
			`t`, `"!ERROR:textAppend: empty string"`,
		},
		{
			"nil value",
			Any("a", nil),
			`a`, `<nil>`,
		},
	} {
		t.Run(test.name, func(t *testing.T) {
			for _, opts := range []struct {
				name       string
				opts       HandlerOptions
				wantPrefix string
				modKey     func(string) string
			}{
				{
					"none",
					HandlerOptions{},
					`time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`,
					func(s string) string { return s },
				},
				{
					"replace",
					HandlerOptions{ReplaceAttr: upperCaseKey},
					`TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`,
					strings.ToUpper,
				},
			} {
				t.Run(opts.name, func(t *testing.T) {
					var buf bytes.Buffer
					h := NewTextHandler(&buf, &opts.opts)
					r := NewRecord(testTime, LevelInfo, "a message", 0)
					r.AddAttrs(test.attr)
					if err := h.Handle(context.Background(), r); err != nil {
						t.Fatal(err)
					}
					got := buf.String()
					// Remove final newline.
					got = got[:len(got)-1]
					want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal
					if got != want {
						t.Errorf("\ngot  %s\nwant %s", got, want)
					}
				})
			}
		})
	}
}

// for testing fmt.Sprint
type name struct {
	First, Last string
}

func (n name) String() string { return n.Last + ", " + n.First }

// for testing TextMarshaler
type text struct {
	s string
}

func (t text) String() string { return t.s } // should be ignored

func (t text) MarshalText() ([]byte, error) {
	if t.s == "" {
		return nil, errors.New("text: empty string")
	}
	return []byte(fmt.Sprintf("text{%q}", t.s)), nil
}

// for testing TextAppender
type textAppend struct {
	s string
}

func (t textAppend) String() string               { return t.s }
func (t textAppend) MarshalText() ([]byte, error) { return []byte(t.s), nil }
func (t textAppend) AppendText(b []byte) ([]byte, error) {
	if t.s == "" {
		return nil, errors.New("textAppend: empty string")
	}
	b = append(b, `textAppend{"`...)
	b = append(b, t.s...)
	b = append(b, `"}`...)
	return b, nil
}

func TestTextHandlerPreformatted(t *testing.T) {
	var buf bytes.Buffer
	var h Handler = NewTextHandler(&buf, nil)
	h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)})
	// Also test omitting time.
	r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0)
	r.AddAttrs(Int("a", 1))
	if err := h.Handle(context.Background(), r); err != nil {
		t.Fatal(err)
	}
	got := strings.TrimSuffix(buf.String(), "\n")
	want := `level=INFO msg=m dur=1m0s b=true a=1`
	if got != want {
		t.Errorf("got %s, want %s", got, want)
	}
}

func TestTextHandlerAlloc(t *testing.T) {
	testenv.SkipIfOptimizationOff(t)
	r := NewRecord(time.Now(), LevelInfo, "msg", 0)
	for i := 0; i < 10; i++ {
		r.AddAttrs(Int("x = y", i))
	}
	var h Handler = NewTextHandler(io.Discard, nil)
	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })

	h = h.WithGroup("s")
	r.AddAttrs(Group("g", Int("a", 1)))
	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
}

func TestNeedsQuoting(t *testing.T) {
	for _, test := range []struct {
		in   string
		want bool
	}{
		{"", true},
		{"ab", false},
		{"a=b", true},
		{`"ab"`, true},
		{"\a\b", true},
		{"a\tb", true},
		{"µåπ", false},
		{"a b", true},
		{"badutf8\xF6", true},
	} {
		got := needsQuoting(test.in)
		if got != test.want {
			t.Errorf("%q: got %t, want %t", test.in, got, test.want)
		}
	}
}
