package protoutil

import (
	"testing"

	"github.com/emicklei/proto"
	"github.com/stretchr/testify/require"
)

const (
	world    = "World"
	elements = "Elements"
	kirby    = "Kirby"
)

// Make a simple replacement of package -> import.
func TestSimpleReplacement(t *testing.T) {
	f, err := parseStringProto(`package "package"`)
	require.NoError(t, err)
	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if _, ok := n.(*proto.Package); ok {
			imp := NewImport("that")
			c.Replace(imp)
		}

		return true
	})
	require.True(t, containsElement(f, NewImport("that")))
	require.False(t, containsElement(f, NewPackage("package")))
}

func TestSimpleInsertAfter(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	message Hello {
		message World {}
	}
	`)
	require.NoError(t, err)

	// keep ref for checking containment.
	var msg *proto.Message
	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if n, ok := n.(*proto.Message); ok {
			if n.Name == world {
				msg = NewMessage("WeComeInPeace")
				c.InsertAfter(msg)
			}
		}
		return true
	})
	require.True(t, containsElement(f, msg))
	// check that it is inserted after "World"
	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if n, ok := n.(*proto.Message); ok {
			if n.Name == world {
				next, ok := c.Next()
				require.True(t, ok)
				require.True(t, next.(*proto.Message).Name == "WeComeInPeace")
			}
		}
		return true
	})
}

// Can really only panic with comments since
// other elements in nodes aren't Visitees.
//
//nolint:dupword
func TestInsertAfterPanic(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3" 
	
	// my import
	import "this";
	`)
	require.NoError(t, err)

	// Try calling insertAfter when c is a Comment
	require.Panics(t, func() {
		Apply(f, nil, func(c *Cursor) bool {
			n := c.Node()
			if _, ok := n.(*proto.Comment); ok {
				c.InsertAfter(NewImport("that"))
			}
			return true
		})
	})
}

func TestSimpleInsertBefore(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	message Say {}
	message World {}
	`)
	require.NoError(t, err)

	// keep ref for checking containment.
	var msg *proto.Message
	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if n, ok := n.(*proto.Message); ok {
			if n.Name == world {
				// add hello between say and world
				msg = NewMessage("Hello")
				c.InsertBefore(msg)
			}
		}
		return true
	})
	require.True(t, containsElement(f, msg))

	// check that it is inserted after "Say"
	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if n, ok := n.(*proto.Message); ok {
			if n.Name == "Say" {
				next, ok := c.Next()
				require.True(t, ok)
				require.True(t, next.(*proto.Message).Name == "Hello")
			}
		}
		return true
	})
}

// Can really only panic with comments since
// other elements in nodes aren't Visitees.
//
//nolint:dupword
func TestInsertBeforePanic(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	// my import
	import "this";
	`)
	require.NoError(t, err)

	// Try calling insertAfter when c is a Comment
	require.Panics(t, func() {
		Apply(f, nil, func(c *Cursor) bool {
			n := c.Node()
			if _, ok := n.(*proto.Comment); ok {
				c.InsertBefore(NewImport("that"))
			}
			return true
		})
	})
}

// Build a skeleton of a file by continuous appends on the file.
func TestAppendFile(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"`)
	require.NoError(t, err)

	i := NewImport("importpath")
	Append(f, i)
	require.True(t, containsElement(f, i))

	p := NewPackage("package")
	Append(f, p)
	require.True(t, containsElement(f, p))

	o := NewOption("this", "that")
	Append(f, o)
	require.True(t, containsElement(f, o))

	oneofF := NewOneofField("this", "string", 2)
	// Can directly append an option if required:
	opt := NewOption("this", "that")
	Append(oneofF, opt)
	require.True(t, containsElement(oneofF, opt))

	oneof := NewOneof("myoneof")
	Append(oneof, oneofF)
	require.True(t, containsElement(oneof, oneofF))

	normalfield := NewField("that", "string", 3)

	m := NewMessage("Hello")
	Append(m, oneof)
	require.True(t, containsElement(m, oneof))
	Append(m, normalfield)
	require.True(t, containsElement(m, normalfield))

	Append(f, m)
	require.True(t, containsElement(f, m))

	// Append an empty service
	s := NewService("Hey")
	Append(f, s)
	require.True(t, containsElement(f, s))

	// An empty enum
	e := NewEnum("Hey")
	// Add an enum field to it:
	ef := NewEnumField("HEY", 1)
	Append(e, ef)
	require.True(t, containsElement(e, ef))

	Append(f, e)
	require.True(t, containsElement(f, e))
}

// Append to a node w/o elements panics.
func TestAppendEdges(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"`)
	require.NoError(t, err)

	// Can't append to a Syntax node, panic.
	require.Panics(t, func() {
		Apply(f, nil, func(c *Cursor) bool {
			n := c.Node()
			if n, ok := n.(*proto.Syntax); ok {
				Append(n, NewImport("that"))
			}
			return true
		})
	})

	// Empty append does nothing.
	elems := len(f.Elements)
	Append(f)
	require.True(t, len(f.Elements) == elems)

	// Appending a non-option to NormalField/OneOfField panics.
	require.Panics(t, func() {
		f := NewField("that", "string", 3)
		Append(f, NewImport("that"))
	})
}

