// Copyright 2018 The gVisor Authors.
//
// Licensed 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 p9

import (
	"fmt"
	"reflect"
	"testing"
)

func TestEncodeDecode(t *testing.T) {
	objs := []encoder{
		&QID{
			Type:    1,
			Version: 2,
			Path:    3,
		},
		&FSStat{
			Type:            1,
			BlockSize:       2,
			Blocks:          3,
			BlocksFree:      4,
			BlocksAvailable: 5,
			Files:           6,
			FilesFree:       7,
			FSID:            8,
			NameLength:      9,
		},
		&AttrMask{
			Mode:        true,
			NLink:       true,
			UID:         true,
			GID:         true,
			RDev:        true,
			ATime:       true,
			MTime:       true,
			CTime:       true,
			INo:         true,
			Size:        true,
			Blocks:      true,
			BTime:       true,
			Gen:         true,
			DataVersion: true,
		},
		&Attr{
			Mode:             Exec,
			UID:              2,
			GID:              3,
			NLink:            4,
			RDev:             5,
			Size:             6,
			BlockSize:        7,
			Blocks:           8,
			ATimeSeconds:     9,
			ATimeNanoSeconds: 10,
			MTimeSeconds:     11,
			MTimeNanoSeconds: 12,
			CTimeSeconds:     13,
			CTimeNanoSeconds: 14,
			BTimeSeconds:     15,
			BTimeNanoSeconds: 16,
			Gen:              17,
			DataVersion:      18,
		},
		&SetAttrMask{
			Permissions:        true,
			UID:                true,
			GID:                true,
			Size:               true,
			ATime:              true,
			MTime:              true,
			CTime:              true,
			ATimeNotSystemTime: true,
			MTimeNotSystemTime: true,
		},
		&SetAttr{
			Permissions:      1,
			UID:              2,
			GID:              3,
			Size:             4,
			ATimeSeconds:     5,
			ATimeNanoSeconds: 6,
			MTimeSeconds:     7,
			MTimeNanoSeconds: 8,
		},
		&Dirent{
			QID:    QID{Type: 1},
			Offset: 2,
			Type:   3,
			Name:   "a",
		},
		&Rlerror{
			Error: 1,
		},
		&Tstatfs{
			FID: 1,
		},
		&Rstatfs{
			FSStat: FSStat{Type: 1},
		},
		&Tlopen{
			FID:   1,
			Flags: WriteOnly,
		},
		&Rlopen{
			QID:    QID{Type: 1},
			IoUnit: 2,
		},
		&Tlconnect{
			FID: 1,
		},
		&Rlconnect{},
		&Tlcreate{
			FID:         1,
			Name:        "a",
			OpenFlags:   2,
			Permissions: 3,
			GID:         4,
		},
		&Rlcreate{
			Rlopen{QID: QID{Type: 1}},
		},
		&Tsymlink{
			Directory: 1,
			Name:      "a",
			Target:    "b",
			GID:       2,
		},
		&Rsymlink{
			QID: QID{Type: 1},
		},
		&Tmknod{
			Directory: 1,
			Name:      "a",
			Mode:      2,
			Major:     3,
			Minor:     4,
			GID:       5,
		},
		&Rmknod{
			QID: QID{Type: 1},
		},
		&Trename{
			FID:       1,
			Directory: 2,
			Name:      "a",
		},
		&Rrename{},
		&Treadlink{
			FID: 1,
		},
		&Rreadlink{
			Target: "a",
		},
		&Tgetattr{
			FID:      1,
			AttrMask: AttrMask{Mode: true},
		},
		&Rgetattr{
			Valid: AttrMask{Mode: true},
			QID:   QID{Type: 1},
			Attr:  Attr{Mode: Write},
		},
		&Tsetattr{
			FID:     1,
			Valid:   SetAttrMask{Permissions: true},
			SetAttr: SetAttr{Permissions: Write},
		},
		&Rsetattr{},
		&Txattrwalk{
			FID:    1,
			NewFID: 2,
			Name:   "a",
		},
		&Rxattrwalk{
			Size: 1,
		},
		&Txattrcreate{
			FID:      1,
			Name:     "a",
			AttrSize: 2,
			Flags:    3,
		},
		&Rxattrcreate{},
		&Tgetxattr{
			FID:  1,
			Name: "abc",
			Size: 2,
		},
		&Rgetxattr{
			Value: "xyz",
		},
		&Tsetxattr{
			FID:   1,
			Name:  "abc",
			Value: "xyz",
			Flags: 2,
		},
		&Rsetxattr{},
		&Treaddir{
			Directory: 1,
			Offset:    2,
			Count:     3,
		},
		&Rreaddir{
			// Count must be sufficient to encode a dirent.
			Count:   0x1a,
			Entries: []Dirent{{QID: QID{Type: 2}}},
		},
		&Tfsync{
			FID: 1,
		},
		&Rfsync{},
		&Tlink{
			Directory: 1,
			Target:    2,
			Name:      "a",
		},
		&Rlink{},
		&Tmkdir{
			Directory:   1,
			Name:        "a",
			Permissions: 2,
			GID:         3,
		},
		&Rmkdir{
			QID: QID{Type: 1},
		},
		&Trenameat{
			OldDirectory: 1,
			OldName:      "a",
			NewDirectory: 2,
			NewName:      "b",
		},
		&Rrenameat{},
		&Tunlinkat{
			Directory: 1,
			Name:      "a",
			Flags:     2,
		},
		&Runlinkat{},
		&Tversion{
			MSize:   1,
			Version: "a",
		},
		&Rversion{
			MSize:   1,
			Version: "a",
		},
		&Tauth{
			AuthenticationFID: 1,
			UserName:          "a",
			AttachName:        "b",
			UID:               2,
		},
		&Rauth{
			QID: QID{Type: 1},
		},
		&Tattach{
			FID:  1,
			Auth: Tauth{AuthenticationFID: 2},
		},
		&Rattach{
			QID: QID{Type: 1},
		},
		&Tflush{
			OldTag: 1,
		},
		&Rflush{},
		&Twalk{
			FID:    1,
			NewFID: 2,
			Names:  []string{"a"},
		},
		&Rwalk{
			QIDs: []QID{{Type: 1}},
		},
		&Tread{
			FID:    1,
			Offset: 2,
			Count:  3,
		},
		&Rread{
			Data: []byte{'a'},
		},
		&Twrite{
			FID:    1,
			Offset: 2,
			Data:   []byte{'a'},
		},
		&Rwrite{
			Count: 1,
		},
		&Tclunk{
			FID: 1,
		},
		&Rclunk{},
		&Tremove{
			FID: 1,
		},
		&Rremove{},
		&Tflushf{
			FID: 1,
		},
		&Rflushf{},
		&Twalkgetattr{
			FID:    1,
			NewFID: 2,
			Names:  []string{"a"},
		},
		&Rwalkgetattr{
			QIDs:  []QID{{Type: 1}},
			Valid: AttrMask{Mode: true},
			Attr:  Attr{Mode: Write},
		},
		&Tucreate{
			Tlcreate: Tlcreate{
				FID:         1,
				Name:        "a",
				OpenFlags:   2,
				Permissions: 3,
				GID:         4,
			},
			UID: 5,
		},
		&Rucreate{
			Rlcreate{Rlopen{QID: QID{Type: 1}}},
		},
		&Tumkdir{
			Tmkdir: Tmkdir{
				Directory:   1,
				Name:        "a",
				Permissions: 2,
				GID:         3,
			},
			UID: 4,
		},
		&Rumkdir{
			Rmkdir{QID: QID{Type: 1}},
		},
		&Tusymlink{
			Tsymlink: Tsymlink{
				Directory: 1,
				Name:      "a",
				Target:    "b",
				GID:       2,
			},
			UID: 3,
		},
		&Rusymlink{
			Rsymlink{QID: QID{Type: 1}},
		},
		&Tumknod{
			Tmknod: Tmknod{
				Directory: 1,
				Name:      "a",
				Mode:      2,
				Major:     3,
				Minor:     4,
				GID:       5,
			},
			UID: 6,
		},
		&Rumknod{
			Rmknod{QID: QID{Type: 1}},
		},
	}

	for _, enc := range objs {
		// Encode the original.
		data := make([]byte, initialBufferLength)
		buf := buffer{data: data[:0]}
		enc.encode(&buf)

		// Create a new object, same as the first.
		enc2 := reflect.New(reflect.ValueOf(enc).Elem().Type()).Interface().(encoder)
		buf2 := buffer{data: buf.data}

		// To be fair, we need to add any payloads (directly).
		if pl, ok := enc.(payloader); ok {
			enc2.(payloader).SetPayload(pl.Payload())
		}

		// And any file payloads (directly).
		if fl, ok := enc.(filer); ok {
			enc2.(filer).SetFilePayload(fl.FilePayload())
		}

		// Mark sure it was okay.
		enc2.decode(&buf2)
		if buf2.isOverrun() {
			t.Errorf("object %#v->%#v got overrun on decode", enc, enc2)
			continue
		}

		// Check that they are equal.
		if !reflect.DeepEqual(enc, enc2) {
			t.Errorf("object %#v and %#v differ", enc, enc2)
			continue
		}
	}
}

