// Copyright 2023 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 qlog

import (
	"bytes"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"sync"
	"testing"
	"time"
)

type testJSONOut struct {
	bytes.Buffer
}

func (o *testJSONOut) Close() error { return nil }

func newTestJSONWriter() *jsonWriter {
	return &jsonWriter{w: &testJSONOut{}}
}

func wantJSONRecord(t *testing.T, w *jsonWriter, want string) {
	t.Helper()
	want = "\x1e" + want + "\n"
	got := w.w.(*testJSONOut).String()
	if got != want {
		t.Errorf("jsonWriter contains unexpected output\ngot:  %q\nwant: %q", got, want)
	}
}

func TestJSONWriterWriteConcurrentRecords(t *testing.T) {
	w := newTestJSONWriter()
	var wg sync.WaitGroup
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			w.writeRecordStart()
			w.writeInt64Field("field", 0)
			w.writeRecordEnd()
		}()
	}
	wg.Wait()
	wantJSONRecord(t, w, strings.Join([]string{
		`{"field":0}`,
		`{"field":0}`,
		`{"field":0}`,
	}, "\n\x1e"))
}

func TestJSONWriterAttrs(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeAttrsField("field", []slog.Attr{
		slog.Any("any", errors.New("value")),
		slog.Bool("bool", true),
		slog.Duration("duration", 1*time.Second),
		slog.Float64("float64", 1),
		slog.Int64("int64", 1),
		slog.String("string", "value"),
		slog.Time("time", time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)),
		slog.Uint64("uint64", 1),
		slog.Group("group", "a", 1),
	})
	w.writeRecordEnd()
	wantJSONRecord(t, w,
		`{"field":{`+
			`"any":"value",`+
			`"bool":true,`+
			`"duration":1000.000000,`+
			`"float64":1,`+
			`"int64":1,`+
			`"string":"value",`+
			`"time":946684800000.000000,`+
			`"uint64":1,`+
			`"group":{"a":1}`+
			`}}`)
}

func TestJSONWriterAttrEmpty(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	var a slog.Attr
	w.writeAttr(a)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{}`)
}

func TestJSONWriterObjectEmpty(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeObjectField("field", func() {})
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":{}}`)
}

func TestJSONWriterObjectFields(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeObjectField("field", func() {
		w.writeStringField("a", "value")
		w.writeInt64Field("b", 10)
	})
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":{"a":"value","b":10}}`)
}

func TestJSONWriterRawField(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeRawField("field", `[1]`)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":[1]}`)
}

func TestJSONWriterBoolField(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeBoolField("true", true)
	w.writeBoolField("false", false)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"true":true,"false":false}`)
}

func TestJSONWriterDurationField(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeDurationField("field1", (10*time.Millisecond)+(2*time.Nanosecond))
	w.writeDurationField("field2", -((10 * time.Millisecond) + (2 * time.Nanosecond)))
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field1":10.000002,"field2":-10.000002}`)
}

func TestJSONWriterFloat64Field(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeFloat64Field("field", 1.1)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":1.1}`)
}

func TestJSONWriterInt64Field(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeInt64Field("field", 1234)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":1234}`)
}

func TestJSONWriterUint64Field(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeUint64Field("field", 1234)
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":1234}`)
}

func TestJSONWriterStringField(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeStringField("field", "value")
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":"value"}`)
}

func TestJSONWriterStringFieldEscaped(t *testing.T) {
	w := newTestJSONWriter()
	w.writeRecordStart()
	w.writeStringField("field", "va\x00ue")
	w.writeRecordEnd()
	wantJSONRecord(t, w, `{"field":"va\u0000ue"}`)
}

func TestJSONWriterStringEscaping(t *testing.T) {
	for c := 0; c <= 0xff; c++ {
		w := newTestJSONWriter()
		w.writeRecordStart()
		w.writeStringField("field", string([]byte{byte(c)}))
		w.writeRecordEnd()
		var want string
		if (c >= 0x20 && c <= 0x21) || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) {
			want = fmt.Sprintf(`%c`, c)
		} else {
			want = fmt.Sprintf(`\u%04x`, c)
		}
		wantJSONRecord(t, w, `{"field":"`+want+`"}`)
	}
}
