// Copyright (c) 2017 Cisco and/or its affiliates.
//
// 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 core

import (
	"testing"
	"time"

	. "github.com/onsi/gomega"

	"git.fd.io/govpp.git/adapter/mock"
	"git.fd.io/govpp.git/api"
	interfaces "git.fd.io/govpp.git/binapi/interface"
	"git.fd.io/govpp.git/binapi/interface_types"
	"git.fd.io/govpp.git/binapi/memif"
	"git.fd.io/govpp.git/binapi/vpe"
)

type testCtx struct {
	mockVpp *mock.VppAdapter
	conn    *Connection
	ch      api.Channel
}

func setupTest(t *testing.T) *testCtx {
	RegisterTestingT(t)

	ctx := &testCtx{
		mockVpp: mock.NewVppAdapter(),
	}

	var err error
	ctx.conn, err = Connect(ctx.mockVpp)
	Expect(err).ShouldNot(HaveOccurred())

	ctx.ch, err = ctx.conn.NewAPIChannel()
	Expect(err).ShouldNot(HaveOccurred())

	return ctx
}

func (ctx *testCtx) teardownTest() {
	ctx.ch.Close()
	ctx.conn.Disconnect()
}

func TestRequestReplyMemifCreate(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	ctx.mockVpp.MockReply(&memif.MemifCreateReply{
		SwIfIndex: 4,
	})

	request := &memif.MemifCreate{
		Role:       10,
		ID:         12,
		RingSize:   8000,
		BufferSize: 50,
	}
	reply := &memif.MemifCreateReply{}

	err := ctx.ch.SendRequest(request).ReceiveReply(reply)
	Expect(err).ShouldNot(HaveOccurred())
	Expect(reply.Retval).To(BeEquivalentTo(0),
		"Incorrect Retval value for MemifCreate")
	Expect(reply.SwIfIndex).To(BeEquivalentTo(4),
		"Incorrect SwIfIndex value for MemifCreate")
}

func TestRequestReplyMemifDelete(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	ctx.mockVpp.MockReply(&memif.MemifDeleteReply{})

	request := &memif.MemifDelete{
		SwIfIndex: 15,
	}
	reply := &memif.MemifDeleteReply{}

	err := ctx.ch.SendRequest(request).ReceiveReply(reply)
	Expect(err).ShouldNot(HaveOccurred())
}

func TestRequestReplyMemifDetails(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	ctx.mockVpp.MockReply(&memif.MemifDetails{
		SwIfIndex: 25,
		IfName:    "memif-name",
		Role:      0,
	})

	request := &memif.MemifDump{}
	reply := &memif.MemifDetails{}

	err := ctx.ch.SendRequest(request).ReceiveReply(reply)
	Expect(err).ShouldNot(HaveOccurred())
	Expect(reply.SwIfIndex).To(BeEquivalentTo(25),
		"Incorrect SwIfIndex value for MemifDetails")
	Expect(reply.IfName).ToNot(BeEmpty(),
		"MemifDetails IfName is empty byte array")
	Expect(reply.Role).To(BeEquivalentTo(0),
		"Incorrect Role value for MemifDetails")
}

func TestMultiRequestReplySwInterfaceMemifDump(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	var msgs []api.Message
	for i := 1; i <= 10; i++ {
		msgs = append(msgs, &memif.MemifDetails{
			SwIfIndex: interface_types.InterfaceIndex(i),
		})
	}
	ctx.mockVpp.MockReply(msgs...)
	ctx.mockVpp.MockReply(&ControlPingReply{})

	reqCtx := ctx.ch.SendMultiRequest(&memif.MemifDump{})
	cnt := 0
	for {
		msg := &memif.MemifDetails{}
		stop, err := reqCtx.ReceiveReply(msg)
		if stop {
			break
		}
		Expect(err).ShouldNot(HaveOccurred())
		cnt++
	}
	Expect(cnt).To(BeEquivalentTo(10))
}