func TestMessageStrings(t *testing.T) {
	for typ := range msgRegistry.factories {
		entry := &msgRegistry.factories[typ]
		if entry.create != nil {
			name := fmt.Sprintf("%+v", typ)
			t.Run(name, func(t *testing.T) {
				defer func() { // Ensure no panic.
					if r := recover(); r != nil {
						t.Errorf("printing %s failed: %v", name, r)
					}
				}()
				m := entry.create()
				_ = fmt.Sprintf("%v", m)
				err := ErrInvalidMsgType{MsgType(typ)}
				_ = err.Error()
			})
		}
	}
}

func TestRegisterDuplicate(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			// We expect a panic.
			t.FailNow()
		}
	}()

	// Register a duplicate.
	msgRegistry.register(MsgRlerror, func() message { return &Rlerror{} })
}

func TestMsgCache(t *testing.T) {
	// Cache starts empty.
	if got, want := len(msgRegistry.factories[MsgRlerror].cache), 0; got != want {
		t.Errorf("Wrong cache size, got: %d, want: %d", got, want)
	}

	// Message can be created with an empty cache.
	msg, err := msgRegistry.get(0, MsgRlerror)
	if err != nil {
		t.Errorf("msgRegistry.get(): %v", err)
	}
	if got, want := len(msgRegistry.factories[MsgRlerror].cache), 0; got != want {
		t.Errorf("Wrong cache size, got: %d, want: %d", got, want)
	}

	// Check that message is added to the cache when returned.
	msgRegistry.put(msg)
	if got, want := len(msgRegistry.factories[MsgRlerror].cache), 1; got != want {
		t.Errorf("Wrong cache size, got: %d, want: %d", got, want)
	}

	// Check that returned message is reused.
	if got, err := msgRegistry.get(0, MsgRlerror); err != nil {
		t.Errorf("msgRegistry.get(): %v", err)
	} else if msg != got {
		t.Errorf("Message not reused, got: %d, want: %d", got, msg)
	}

	// Check that cache doesn't grow beyond max size.
	for i := 0; i < maxCacheSize+1; i++ {
		msgRegistry.put(&Rlerror{})
	}
	if got, want := len(msgRegistry.factories[MsgRlerror].cache), maxCacheSize; got != want {
		t.Errorf("Wrong cache size, got: %d, want: %d", got, want)
	}
}