// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package sftest

import (
	"encoding/json"
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"

	structform "github.com/elastic/go-structform"
)

type Recording []Record

type Record interface {
	Replay(v structform.ExtVisitor) error
}

type NilRec struct{}
type BoolRec struct{ Value bool }
type StringRec struct{ Value string }
type IntRec struct{ Value int }
type Int8Rec struct{ Value int8 }
type Int16Rec struct{ Value int16 }
type Int32Rec struct{ Value int32 }
type Int64Rec struct{ Value int64 }
type UintRec struct{ Value uint }
type ByteRec struct{ Value byte }
type Uint8Rec struct{ Value uint8 }
type Uint16Rec struct{ Value uint16 }
type Uint32Rec struct{ Value uint32 }
type Uint64Rec struct{ Value uint64 }
type Float32Rec struct{ Value float32 }
type Float64Rec struct{ Value float64 }

// extended (yet) non-recordible records
type Int8ArrRec struct{ Value []int8 }
type StringArrRec struct{ Value []string }
type StringObjRec struct{ Value map[string]string }
type UintObjRec struct{ Value map[string]uint }

type ObjectStartRec struct {
	Len int
	T   structform.BaseType
}
type ObjectFinishRec struct{}
type ObjectKeyRec struct{ Value string }

type ArrayStartRec struct {
	Len int
	T   structform.BaseType
}
type ArrayFinishRec struct{}

func (NilRec) Replay(vs structform.ExtVisitor) error            { return vs.OnNil() }
func (r BoolRec) Replay(vs structform.ExtVisitor) error         { return vs.OnBool(r.Value) }
func (r StringRec) Replay(vs structform.ExtVisitor) error       { return vs.OnString(r.Value) }
func (r IntRec) Replay(vs structform.ExtVisitor) error          { return vs.OnInt(r.Value) }
func (r Int8Rec) Replay(vs structform.ExtVisitor) error         { return vs.OnInt8(r.Value) }
func (r Int16Rec) Replay(vs structform.ExtVisitor) error        { return vs.OnInt16(r.Value) }
func (r Int32Rec) Replay(vs structform.ExtVisitor) error        { return vs.OnInt32(r.Value) }
func (r Int64Rec) Replay(vs structform.ExtVisitor) error        { return vs.OnInt64(r.Value) }
func (r UintRec) Replay(vs structform.ExtVisitor) error         { return vs.OnUint(r.Value) }
func (r ByteRec) Replay(vs structform.ExtVisitor) error         { return vs.OnByte(r.Value) }
func (r Uint8Rec) Replay(vs structform.ExtVisitor) error        { return vs.OnUint8(r.Value) }
func (r Uint16Rec) Replay(vs structform.ExtVisitor) error       { return vs.OnUint16(r.Value) }
func (r Uint32Rec) Replay(vs structform.ExtVisitor) error       { return vs.OnUint32(r.Value) }
func (r Uint64Rec) Replay(vs structform.ExtVisitor) error       { return vs.OnUint64(r.Value) }
func (r Float32Rec) Replay(vs structform.ExtVisitor) error      { return vs.OnFloat32(r.Value) }
func (r Float64Rec) Replay(vs structform.ExtVisitor) error      { return vs.OnFloat64(r.Value) }
func (r ObjectStartRec) Replay(vs structform.ExtVisitor) error  { return vs.OnObjectStart(r.Len, r.T) }
func (r ObjectFinishRec) Replay(vs structform.ExtVisitor) error { return vs.OnObjectFinished() }
func (r ObjectKeyRec) Replay(vs structform.ExtVisitor) error    { return vs.OnKey(r.Value) }
func (r ArrayStartRec) Replay(vs structform.ExtVisitor) error   { return vs.OnArrayStart(r.Len, r.T) }
func (r ArrayFinishRec) Replay(vs structform.ExtVisitor) error  { return vs.OnArrayFinished() }

func (r Int8ArrRec) Replay(vs structform.ExtVisitor) error   { return vs.OnInt8Array(r.Value) }
func (r StringArrRec) Replay(vs structform.ExtVisitor) error { return vs.OnStringArray(r.Value) }
func (r UintObjRec) Replay(vs structform.ExtVisitor) error   { return vs.OnUintObject(r.Value) }
func (r StringObjRec) Replay(vs structform.ExtVisitor) error { return vs.OnStringObject(r.Value) }

func (rec *Recording) Replay(vs structform.Visitor) error {
	evs := structform.EnsureExtVisitor(vs)
	for _, r := range *rec {
		if err := r.Replay(evs); err != nil {
			return err
		}
	}
	return nil
}

func (r *Recording) add(v Record) error {
	*r = append(*r, v)
	return nil
}

func (r *Recording) OnNil() error              { return r.add(NilRec{}) }
func (r *Recording) OnBool(b bool) error       { return r.add(BoolRec{b}) }
func (r *Recording) OnString(s string) error   { return r.add(StringRec{s}) }
func (r *Recording) OnInt8(i int8) error       { return r.add(Int8Rec{i}) }
func (r *Recording) OnInt16(i int16) error     { return r.add(Int16Rec{i}) }
func (r *Recording) OnInt32(i int32) error     { return r.add(Int32Rec{i}) }
func (r *Recording) OnInt64(i int64) error     { return r.add(Int64Rec{i}) }
func (r *Recording) OnInt(i int) error         { return r.add(IntRec{i}) }
func (r *Recording) OnUint8(i uint8) error     { return r.add(Uint8Rec{i}) }
func (r *Recording) OnByte(i byte) error       { return r.add(ByteRec{i}) }
func (r *Recording) OnUint16(i uint16) error   { return r.add(Uint16Rec{i}) }
func (r *Recording) OnUint32(i uint32) error   { return r.add(Uint32Rec{i}) }
func (r *Recording) OnUint64(i uint64) error   { return r.add(Uint64Rec{i}) }
func (r *Recording) OnUint(i uint) error       { return r.add(UintRec{i}) }
func (r *Recording) OnFloat32(i float32) error { return r.add(Float32Rec{i}) }
func (r *Recording) OnFloat64(i float64) error { return r.add(Float64Rec{i}) }

