/*
Copyright (c) 2017 VMware, Inc. All Rights Reserved.

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 toolbox

import (
	"bytes"
	"context"
	"errors"
	"flag"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"sync"
	"testing"
	"time"

	"github.com/vmware/govmomi/toolbox/hgfs"
	"github.com/vmware/govmomi/toolbox/vix"
	"github.com/vmware/govmomi/vim25/types"
)

func TestDefaultIP(t *testing.T) {
	ip := DefaultIP()
	if ip == "" {
		t.Error("failed to get a default IP address")
	}
}

type testRPC struct {
	cmd    string
	expect string
}

type mockChannelIn struct {
	t       *testing.T
	service *Service
	rpc     []*testRPC
	wg      sync.WaitGroup
	start   error
}

func (c *mockChannelIn) Start() error {
	return c.start
}

func (c *mockChannelIn) Stop() error {
	return nil
}

func (c *mockChannelIn) Receive() ([]byte, error) {
	if len(c.rpc) == 0 {
		if c.rpc != nil {
			// All test RPC requests have been consumed
			c.wg.Done()
			c.rpc = nil
		}
		return nil, io.EOF
	}

	return []byte(c.rpc[0].cmd), nil
}

func (c *mockChannelIn) Send(buf []byte) error {
	if buf == nil {
		return nil
	}

	expect := c.rpc[0].expect
	if string(buf) != expect {
		c.t.Errorf("expected %q reply for request %q, got: %q", expect, c.rpc[0].cmd, buf)
	}

	c.rpc = c.rpc[1:]

	return nil
}

// discard rpc out for now
type mockChannelOut struct {
	reply [][]byte
	start error
}

func (c *mockChannelOut) Start() error {
	return c.start
}

func (c *mockChannelOut) Stop() error {
	return nil
}

func (c *mockChannelOut) Receive() ([]byte, error) {
	if len(c.reply) == 0 {
		return nil, io.EOF
	}
	reply := c.reply[0]
	c.reply = c.reply[1:]
	return reply, nil
}

func (c *mockChannelOut) Send(buf []byte) error {
	if len(buf) == 0 {
		return io.ErrShortBuffer
	}
	return nil
}

func TestServiceRun(t *testing.T) {
	in := new(mockChannelIn)
	out := new(mockChannelOut)

	service := NewService(in, out)

	in.rpc = []*testRPC{
		{"reset", "OK ATR toolbox"},
		{"ping", "OK "},
		{"Set_Option synctime 0", "OK "},
		{"Capabilities_Register", "OK "},
		{"Set_Option broadcastIP 1", "OK "},
	}

	in.wg.Add(1)

	// replies to register capabilities
	for i := 0; i < len(capabilities); i++ {
		out.reply = append(out.reply, rpciOK)
	}

	out.reply = append(out.reply,
		rpciOK, // reply to SendGuestInfo call in Reset()
		rpciOK, // reply to IP broadcast
	)

	in.service = service

	in.t = t

	err := service.Start()
	if err != nil {
		t.Fatal(err)
	}

	in.wg.Wait()

	service.Stop()
	service.Wait()

	// verify we don't set delay > maxDelay
	for i := 0; i <= maxDelay+1; i++ {
		service.backoff()
	}

	if service.delay != maxDelay {
		t.Errorf("delay=%d", service.delay)
	}
}

func TestServiceErrors(t *testing.T) {
	Trace = true
	if !testing.Verbose() {
		// cover TraceChannel but discard output
		traceLog = ioutil.Discard
	}

	netInterfaceAddrs = func() ([]net.Addr, error) {
		return nil, io.EOF
	}

	in := new(mockChannelIn)
	out := new(mockChannelOut)

	service := NewService(in, out)

	service.RegisterHandler("Sorry", func([]byte) ([]byte, error) {
		return nil, errors.New("i am so sorry")
	})

	ip := ""
	service.PrimaryIP = func() string {
		if ip == "" {
			ip = "127"
		} else if ip == "127" {
			ip = "127.0.0.1"
		} else if ip == "127.0.0.1" {
			ip = ""
		}
		return ip
	}

	in.rpc = []*testRPC{
		{"Capabilities_Register", "OK "},
		{"Set_Option broadcastIP 1", "ERR "},
		{"Set_Option broadcastIP 1", "OK "},
		{"Set_Option broadcastIP 1", "OK "},
		{"NOPE", "Unknown Command"},
		{"Sorry", "ERR "},
	}

	in.wg.Add(1)

	// replies to register capabilities
	for i := 0; i < len(capabilities); i++ {
		out.reply = append(out.reply, rpciERR)
	}

	foo := []byte("foo")
	out.reply = append(
		out.reply,
		rpciERR,
		rpciOK,
		rpciOK,
		append(rpciOK, foo...),
		rpciERR,
	)

	in.service = service

	in.t = t

	err := service.Start()
	if err != nil {
		t.Fatal(err)
	}

	in.wg.Wait()

	// Done serving RPCs, test ChannelOut errors
	reply, err := service.out.Request(rpciOK)
	if err != nil {
		t.Error(err)
	}

	if !bytes.Equal(reply, foo) {
		t.Errorf("reply=%q", foo)
	}

	_, err = service.out.Request(rpciOK)
	if err == nil {
		t.Error("expected error")
	}

	_, err = service.out.Request(nil)
	if err == nil {
		t.Error("expected error")
	}

	service.Stop()
	service.Wait()

	// cover service start error paths
	start := errors.New("fail")

	in.start = start
	err = service.Start()
	if err != start {
		t.Error("expected error")
	}

	in.start = nil
	out.start = start
	err = service.Start()
	if err != start {
		t.Error("expected error")
	}
}

var (
	testESX = flag.Bool("toolbox.testesx", false, "Test toolbox service against ESX (vmtoolsd must not be running)")
	testPID = flag.Int64("toolbox.testpid", 0, "PID to return from toolbox start command")
	testOn  = flag.String("toolbox.powerState", "", "Power state of VM prior to starting the test")
)

// echoHandler for testing hgfs.FileHandler
type echoHandler struct{}

func (e *echoHandler) Stat(u *url.URL) (os.FileInfo, error) {
	if u.RawQuery == "" {
		return nil, errors.New("no query")
	}

	if u.Query().Get("foo") != "bar" {
		return nil, errors.New("invalid query")
	}

	return os.Stat(u.Path)
}

func (e *echoHandler) Open(u *url.URL, mode int32) (hgfs.File, error) {
	_, err := e.Stat(u)
	if err != nil {
		return nil, err
	}

	return os.Open(u.Path)
}

func TestServiceRunESX(t *testing.T) {
	if *testESX == false {
		t.SkipNow()
	}

	Trace = testing.Verbose()

	// A server that echos HTTP requests, for testing toolbox's http.RoundTripper
	echo := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_ = r.Write(w)
	}))
	// Client side can use 'govc guest.getenv' to get the URL w/ random port
	_ = os.Setenv("TOOLBOX_ECHO_SERVER", echo.URL)

	var wg sync.WaitGroup

	in := NewBackdoorChannelIn()
	out := NewBackdoorChannelOut()

	service := NewService(in, out)
	service.Command.FileServer.RegisterFileHandler("echo", new(echoHandler))

	ping := sync.NewCond(new(sync.Mutex))

	service.RegisterHandler("ping", func(b []byte) ([]byte, error) {
		ping.Broadcast()
		return service.Ping(b)
	})

	// assert that reset, ping, Set_Option and Capabilities_Register are called at least once
	for _, name := range []string{"reset", "ping", "Set_Option", "Capabilities_Register"} {
		n := name
		h := service.handlers[name]
		wg.Add(1)

		service.handlers[name] = func(b []byte) ([]byte, error) {
			defer wg.Done()

			service.handlers[n] = h // reset

			return h(b)
		}
	}

	if *testOn == string(types.VirtualMachinePowerStatePoweredOff) {
		wg.Add(1)
		service.Power.PowerOn.Handler = func() error {
			defer wg.Done()
			log.Print("power on event")
			return nil
		}
	} else {
		log.Print("skipping power on test")
	}

	if *testPID != 0 {
		service.Command.ProcessStartCommand = func(m *ProcessManager, r *vix.StartProgramRequest) (int64, error) {
			wg.Add(1)
			defer wg.Done()

			switch r.ProgramPath {
			case "/bin/date":
				return *testPID, nil
			case "sleep":
				p := NewProcessFunc(func(ctx context.Context, arg string) error {
					d, err := time.ParseDuration(arg)
					if err != nil {
						return err
					}

					select {
					case <-ctx.Done():
						return &ProcessError{Err: ctx.Err(), ExitCode: 42}
					case <-time.After(d):
					}

					return nil
				})
				return m.Start(r, p)
			default:
				return DefaultStartCommand(m, r)
			}
		}
	}

	service.PrimaryIP = func() string {
		log.Print("broadcasting IP")
		return DefaultIP()
	}

	log.Print("starting toolbox service")
	err := service.Start()
	if err != nil {
		log.Fatal(err)
	}

	wg.Wait()

	// wait for 1 last ping to make sure the final response has reached the client before stopping
	ping.L.Lock()
	ping.Wait()
	ping.L.Unlock()

	service.Stop()
	service.Wait()
}
