// Copyright (c) 2016 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

package hyperstart_test

import (
	"math"
	"net"
	"reflect"
	"testing"
	"time"

	. "github.com/kata-containers/runtime/virtcontainers/pkg/hyperstart"
	"github.com/kata-containers/runtime/virtcontainers/pkg/hyperstart/mock"
)

const (
	testSockType = "unix"
	testSequence = uint64(100)
	testMessage  = "test_message"
)

func connectHyperstartNoMulticast(h *Hyperstart) error {
	return h.OpenSocketsNoMulticast()
}

func connectHyperstart(h *Hyperstart) error {
	return h.OpenSockets()
}

func disconnectHyperstart(h *Hyperstart) {
	h.CloseSockets()
}

func connectMockHyperstart(t *testing.T, multiCast bool) (*mock.Hyperstart, *Hyperstart, error) {
	mockHyper := mock.NewHyperstart(t)

	mockHyper.Start()

	ctlSock, ioSock := mockHyper.GetSocketPaths()

	h := NewHyperstart(ctlSock, ioSock, testSockType)

	var err error
	if multiCast {
		err = connectHyperstart(h)
	} else {
		err = connectHyperstartNoMulticast(h)
	}
	if err != nil {
		mockHyper.Stop()
		return nil, nil, err
	}

	return mockHyper, h, nil
}

func TestNewHyperstart(t *testing.T) {
	ctlSock := "/tmp/test_hyper.sock"
	ioSock := "/tmp/test_tty.sock"
	sockType := "test_unix"

	h := NewHyperstart(ctlSock, ioSock, sockType)

	resultCtlSockPath := h.GetCtlSockPath()
	resultIoSockPath := h.GetIoSockPath()
	resultSockType := h.GetSockType()

	if resultCtlSockPath != ctlSock {
		t.Fatalf("CTL sock result %s should be the same than %s", resultCtlSockPath, ctlSock)
	}

	if resultIoSockPath != ioSock {
		t.Fatalf("IO sock result %s should be the same than %s", resultIoSockPath, ioSock)
	}

	if resultSockType != sockType {
		t.Fatalf("Sock type result %s should be the same than %s", resultSockType, sockType)
	}
}

func TestOpenSockets(t *testing.T) {
	mockHyper := mock.NewHyperstart(t)

	mockHyper.Start()

	ctlSock, ioSock := mockHyper.GetSocketPaths()

	h := NewHyperstart(ctlSock, ioSock, testSockType)

	err := h.OpenSockets()
	if err != nil {
		mockHyper.Stop()
		t.Fatal()
	}

	mockHyper.Stop()

	disconnectHyperstart(h)
}

func TestCloseSockets(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}

	mockHyper.Stop()

	err = h.CloseSockets()
	if err != nil {
		t.Fatal()
	}
}

func TestSetDeadline(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, false)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	timeoutDuration := 1 * time.Second

	err = h.SetDeadline(time.Now().Add(timeoutDuration))
	if err != nil {
		t.Fatal()
	}

	mockHyper.SendMessage(ReadyCode, []byte{})

	buf := make([]byte, 512)
	_, err = h.GetCtlSock().Read(buf)
	if err != nil {
		t.Fatal()
	}

	err = h.SetDeadline(time.Now().Add(timeoutDuration))
	if err != nil {
		t.Fatal()
	}

	time.Sleep(timeoutDuration)

	_, err = h.GetCtlSock().Read(buf)
	netErr, ok := err.(net.Error)
	if ok && netErr.Timeout() == false {
		t.Fatal()
	}
}

func TestIsStartedFalse(t *testing.T) {
	h := &Hyperstart{}

	if h.IsStarted() == true {
		t.Fatal()
	}
}

func TestIsStartedTrue(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	if h.IsStarted() == false {
		t.Fatal()
	}
}

func testFormatMessage(t *testing.T, payload interface{}, expected []byte) {
	res, err := FormatMessage(payload)
	if err != nil {
		t.Fatal()
	}

	if reflect.DeepEqual(res, expected) == false {
		t.Fatal()
	}
}

func TestFormatMessageFromString(t *testing.T) {
	payload := testMessage
	expectedOut := []byte(payload)

	testFormatMessage(t, payload, expectedOut)
}