func TestNotificationEvent(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// subscribe for notification
	notifChan := make(chan api.Message, 1)
	sub, err := ctx.ch.SubscribeNotification(notifChan, &interfaces.SwInterfaceEvent{})
	Expect(err).ShouldNot(HaveOccurred())

	// mock event and force its delivery
	ctx.mockVpp.MockReply(&interfaces.SwInterfaceEvent{
		SwIfIndex: 2,
		Flags:     interface_types.IF_STATUS_API_FLAG_LINK_UP,
	})
	ctx.mockVpp.SendMsg(0, []byte(""))

	// receive the notification
	var notif *interfaces.SwInterfaceEvent
	Eventually(func() *interfaces.SwInterfaceEvent {
		select {
		case n := <-notifChan:
			notif = n.(*interfaces.SwInterfaceEvent)
			return notif
		default:
			return nil
		}
	}).ShouldNot(BeNil())

	// verify the received notifications
	Expect(notif.SwIfIndex).To(BeEquivalentTo(2), "Incorrect SwIfIndex value for SwInterfaceSetFlags")
	Expect(notif.Flags).To(BeEquivalentTo(interface_types.IF_STATUS_API_FLAG_LINK_UP), "Incorrect LinkUpDown value for SwInterfaceSetFlags")

	err = sub.Unsubscribe()
	Expect(err).ShouldNot(HaveOccurred())
}

func TestSetReplyTimeout(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	ctx.ch.SetReplyTimeout(time.Millisecond)

	// mock reply
	ctx.mockVpp.MockReply(&ControlPingReply{})

	// first one request should work
	err := ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).ShouldNot(HaveOccurred())

	// no other reply ready - expect timeout
	err = ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("timeout"))
}

func TestSetReplyTimeoutMultiRequest(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	ctx.ch.SetReplyTimeout(time.Millisecond * 100)

	// mock reply
	ctx.mockVpp.MockReply(
		&interfaces.SwInterfaceDetails{
			SwIfIndex:     1,
			InterfaceName: "if-name-test",
		},
		&interfaces.SwInterfaceDetails{
			SwIfIndex:     2,
			InterfaceName: "if-name-test",
		},
		&interfaces.SwInterfaceDetails{
			SwIfIndex:     3,
			InterfaceName: "if-name-test",
		},
	)
	ctx.mockVpp.MockReply(&ControlPingReply{})

	cnt := 0
	sendMultiRequest := func() error {
		reqCtx := ctx.ch.SendMultiRequest(&interfaces.SwInterfaceDump{})
		for {
			msg := &interfaces.SwInterfaceDetails{}
			stop, err := reqCtx.ReceiveReply(msg)
			if err != nil {
				return err
			}
			if stop {
				break
			}
			cnt++
		}
		return nil
	}

	// first one request should work
	err := sendMultiRequest()
	Expect(err).ShouldNot(HaveOccurred())

	// no other reply ready - expect timeout
	err = sendMultiRequest()
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("timeout"))

	Expect(cnt).To(BeEquivalentTo(3))
}

func TestReceiveReplyNegative(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// invalid context 1
	reqCtx1 := &requestCtx{}
	err := reqCtx1.ReceiveReply(&ControlPingReply{})
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("invalid request context"))

	// invalid context 2
	reqCtx2 := &multiRequestCtx{}
	_, err = reqCtx2.ReceiveReply(&ControlPingReply{})
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("invalid request context"))

	// NU
	reqCtx3 := &requestCtx{}
	err = reqCtx3.ReceiveReply(nil)
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("invalid request context"))
}

func TestMultiRequestDouble(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	var msgs []mock.MsgWithContext
	for i := 1; i <= 3; i++ {
		msgs = append(msgs, mock.MsgWithContext{
			Msg: &interfaces.SwInterfaceDetails{
				SwIfIndex:     interface_types.InterfaceIndex(i),
				InterfaceName: "if-name-test",
			},
			Multipart: true,
			SeqNum:    1,
		})
	}
	msgs = append(msgs, mock.MsgWithContext{Msg: &ControlPingReply{}, Multipart: true, SeqNum: 1})

	for i := 1; i <= 3; i++ {
		msgs = append(msgs,
			mock.MsgWithContext{
				Msg: &interfaces.SwInterfaceDetails{
					SwIfIndex:     interface_types.InterfaceIndex(i),
					InterfaceName: "if-name-test",
				},
				Multipart: true,
				SeqNum:    2,
			})
	}
	msgs = append(msgs, mock.MsgWithContext{Msg: &ControlPingReply{}, Multipart: true, SeqNum: 2})

	ctx.mockVpp.MockReplyWithContext(msgs...)

	cnt := 0
	var sendMultiRequest = func() error {
		reqCtx := ctx.ch.SendMultiRequest(&interfaces.SwInterfaceDump{})
		for {
			msg := &interfaces.SwInterfaceDetails{}
			stop, err := reqCtx.ReceiveReply(msg)
			if stop {
				break
			}
			if err != nil {
				return err
			}
			cnt++
		}
		return nil
	}

	err := sendMultiRequest()
	Expect(err).ShouldNot(HaveOccurred())

	err = sendMultiRequest()
	Expect(err).ShouldNot(HaveOccurred())

	Expect(cnt).To(BeEquivalentTo(6))
}

