package jsontext_test

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

	"github.com/quic-go/quic-go/qlogwriter/jsontext"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestEncoderSimpleObject(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.String("foo"))
	enc.WriteToken(jsontext.String("bar"))
	enc.WriteToken(jsontext.String("foo2"))
	enc.WriteToken(jsontext.String("bar2"))
	enc.WriteToken(jsontext.EndObject)
	output := buf.String()

	var got map[string]string
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, map[string]string{"foo": "bar", "foo2": "bar2"}, got)
}

func TestEncoderArrayInts(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginArray)
	enc.WriteToken(jsontext.Int(1))
	enc.WriteToken(jsontext.Int(2))
	enc.WriteToken(jsontext.Int(3))
	enc.WriteToken(jsontext.EndArray)
	output := buf.String()

	var got []int
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, []int{1, 2, 3}, got)
}

func TestEncoderArrayStrings(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginArray)
	enc.WriteToken(jsontext.String("one"))
	enc.WriteToken(jsontext.String("two"))
	enc.WriteToken(jsontext.EndArray)
	output := buf.String()

	var got []string
	err := json.Unmarshal([]byte(output), &got)
	require.NoError(t, err)
	require.Equal(t, []string{"one", "two"}, got)
}

func TestEncoderNestedObject(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.String("outer"))
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.String("inner"))
	enc.WriteToken(jsontext.String("value"))
	enc.WriteToken(jsontext.EndObject)
	enc.WriteToken(jsontext.EndObject)
	output := buf.String()

	var got map[string]map[string]string
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, map[string]map[string]string{"outer": {"inner": "value"}}, got)
}

func TestEncoderNumbersAndBool(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.String("int"))
	enc.WriteToken(jsontext.Int(42))
	enc.WriteToken(jsontext.String("uint"))
	enc.WriteToken(jsontext.Uint(100))
	enc.WriteToken(jsontext.String("float"))
	enc.WriteToken(jsontext.Float(3.14))
	enc.WriteToken(jsontext.String("true"))
	enc.WriteToken(jsontext.True)
	enc.WriteToken(jsontext.String("false"))
	enc.WriteToken(jsontext.False)
	enc.WriteToken(jsontext.String("nullv"))
	enc.WriteToken(jsontext.Null)
	enc.WriteToken(jsontext.EndObject)
	output := buf.String()

	var got map[string]any
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, map[string]any{
		"int":   float64(42), // json.Unmarshal decodes numbers as float64
		"uint":  float64(100),
		"float": 3.14,
		"true":  true,
		"false": false,
		"nullv": nil,
	}, got)
}

func TestEncoderEmptyObject(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.EndObject)
	output := buf.String()

	var got map[string]any
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, map[string]any{}, got)
}

func TestEncoderEmptyArray(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginArray)
	enc.WriteToken(jsontext.EndArray)
	output := buf.String()

	var got []any
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, []any{}, got)
}

func TestEncoderArrayWithNulls(t *testing.T) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginArray)
	enc.WriteToken(jsontext.Null)
	enc.WriteToken(jsontext.String("x"))
	enc.WriteToken(jsontext.Null)
	enc.WriteToken(jsontext.EndArray)
	output := buf.String()

	var got []any
	require.NoError(t, json.Unmarshal([]byte(output), &got))
	require.Equal(t, []any{nil, "x", nil}, got)
}

func TestEncoderEscapedStrings(t *testing.T) {
	t.Run("no escapes", func(t *testing.T) {
		testEncoderEscapedStrings(t, "simplekey", "simplevalue")
	})

	t.Run("basic escapes", func(t *testing.T) {
		key := `key"\/`
		value := `value"\/`
		testEncoderEscapedStrings(t, key, value)
	})

	t.Run("control characters", func(t *testing.T) {
		key := "key\b\f\n\r\t"
		value := "value\b\f\n\r\t"
		testEncoderEscapedStrings(t, key, value)
	})

	t.Run("unicode low", func(t *testing.T) {
		key := "key\u0007\u001f"
		value := "value\u0007\u001f"
		testEncoderEscapedStrings(t, key, value)
	})

	t.Run("mixed all", func(t *testing.T) {
		key := `key"\\\/\b\f\n\r\t\u0007\u001f`
		value := `value"\\\/\b\f\n\r\t\u0007\u001f`
		testEncoderEscapedStrings(t, key, value)
	})
}

func testEncoderEscapedStrings(t *testing.T, key, value string) {
	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	enc.WriteToken(jsontext.BeginObject)
	enc.WriteToken(jsontext.String(key))
	enc.WriteToken(jsontext.String(value))
	enc.WriteToken(jsontext.EndObject)
	output := buf.String()

	var got map[string]string
	err := json.Unmarshal([]byte(output), &got)
	require.NoError(t, err)
	expected := map[string]string{key: value}
	require.Equal(t, expected, got)
}

