package mock

import (
	"text/template"

	pgs "github.com/lyft/protoc-gen-star"
	pgsgo "github.com/lyft/protoc-gen-star/lang/go"
)

var mockServerTmpl = template.Must(template.New("mockServerTmpl").Parse(mockServer))

// MockServerModule generates a mock server implementation to be used for
// stubbing GRPC servers in tests.
type MockServerModule struct {
	*pgs.ModuleBase
	ctx pgsgo.Context
}

// MockServer returns an initialized MockServerModule
func MockServer() *MockServerModule {
	return &MockServerModule{ModuleBase: &pgs.ModuleBase{}}
} // nolint: govet

func (m *MockServerModule) InitContext(c pgs.BuildContext) {
	m.ModuleBase.InitContext(c)
	m.ctx = pgsgo.InitContext(c.Parameters())
}

// Name satisfies the pgs.Module interface
func (m *MockServerModule) Name() string { return "mock" }

// Execute satisfies the pgs.Module interface
func (m *MockServerModule) Execute(targets map[string]pgs.File, pkgs map[string]pgs.Package) []pgs.Artifact {
	for _, f := range targets {
		// skip files with no services; also assume there isn't more than ONE
		// service in each proto file (FIXME?)
		if n := len(f.Services()); n != 1 {
			m.Debugf("%d service(s) in %v, skipping", n, f.Name())
			continue
		}

		s := f.Services()[0]
		m.Push(s.Name().String())

		m.Debugf("processing service %v", s.Name())
		name := m.ctx.OutputPath(f).SetExt(".mock.go")
		m.AddGeneratorTemplateFile(
			name.String(),
			mockServerTmpl,
			struct {
				SourceFile, MockName, ServiceName, PkgName string
				Methods                                    []method
				Imports                                    []goimport
			}{
				f.File().Name().String(),
				s.Name().String() + "ServerMock",
				s.Name().String() + "Server",
				m.ctx.PackageName(f).String(),
				m.methodsForTemplate(s, s.Methods()),
				m.collectImportPackages(s, s.Methods()),
			})
		m.Pop()
	}

	return m.Artifacts()
}

// Do some data processing here, so the template can be simpler

// collectImportPackages iterates over all the used messages (input/output) of
// the service, and collects those that come from a different go package (such
// as ptypes/empty, ptypes/timestamp, etc). Since we don't care of a message's
// fields, these can be LESS than f.Imports(), which gives all the imports of
// the proto file under consideration.
func (m *MockServerModule) collectImportPackages(this pgs.Service, meths []pgs.Method) []goimport {
	importPackages := []goimport{}
	for _, meth := range meths {
		if m.ctx.ImportPath(meth.Input()) != m.ctx.ImportPath(this) {
			importPackages = append(importPackages, goimport{
				ImportPath: m.ctx.ImportPath(meth.Input()).String(),
				GoName:     m.ctx.PackageName(meth.Input()).String(),
			})
		}
		if m.ctx.ImportPath(meth.Output()) != m.ctx.ImportPath(this) {
			importPackages = append(importPackages, goimport{
				ImportPath: m.ctx.ImportPath(meth.Output()).String(),
				GoName:     m.ctx.PackageName(meth.Output()).String(),
			})
		}
	}
	return importPackages
}

func (m *MockServerModule) methodsForTemplate(this pgs.Service, ms []pgs.Method) []method {
	ret := []method{}
	for _, meth := range ms {
		ret = append(ret, method{
			Name:   meth.Name().String(),
			Input:  m.maybePrefixType(this, meth.Input()),
			Output: m.maybePrefixType(this, meth.Output()),
		})
	}
	return ret
}

// If the message type is defined in the same Go package, we don't need to
// prefix it with the package.
func (m *MockServerModule) maybePrefixType(this pgs.Service, msg pgs.Message) string {

	if m.ctx.ImportPath(msg) == m.ctx.ImportPath(this) {
		return m.ctx.Name(msg).String()
	}
	return m.ctx.PackageName(msg).String() + "." + m.ctx.Name(msg).String()
}

type method struct {
	Name, Input, Output string
}

type goimport struct {
	GoName, ImportPath string
}

// https://golang.org/pkg/text/template/ is the canonical, albeit not too good,
// source for this templating language
const mockServer = `
// Code generated by protoc-gen-grpc-mock. DO NOT EDIT.
// source: {{ .SourceFile }}

package {{ .PkgName }}

import (
	"context"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	{{- range .Imports }}
	{{ .GoName }} "{{ .ImportPath }}"
	{{- end }}
)

// verify that the mock satisfies the {{ .ServiceName }} interface (at compile time)
var _ {{ .ServiceName }} = &{{ .MockName }}{}

// New{{ .MockName }} gives you a fresh instance of {{ .MockName }}.
func New{{ .MockName }}() *{{ .MockName }} {
	return &{{ .MockName }}{validateRequests: true}
}

// New{{ .MockName }}WithoutValidation gives you a fresh instance of
// {{ .MockName }} which does not attempt to validate requests before passing
// them to their respective '*Func'.
func New{{ .MockName }}WithoutValidation() *{{ .MockName }} {
	return &{{ .MockName }}{}
}

// {{ .MockName }} is the mock-what-you-want struct that stubs all not-overridden
// methods with "not implemented" returns
type {{ .MockName }} struct {
	validateRequests bool
	{{- range .Methods }}
	{{ .Name }}Func func(context.Context, *{{ .Input }}) (*{{ .Output }}, error)
	{{- end }}
}
{{ $mockName := .MockName }}
{{- range .Methods }}

func (m *{{ $mockName }}) {{ .Name }}(ctx context.Context, req *{{ .Input }}) (*{{ .Output }}, error) {
	if msg, ok := interface{}(req).(interface { Validate() error }); m.validateRequests && ok {
		if err := msg.Validate(); err != nil {
			return nil, status.Error(codes.InvalidArgument, err.Error())
		}
	}
	if f := m.{{ .Name }}Func; f != nil {
		return f(ctx, req)
	}
	return nil, status.Error(codes.Internal, "mock: '{{ .Name }}' not implemented")
}
{{- end }}

// Reset resets all overridden functions
func (m *{{ .MockName }}) Reset() {
{{- range .Methods }}
	m.{{ .Name }}Func = nil
{{- end }}
}
`

var _ pgs.Module = (*MockServerModule)(nil)