type TestStruct struct {
	FieldString string `json:"fieldString"`
	FieldInt    int    `json:"fieldInt"`
}

func TestFormatMessageFromStruct(t *testing.T) {
	payload := TestStruct{
		FieldString: "test_string",
		FieldInt:    100,
	}

	expectedOut := []byte("{\"fieldString\":\"test_string\",\"fieldInt\":100}")

	testFormatMessage(t, payload, expectedOut)
}

func TestReadCtlMessage(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, false)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	expected := &DecodedMessage{
		Code:    ReadyCode,
		Message: []byte{},
	}

	mockHyper.SendMessage(int(expected.Code), expected.Message)

	reply, err := ReadCtlMessage(h.GetCtlSock())
	if err != nil {
		t.Fatal()
	}

	if reflect.DeepEqual(reply, expected) == false {
		t.Fatal()
	}
}

func TestWriteCtlMessage(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, false)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	msg := DecodedMessage{
		Code:    PingCode,
		Message: []byte{},
	}

	err = h.WriteCtlMessage(h.GetCtlSock(), &msg)
	if err != nil {
		t.Fatal()
	}

	for {
		reply, err := ReadCtlMessage(h.GetCtlSock())
		if err != nil {
			t.Fatal()
		}

		if reply.Code == NextCode {
			continue
		}

		err = h.CheckReturnedCode(reply, AckCode)
		if err != nil {
			t.Fatal()
		}

		break
	}

	msgs := mockHyper.GetLastMessages()
	if msgs == nil {
		t.Fatal()
	}

	if msgs[0].Code != msg.Code || string(msgs[0].Message) != string(msg.Message) {
		t.Fatal()
	}
}

func TestReadIoMessage(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	mockHyper.SendIo(testSequence, []byte(testMessage))

	msg, err := h.ReadIoMessage()
	if err != nil {
		t.Fatal()
	}

	if msg.Session != testSequence || string(msg.Message) != testMessage {
		t.Fatal()
	}
}

func TestReadIoMessageWithConn(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	mockHyper.SendIo(testSequence, []byte(testMessage))

	msg, err := ReadIoMessageWithConn(h.GetIoSock())
	if err != nil {
		t.Fatal()
	}

	if msg.Session != testSequence || string(msg.Message) != testMessage {
		t.Fatal()
	}
}

func TestSendIoMessage(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	msg := &TtyMessage{
		Session: testSequence,
		Message: []byte(testMessage),
	}

	err = h.SendIoMessage(msg)
	if err != nil {
		t.Fatal()
	}

	buf := make([]byte, 512)
	n, seqRecv := mockHyper.ReadIo(buf)

	if seqRecv != testSequence || string(buf[TtyHdrSize:n]) != testMessage {
		t.Fatal()
	}
}

func TestSendIoMessageWithConn(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	msg := &TtyMessage{
		Session: testSequence,
		Message: []byte(testMessage),
	}

	err = SendIoMessageWithConn(h.GetIoSock(), msg)
	if err != nil {
		t.Fatal()
	}

	buf := make([]byte, 512)
	n, seqRecv := mockHyper.ReadIo(buf)

	if seqRecv != testSequence || string(buf[TtyHdrSize:n]) != testMessage {
		t.Fatal()
	}
}

func testCodeFromCmd(t *testing.T, cmd string, expected uint32) {
	h := &Hyperstart{}

	code, err := h.CodeFromCmd(cmd)
	if err != nil || code != expected {
		t.Fatal()
	}
}

func TestCodeFromCmdVersion(t *testing.T) {
	testCodeFromCmd(t, Version, VersionCode)
}

func TestCodeFromCmdStartSandbox(t *testing.T) {
	testCodeFromCmd(t, StartSandbox, StartSandboxCode)
}

func TestCodeFromCmdDestroySandbox(t *testing.T) {
	testCodeFromCmd(t, DestroySandbox, DestroySandboxCode)
}

func TestCodeFromCmdExecCmd(t *testing.T) {
	testCodeFromCmd(t, ExecCmd, ExecCmdCode)
}

func TestCodeFromCmdReady(t *testing.T) {
	testCodeFromCmd(t, Ready, ReadyCode)
}

func TestCodeFromCmdAck(t *testing.T) {
	testCodeFromCmd(t, Ack, AckCode)
}