func (r *Recording) OnArrayStart(len int, baseType structform.BaseType) error {
	return r.add(ArrayStartRec{len, baseType})
}
func (r *Recording) OnArrayFinished() error {
	return r.add(ArrayFinishRec{})
}

func (r *Recording) OnObjectStart(len int, baseType structform.BaseType) error {
	return r.add(ObjectStartRec{len, baseType})
}
func (r *Recording) OnObjectFinished() error {
	return r.add(ObjectFinishRec{})
}
func (r *Recording) OnKey(s string) error {
	return r.add(ObjectKeyRec{s})
}

func (r *Recording) expand() Recording {
	var to Recording
	r.Replay(&to)
	return to
}

func (r Recording) Assert(t *testing.T, expected Recording) {
	exp, err := expected.ToJSON()
	if err != nil {
		t.Error("Assert (expected): ", err)
		t.Logf("  recording: %#v", exp)
		return
	}

	act, err := r.ToJSON()
	if err != nil {
		t.Error("Assert (actual): ", err)
		t.Logf("  recording: %#v", r)
		return
	}

	assert.Equal(t, exp, act)
}

func (r Recording) ToJSON() (string, error) {
	v, _, err := buildValue(r.expand())
	if err != nil {
		return "", err
	}

	b, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return "", err
	}

	return string(b), nil
}

type builder struct {
	stack []interface{}
	value interface{}
}

func buildValue(rec Recording) (interface{}, Recording, error) {
	if len(rec) == 0 {
		return nil, nil, errors.New("empty recording")
	}

	switch v := rec[0].(type) {
	case NilRec:
		return nil, rec[1:], nil
	case BoolRec:
		return v.Value, rec[1:], nil
	case StringRec:
		return v.Value, rec[1:], nil
	case IntRec:
		return v.Value, rec[1:], nil
	case Int8Rec:
		return v.Value, rec[1:], nil
	case Int16Rec:
		return v.Value, rec[1:], nil
	case Int32Rec:
		return v.Value, rec[1:], nil
	case Int64Rec:
		return v.Value, rec[1:], nil
	case UintRec:
		return v.Value, rec[1:], nil
	case ByteRec:
		return v.Value, rec[1:], nil
	case Uint8Rec:
		return v.Value, rec[1:], nil
	case Uint16Rec:
		return v.Value, rec[1:], nil
	case Uint32Rec:
		return v.Value, rec[1:], nil
	case Uint64Rec:
		return v.Value, rec[1:], nil
	case Float32Rec:
		return v.Value, rec[1:], nil
	case Float64Rec:
		return v.Value, rec[1:], nil
	case ArrayStartRec:
		return buildArray(rec[1:])
	case ObjectStartRec:
		return buildObject(rec[1:])

	default:
		return nil, nil, fmt.Errorf("Invalid record entry: %v", v)
	}
}

func buildArray(rec Recording) (interface{}, Recording, error) {
	a := []interface{}{}

	for len(rec) > 0 {
		var (
			v   interface{}
			err error
		)

		if _, end := rec[0].(ArrayFinishRec); end {
			return a, rec[1:], nil
		}

		v, rec, err = buildValue(rec)
		if err != nil {
			return nil, nil, err
		}

		a = append(a, v)
	}

	return nil, nil, errors.New("missing array finish record")
}

func buildObject(rec Recording) (interface{}, Recording, error) {
	obj := map[string]interface{}{}

	for len(rec) > 0 {
		var (
			key string
			v   interface{}
			err error
		)

		switch v := rec[0].(type) {
		case ObjectFinishRec:
			return obj, rec[1:], nil
		case ObjectKeyRec:
			key = v.Value
		}

		v, rec, err = buildValue(rec[1:])
		if err != nil {
			return nil, nil, err
		}

		obj[key] = v
	}

	return nil, nil, errors.New("missing object finish record")
}

func TestEncodeParseConsistent(
	t *testing.T,
	samples []Recording,
	constr func() (structform.Visitor, func(structform.Visitor) error),
) {
	for i, sample := range samples {
		sample := sample
		expected, err := sample.ToJSON()
		if err != nil {
			panic(err)
		}

		title := fmt.Sprintf("test %v: %#v => %v", i, sample, expected)
		t.Run(title, func(t *testing.T) {
			t.Parallel()

			enc, dec := constr()

			err = sample.Replay(enc)
			if err != nil {
				t.Errorf("Failed to encode %#v with %v", sample, err)
				return
			}

			var target Recording
			err = dec(&target)
			if err != nil {
				t.Errorf("Failed to decode %#v with %v", target, err)
				t.Logf("  recording: %#v", target)
			}

			target.Assert(t, sample)
		})
	}
}

func concatSamples(recs ...[]Recording) []Recording {
	var out []Recording
	for _, r := range recs {
		out = append(out, r...)
	}
	return out
}