func encodeValue(t testing.TB, enc *jsontext.Encoder, v any) (isSupported bool) {
	t.Helper()

	switch val := v.(type) {
	case map[string]any:
		require.NoError(t, enc.WriteToken(jsontext.BeginObject))
		for k, vv := range val {
			require.NoError(t, enc.WriteToken(jsontext.String(k)))
			if !encodeValue(t, enc, vv) {
				return false
			}
		}
		require.NoError(t, enc.WriteToken(jsontext.EndObject))
		return true
	case []any:
		require.NoError(t, enc.WriteToken(jsontext.BeginArray))
		for _, vv := range val {
			if !encodeValue(t, enc, vv) {
				return false // Propagate unsupported if any nested value fails
			}
		}
		require.NoError(t, enc.WriteToken(jsontext.EndArray))
		return true
	case string:
		require.NoError(t, enc.WriteToken(jsontext.String(val)))
		return true
	case int64:
		require.NoError(t, enc.WriteToken(jsontext.Int(val)))
		return true
	case uint64:
		require.NoError(t, enc.WriteToken(jsontext.Uint(val)))
		return true
	case float64:
		require.NoError(t, enc.WriteToken(jsontext.Float(val)))
		return true
	case bool:
		require.NoError(t, enc.WriteToken(jsontext.Bool(val)))
		return true
	case nil:
		require.NoError(t, enc.WriteToken(jsontext.Null))
		return true
	default:
		return false
	}
}

type errorWriter struct {
	N int
}

func (w *errorWriter) Write(p []byte) (int, error) {
	n := min(len(p), w.N)
	w.N -= n
	if w.N <= 0 {
		return n, assert.AnError
	}
	return n, nil
}

func TestEncoderComprehensive(t *testing.T) {
	// encodes an object with all token types and nested structures
	encode := func(enc *jsontext.Encoder) error {
		if err := enc.WriteToken(jsontext.BeginObject); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.String("simple")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("value")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("escaped")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String(`"quoted\"string"`)); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.String("int")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.Int(-42)); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("uint")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.Uint(100)); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("float")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.Float(3.14)); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.String("true")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.True); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("false")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.False); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.String("array")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.BeginArray); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.String("item1")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.Int(1)); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.EndArray); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.String("nested")); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.BeginObject); err != nil {
			return err
		}
		if err := enc.WriteToken(jsontext.EndObject); err != nil {
			return err
		}

		if err := enc.WriteToken(jsontext.EndObject); err != nil {
			return err
		}
		return nil
	}

	buf := bytes.NewBuffer(nil)
	enc := jsontext.NewEncoder(buf)
	require.NoError(t, encode(enc))

	for i := range buf.Len() {
		enc := jsontext.NewEncoder(&errorWriter{N: i})
		require.ErrorIs(t, encode(enc), assert.AnError)
	}
}

func FuzzEncoder(f *testing.F) {
	examples := []string{
		`{"hello": "world"}`,
		`{"foo": 123, "bar": [1, 2, 3]}`,
		`{"nested": {"a": 1, "b": [true, false, "foobar", null]}}`,
		`[{"x": 1}, {"y": "foo"}]`,
		`["foo", "bar"]`,
		`["a", {"b": [1, 2, {"c": "d"}]}, 3]`,
		`{"emptyObj": {}, "emptyArr": []}`,
		`{"mixed": [1, "two", {"three": 3}]}`,
		`[null]`,
	}
	for _, tc := range examples {
		// first test that
		// 1. it's valid JSON
		d := json.NewDecoder(bytes.NewReader([]byte(tc)))
		var expected any
		require.NoError(f, d.Decode(&expected), "corpus entry `%s` is not valid JSON", tc)
		// 2. the jsontext encoder can handle
		enc := jsontext.NewEncoder(&bytes.Buffer{})
		require.True(f, encodeValue(f, enc, expected), "expected `%s` to be supported", tc)

		f.Add([]byte(tc))
	}

	var stdlibBuf, ourBuf bytes.Buffer

	f.Fuzz(func(t *testing.T, b []byte) {
		stdlibBuf.Truncate(0)
		ourBuf.Truncate(0)
		stdlibBuf.Grow(len(b))
		ourBuf.Grow(len(b))

		d := json.NewDecoder(bytes.NewReader(b))
		var expected any
		if err := d.Decode(&expected); err != nil {
			return // invalid JSON
		}

		// only attempt to handle inputs that the standard library can handle
		stdlibEnc := json.NewEncoder(&stdlibBuf)
		require.NoError(t, stdlibEnc.Encode(expected))
		if !json.Valid(stdlibBuf.Bytes()) {
			return
		}

		// then encode using the jsontext encoder
		enc := jsontext.NewEncoder(&ourBuf)
		if isSupported := encodeValue(t, enc, expected); !isSupported {
			return
		}

		output := ourBuf.Bytes()
		require.Truef(t, json.Valid(output), "produced invalid JSON: %s", output)

		var got any
		require.NoError(t, json.Unmarshal(output, &got))
		require.JSONEq(t, ourBuf.String(), stdlibBuf.String())
	})
}