func TestCodeFromCmdError(t *testing.T) {
	testCodeFromCmd(t, Error, ErrorCode)
}

func TestCodeFromCmdWinSize(t *testing.T) {
	testCodeFromCmd(t, WinSize, WinsizeCode)
}

func TestCodeFromCmdPing(t *testing.T) {
	testCodeFromCmd(t, Ping, PingCode)
}

func TestCodeFromCmdNext(t *testing.T) {
	testCodeFromCmd(t, Next, NextCode)
}

func TestCodeFromCmdWriteFile(t *testing.T) {
	testCodeFromCmd(t, WriteFile, WriteFileCode)
}

func TestCodeFromCmdReadFile(t *testing.T) {
	testCodeFromCmd(t, ReadFile, ReadFileCode)
}

func TestCodeFromCmdNewContainer(t *testing.T) {
	testCodeFromCmd(t, NewContainer, NewContainerCode)
}

func TestCodeFromCmdKillContainer(t *testing.T) {
	testCodeFromCmd(t, KillContainer, KillContainerCode)
}

func TestCodeFromCmdOnlineCPUMem(t *testing.T) {
	testCodeFromCmd(t, OnlineCPUMem, OnlineCPUMemCode)
}

func TestCodeFromCmdSetupInterface(t *testing.T) {
	testCodeFromCmd(t, SetupInterface, SetupInterfaceCode)
}

func TestCodeFromCmdSetupRoute(t *testing.T) {
	testCodeFromCmd(t, SetupRoute, SetupRouteCode)
}

func TestCodeFromCmdRemoveContainer(t *testing.T) {
	testCodeFromCmd(t, RemoveContainer, RemoveContainerCode)
}

func TestCodeFromCmdUnknown(t *testing.T) {
	h := &Hyperstart{}

	code, err := h.CodeFromCmd("unknown")
	if err == nil || code != math.MaxUint32 {
		t.Fatal()
	}
}

func testCheckReturnedCode(t *testing.T, recvMsg *DecodedMessage, refCode uint32) {
	h := &Hyperstart{}

	err := h.CheckReturnedCode(recvMsg, refCode)
	if err != nil {
		t.Fatal()
	}
}

func TestCheckReturnedCodeList(t *testing.T) {
	for _, code := range CodeList {
		recvMsg := DecodedMessage{Code: code}
		testCheckReturnedCode(t, &recvMsg, code)
	}
}

func testCheckReturnedCodeFailure(t *testing.T, recvMsg *DecodedMessage, refCode uint32) {
	h := &Hyperstart{}

	err := h.CheckReturnedCode(recvMsg, refCode)
	if err == nil {
		t.Fatal()
	}
}

func TestCheckReturnedCodeListWrong(t *testing.T) {
	for _, code := range CodeList {
		msg := DecodedMessage{Code: code}
		if code != ReadyCode {
			testCheckReturnedCodeFailure(t, &msg, ReadyCode)
		} else {
			testCheckReturnedCodeFailure(t, &msg, PingCode)
		}
	}
}

func TestWaitForReady(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	mockHyper.SendMessage(int(ReadyCode), []byte{})

	err = h.WaitForReady()
	if err != nil {
		t.Fatal()
	}
}

func TestWaitForReadyError(t *testing.T) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	mockHyper.SendMessage(int(ErrorCode), []byte{})

	err = h.WaitForReady()
	if err == nil {
		t.Fatal()
	}
}

var cmdList = []string{
	Version,
	StartSandbox,
	DestroySandbox,
	ExecCmd,
	Ready,
	Ack,
	Error,
	WinSize,
	Ping,
	Next,
	NewContainer,
	KillContainer,
	OnlineCPUMem,
	SetupInterface,
	SetupRoute,
	RemoveContainer,
}

func testSendCtlMessage(t *testing.T, cmd string) {
	mockHyper, h, err := connectMockHyperstart(t, true)
	if err != nil {
		t.Fatal()
	}
	defer disconnectHyperstart(h)
	defer mockHyper.Stop()

	msg, err := h.SendCtlMessage(cmd, []byte{})
	if err != nil {
		t.Fatal()
	}

	if msg.Code != AckCode {
		t.Fatal()
	}
}

func TestSendCtlMessage(t *testing.T) {
	for _, cmd := range cmdList {
		testSendCtlMessage(t, cmd)
	}
}