func TestCursorOps(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	message Hello {}
	message World {
		message Hey {}
		enum E {}
	}
	`)
	require.NoError(t, err)

	Apply(f, nil, func(c *Cursor) bool {
		n := c.Node()
		if n, ok := n.(*proto.Message); ok {
			if n.Name == "Hello" {
				require.False(t, c.IsLast())
				n, ok := c.Next()
				require.True(t, ok)
				require.NotNil(t, n)

				parent, ok := c.Parent().(*proto.Proto)
				require.True(t, ok)
				require.True(t, parent.Filename == "")
				// currently useless.
				require.True(t, c.Name() == elements)
			}
			if n.Name == world {
				require.True(t, c.IsLast())
				n, ok := c.Next()
				require.False(t, ok)
				require.Nil(t, n)

				parent, ok := c.Parent().(*proto.Proto)
				require.True(t, ok)
				require.True(t, parent.Filename == "")
				// currently useless.
				require.True(t, c.Name() == elements)
			}

			if n.Name == "Hey" {
				require.False(t, c.IsLast())
				n, ok := c.Next()
				require.True(t, ok)
				require.NotNil(t, n)

				// parent is the message
				parent, ok := c.Parent().(*proto.Message)
				require.True(t, ok)
				require.True(t, parent.Name == "World")
				// currently useless.
				require.True(t, c.Name() == elements)
			}
		}

		if _, ok := n.(*proto.Enum); ok {
			require.True(t, c.IsLast())
			n, ok := c.Next()
			require.False(t, ok)
			require.Nil(t, n)

			// parent is the message
			parent, ok := c.Parent().(*proto.Message)
			require.True(t, ok)
			require.True(t, parent.Name == "World")
			// currently useless.
			require.True(t, c.Name() == elements)
		}

		// Don't make sense for elements not contained in a slice (currently
		// proto.Proto or comments)
		if _, ok := n.(*proto.Proto); ok {
			require.Panics(t, func() { c.IsLast() })
			require.Panics(t, func() { c.Next() })
		}
		return true
	})
}

// Also test the utilities here.

func TestAddImports(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"`)
	require.NoError(t, err)

	// Add an import
	err = AddImports(f, true, NewImport("this.proto"))
	require.NoError(t, err)
	require.True(t, HasImport(f, "this.proto"))
	// Note: added in reverse order.
	err = AddImports(f, true,
		NewImport("that.proto"),
		NewImport("the.other.proto"),
		NewImport("and.another.proto"),
	)
	require.NoError(t, err)
	require.True(t, HasImport(f, "that.proto"))
	require.True(t, HasImport(f, "the.other.proto"))
	require.True(t, HasImport(f, "and.another.proto"))

	// Empty import is no-op.
	require.NoError(t, AddImports(f, true))
	// Importing on empty file is currently an error.
	require.Error(t, AddImports(
		&proto.Proto{},
		true,
		NewImport("this.proto"),
	))

	// Exercise the recursive case:
	f, err = parseStringProto(`syntax = "proto3"`)
	require.NoError(t, err)
	err = AddImports(f, true,
		NewImport("this.proto"),
		NewImport("that.proto"),
	)
	require.NoError(t, err)
	require.True(t, HasImport(f, "this.proto"))
	require.True(t, HasImport(f, "that.proto"))

	f, err = parseStringProto(`syntax = "proto3";
package cosmonaut.chainname.chainname;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "chainname/params.proto";
`)
	require.NoError(t, err)
	err = AddImports(f, true, NewImport("chainname/bleep.proto"))
	require.NoError(t, err)
	// Add dupes:
	err = AddImports(f, true, NewImport("chainname/bleep.proto"))
	require.NoError(t, err)
	err = AddImports(f, true, NewImport("chainname/bleep.proto"))
	require.NoError(t, err)
	err = AddImports(f, true, NewImport("chainname/params.proto"))
	require.NoError(t, err)
	// just checking that is added last.
	// fmt.Print(Printer(f))

	// Check that adding duplicates does nothing.
	f, err = parseStringProto(`syntax = "proto3";
package cosmonaut.chainname.chainname;

import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "cosmos/base/query/v1beta1/pagination.proto";
import "chainname/params.proto";
`)
	require.NoError(t, err)
	imports := []*proto.Import{
		NewImport("chainname/params.proto"),
		NewImport("gogoproto/gogo.proto"),
	}
	err = AddImports(f, true, imports...)
	require.NoError(t, err)
	require.Equal(t, len(f.Elements), 6, "The number of elements shouldn't have changed")

	// Pass an empty import list.
	f, err = parseStringProto(`syntax = "proto3";`)
	require.NoError(t, err)
	ret := AddImports(f, true)
	require.Nil(t, ret)

	// No imports, no fallback.
	f, err = parseStringProto(`syntax = "proto3";`)
	require.NoError(t, err)
	err = AddImports(f, false, NewImport("this.proto"))
	require.Error(t, err)
}