func TestReceiveReplyAfterTimeout(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	ctx.ch.SetReplyTimeout(time.Millisecond)

	// mock reply
	ctx.mockVpp.MockReplyWithContext(mock.MsgWithContext{Msg: &ControlPingReply{}, SeqNum: 1})
	// first one request should work

	err := ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).ShouldNot(HaveOccurred())

	err = ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("timeout"))

	ctx.mockVpp.MockReplyWithContext(
		// simulating late reply
		mock.MsgWithContext{
			Msg:    &ControlPingReply{},
			SeqNum: 2,
		},
		// normal reply for next request
		mock.MsgWithContext{
			Msg:    &interfaces.SwInterfaceSetFlagsReply{},
			SeqNum: 3,
		},
	)

	req := &interfaces.SwInterfaceSetFlags{
		SwIfIndex: 1,
		Flags:     interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
	}
	reply := &interfaces.SwInterfaceSetFlagsReply{}

	// should succeed
	err = ctx.ch.SendRequest(req).ReceiveReply(reply)
	Expect(err).ShouldNot(HaveOccurred())
}

func TestReceiveReplyAfterTimeoutMultiRequest(t *testing.T) {
	/*
		TODO: fix mock adapter
		This test will fail because mock adapter will stop sending replies
		when it encounters control_ping_reply from multi request,
		thus never sending reply for next request
	*/
	t.Skip()

	ctx := setupTest(t)
	defer ctx.teardownTest()

	ctx.ch.SetReplyTimeout(time.Millisecond * 100)

	// mock reply
	ctx.mockVpp.MockReplyWithContext(mock.MsgWithContext{Msg: &ControlPingReply{}, SeqNum: 1})

	// first one request should work
	err := ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).ShouldNot(HaveOccurred())

	cnt := 0
	var sendMultiRequest = func() error {
		reqCtx := ctx.ch.SendMultiRequest(&interfaces.SwInterfaceDump{})
		for {
			msg := &interfaces.SwInterfaceDetails{}
			stop, err := reqCtx.ReceiveReply(msg)
			if stop {
				break
			}
			if err != nil {
				return err
			}
			cnt++
		}
		return nil
	}

	err = sendMultiRequest()
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("timeout"))
	Expect(cnt).To(BeEquivalentTo(0))

	// simulating late replies
	var msgs []mock.MsgWithContext
	for i := 1; i <= 3; i++ {
		msgs = append(msgs, mock.MsgWithContext{
			Msg: &interfaces.SwInterfaceDetails{
				SwIfIndex:     interface_types.InterfaceIndex(i),
				InterfaceName: "if-name-test",
			},
			Multipart: true,
			SeqNum:    2,
		})
	}
	msgs = append(msgs, mock.MsgWithContext{Msg: &ControlPingReply{}, Multipart: true, SeqNum: 2})
	ctx.mockVpp.MockReplyWithContext(msgs...)

	// normal reply for next request
	ctx.mockVpp.MockReplyWithContext(mock.MsgWithContext{Msg: &interfaces.SwInterfaceSetFlagsReply{}, SeqNum: 3})

	req := &interfaces.SwInterfaceSetFlags{
		SwIfIndex: 1,
		Flags:     interface_types.IF_STATUS_API_FLAG_ADMIN_UP,
	}
	reply := &interfaces.SwInterfaceSetFlagsReply{}

	// should succeed
	err = ctx.ch.SendRequest(req).ReceiveReply(reply)
	Expect(err).ShouldNot(HaveOccurred())
}

func TestInvalidMessageID(t *testing.T) {
	ctx := setupTest(t)
	defer ctx.teardownTest()

	// mock reply
	ctx.mockVpp.MockReply(&vpe.ShowVersionReply{})
	ctx.mockVpp.MockReply(&vpe.ShowVersionReply{})

	// first one request should work
	err := ctx.ch.SendRequest(&vpe.ShowVersion{}).ReceiveReply(&vpe.ShowVersionReply{})
	Expect(err).ShouldNot(HaveOccurred())

	// second should fail with error invalid message ID
	err = ctx.ch.SendRequest(&ControlPing{}).ReceiveReply(&ControlPingReply{})
	Expect(err).Should(HaveOccurred())
	Expect(err.Error()).To(ContainSubstring("unexpected message"))
}