func TestHasImport(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"

	import "this.proto";
	import "that.proto";
	import "the.other.proto"
	`)
	require.NoError(t, err)
	require.True(t, HasImport(f, "this.proto"))
	require.True(t, HasImport(f, "that.proto"))
	require.True(t, HasImport(f, "the.other.proto"))
	require.False(t, HasImport(f, "this.proto.proto"))
}

func TestGetMessage(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	message Hello {
		message World {
			message WeComeInPeace {
				message TheAnswerToLifeTheUniverseAndEverything {
					message IsActuallyFortyTwo {}
				}
			}
		}
	}
	`)
	require.NoError(t, err)
	m, err := GetMessageByName(f, "Hello")
	require.NoError(t, err)
	require.Equal(t, "Hello", m.Name)

	m, err = GetMessageByName(f, "World")
	require.NoError(t, err)
	require.Equal(t, "World", m.Name)

	m, err = GetMessageByName(f, "WeComeInPeace")
	require.NoError(t, err)
	require.Equal(t, "WeComeInPeace", m.Name)

	m, err = GetMessageByName(f, "TheAnswerToLifeTheUniverseAndEverything")
	require.NoError(t, err)
	require.Equal(t, "TheAnswerToLifeTheUniverseAndEverything", m.Name)

	m, err = GetMessageByName(f, "IsActuallyFortyTwo")
	require.NoError(t, err)
	require.Equal(t, "IsActuallyFortyTwo", m.Name)

	_, err = GetMessageByName(f, "DoesNotExist")
	require.Error(t, err)
}

func TestHasMessage(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	message Hello {
		message World {
			message WeComeInPeace {
				message TheAnswerToLifeTheUniverseAndEverything {
					message IsActuallyFortyTwo {}
				}
			}
		}
	}
	`)
	require.NoError(t, err)
	require.True(t, HasMessage(f, "Hello"))
	require.True(t, HasMessage(f, "World"))
	require.True(t, HasMessage(f, "WeComeInPeace"))
	require.True(t, HasMessage(f, "TheAnswerToLifeTheUniverseAndEverything"))
	require.True(t, HasMessage(f, "IsActuallyFortyTwo"))
	require.False(t, HasMessage(f, "DoesNotExist"))
	require.False(t, HasMessage(f, "Hello.World"))
}

func TestGetService(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"
	
	service Msg {
	}
	service AnotherMsg {}
	service YetAnotherMsg {
		rpc Foo(Bar) returns (Bar) {}
	}
	`)
	require.NoError(t, err)
	s, err := GetServiceByName(f, "Msg")
	require.NoError(t, err)
	require.Equal(t, "Msg", s.Name)

	s, err = GetServiceByName(f, "AnotherMsg")
	require.NoError(t, err)
	require.Equal(t, "AnotherMsg", s.Name)

	s, err = GetServiceByName(f, "YetAnotherMsg")
	require.NoError(t, err)
	require.Equal(t, "YetAnotherMsg", s.Name)

	_, err = GetServiceByName(f, "DoesNotExist")
	require.Error(t, err)
}

func TestHasService(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"

	service Msg {}
	service AnotherMsg {}
	service YetAnotherMsg {}
	`)
	require.NoError(t, err)
	require.True(t, HasService(f, "Msg"))
	require.True(t, HasService(f, "AnotherMsg"))
	require.True(t, HasService(f, "YetAnotherMsg"))
	require.False(t, HasService(f, "DoesNotExist"))
}

func TestGetNextId(t *testing.T) {
	f, err := parseStringProto(`syntax = "proto3"

	message Hello {
		string g = 1;
		message World {
			message WeComeInPeace {
				message TheAnswerToLifeTheUniverseAndEverything {
					message IsActuallyFortyTwo {
						string foo = 1;
						int32 bar = 2;
						int64 baz = 3;
					}
				}
			}
		}
	}
	`)
	require.NoError(t, err)

	m, err := GetMessageByName(f, "IsActuallyFortyTwo")
	require.NoError(t, err)
	require.Equal(t, 4, NextUniqueID(m))

	m, err = GetMessageByName(f, "Hello")
	require.NoError(t, err)
	require.Equal(t, 2, NextUniqueID(m))

	f, err = parseStringProto(`syntax = "proto3"

	message Hello {
		string g = 1;
		string foo = 2;
		int32 bar = 3;
		int64 baz = 5;
	}`)
	require.NoError(t, err)
	m, err = GetMessageByName(f, "Hello")
	require.NoError(t, err)
	require.Equal(t, 6, NextUniqueID(m))
}
