package syncer_test

import (
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/format"
	gomegaTypes "github.com/onsi/gomega/types"
	"github.com/pkg/errors"
	. "github.com/submariner-io/admiral/pkg/gomega"
	"github.com/submariner-io/admiral/pkg/syncer/test"
	"github.com/submariner-io/admiral/pkg/watcher"
	submarinerv1 "github.com/submariner-io/submariner/pkg/apis/submariner.io/v1"
	fakeEngine "github.com/submariner-io/submariner/pkg/cableengine/fake"
	"github.com/submariner-io/submariner/pkg/cableengine/healthchecker"
	"github.com/submariner-io/submariner/pkg/cableengine/healthchecker/fake"
	"github.com/submariner-io/submariner/pkg/cableengine/syncer"
	fakeClientset "github.com/submariner-io/submariner/pkg/client/clientset/versioned/fake"
	fakeClientsetv1 "github.com/submariner-io/submariner/pkg/client/clientset/versioned/typed/submariner.io/v1/fake"
	submarinerInformers "github.com/submariner-io/submariner/pkg/client/informers/externalversions"
	"github.com/submariner-io/submariner/pkg/types"
	"github.com/submariner-io/submariner/pkg/util"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/client-go/dynamic"
	fakeClient "k8s.io/client-go/dynamic/fake"
	kubeScheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog"
)

const (
	namespace = "submariner"
)

func init() {
	klog.InitFlags(nil)
}

var _ = BeforeSuite(func() {
	syncer.GatewayUpdateInterval = 200 * time.Millisecond
	syncer.GatewayStaleTimeout = 1 * time.Second
})

var _ = Describe("", func() {
	Context("Gateway syncing", testGatewaySyncing)
	Context("Stale Gateway cleanup", testStaleGatewayCleanup)
	Context("Gateway sync errors", testGatewaySyncErrors)
	Context("Gateway latency info", testGatewayLatencyInfo)
})

func testGatewaySyncing() {
	var t *testDriver

	BeforeEach(func() {
		t = newTestDriver()
	})

	JustBeforeEach(func() {
		t.run()
	})

	AfterEach(func() {
		t.stop()
	})

	When("the syncer is started", func() {
		It("should create the Gateway resource with the correct information", func() {
			t.awaitGatewayUpdated(t.expectedGateway)
		})
	})

	When("the cable engine info changes", func() {
		BeforeEach(func() {
			t.engine.Connections = nil
		})

		It("should update the Gateway Status with the correct information", func() {
			t.awaitGatewayUpdated(t.expectedGateway)

			t.engine.Lock()

			t.expectedGateway.Status.HAStatus = submarinerv1.HAStatusActive
			t.engine.HAStatus = t.expectedGateway.Status.HAStatus

			t.expectedGateway.Status.Connections = []submarinerv1.Connection{
				{
					Status:        submarinerv1.Connecting,
					StatusMessage: "Connecting to 1.2.3.4:400",
					Endpoint: submarinerv1.EndpointSpec{
						ClusterID: "west",
						CableName: "submariner-cable-west-192-68-1-10",
						PrivateIP: "192.6.1.11",
						Backend:   "libreswan",
					},
				},
				{
					Status:        submarinerv1.Connected,
					StatusMessage: "Connected to 1.2.3.5:500",
					Endpoint: submarinerv1.EndpointSpec{
						ClusterID: "north",
						CableName: "submariner-cable-north-192-68-1-20",
						PrivateIP: "192.6.1.21",
						Backend:   "wireguard",
					},
				},
			}
			t.engine.Connections = t.expectedGateway.Status.Connections

			t.engine.Unlock()

			t.awaitGatewayUpdated(t.expectedGateway)
			t.awaitNoGatewayUpdated()
		})
	})

	When("a specific status error is set", func() {
		It("should create the Gateway resource with the correct StatusFailure", func() {
			t.awaitGatewayUpdated(t.expectedGateway)

			statusErr := errors.New("fake error")
			t.expectedGateway.Status.StatusFailure = statusErr.Error()

			t.syncer.SetGatewayStatusError(statusErr)
			t.awaitGatewayUpdated(t.expectedGateway)
		})
	})
}

func testStaleGatewayCleanup() {
	var t *testDriver
	var staleGateway *submarinerv1.Gateway

	BeforeEach(func() {
		t = newTestDriver()
		staleGateway = &submarinerv1.Gateway{
			ObjectMeta: metav1.ObjectMeta{
				Name: "raiders",
			},
			Status: submarinerv1.GatewayStatus{
				HAStatus: submarinerv1.HAStatusPassive,
			},
		}

		t.expectedGateway.Status.HAStatus = submarinerv1.HAStatusActive
		t.engine.HAStatus = t.expectedGateway.Status.HAStatus
	})

	JustBeforeEach(func() {
		t.run()

		t.awaitGatewayUpdated(t.expectedGateway)

		_, err := t.gateways.Create(staleGateway)
		Expect(err).To(Succeed())

		t.awaitGatewayUpdated(staleGateway)
	})

	AfterEach(func() {
		t.stop()
	})

	When("the Gateway's update timestamp expires", func() {
		BeforeEach(func() {
			staleGateway.Annotations = map[string]string{"update-timestamp": strconv.FormatInt(time.Now().UTC().Unix(), 10)}
		})

		It("should delete the Gateway", func() {
			t.awaitGatewayDeleted(staleGateway)
			t.awaitNoGatewayDeleted()
		})
	})

	When("the Gateway's update-timestamp annotations is missing", func() {
		BeforeEach(func() {
			staleGateway.Annotations = map[string]string{}
		})

		It("should delete the Gateway", func() {
			t.awaitGatewayDeleted(staleGateway)
		})
	})

	When("the Gateway's annotations are missing", func() {
		It("should delete the Gateway", func() {
			t.awaitGatewayDeleted(staleGateway)
		})
	})

	When("the Gateway's update-timestamp annotation is invalid", func() {
		BeforeEach(func() {
			staleGateway.Annotations = map[string]string{"update-timestamp": "invalid"}
		})

		It("should delete the Gateway", func() {
			t.awaitGatewayDeleted(staleGateway)
		})
	})

	When("listing of Gateways fails", func() {
		BeforeEach(func() {
			t.gateways.FailOnList = errors.New("fake error")
		})

		It("should log the error", func() {
			Eventually(t.handledError, 5).Should(Receive(ContainErrorSubstring(t.gateways.FailOnList)))
		})
	})

	When("Gateway delete fails", func() {
		BeforeEach(func() {
			t.gateways.FailOnDelete = errors.New("fake error")
			t.expectedDeletedAfter = nil
		})

		It("should log the error", func() {
			Eventually(t.handledError, 5).Should(Receive(ContainErrorSubstring(t.gateways.FailOnDelete)))
		})
	})
}

func testGatewaySyncErrors() {
	var t *testDriver
	var expectedErr error

	BeforeEach(func() {
		t = newTestDriver()
		expectedErr = errors.New("fake error")
		t.expectedDeletedAfter = nil
	})

	JustBeforeEach(func() {
		t.run()
	})

	AfterEach(func() {
		t.stop()
	})

	When("Gateway create fails", func() {
		BeforeEach(func() {
			t.gateways.FailOnCreate = expectedErr
		})

		It("should log the error", func() {
			Eventually(t.handledError, 5).Should(Receive(ContainErrorSubstring(expectedErr)))
		})
	})

	When("Gateway update fails", func() {
		BeforeEach(func() {
			t.gateways.FailOnUpdate = expectedErr
		})

		It("should log the error", func() {
			t.awaitGatewayUpdated(t.expectedGateway)

			t.engine.Lock()
			t.engine.HAStatus = submarinerv1.HAStatusActive
			t.engine.Unlock()

			Eventually(t.handledError, 5).Should(Receive(ContainErrorSubstring(expectedErr)))
		})
	})

	When("existing Gateway retrieval fails", func() {
		BeforeEach(func() {
			t.gateways.FailOnGet = expectedErr
		})

		It("should log the error", func() {
			Eventually(t.handledError, 5).Should(Receive(ContainErrorSubstring(expectedErr)))
		})
	})

	When("listing of cable engine connections fails", func() {
		BeforeEach(func() {
			t.engine.ListCableConnectionsError = expectedErr
			t.expectedGateway.Status.StatusFailure = expectedErr.Error()
		})

		It("update the Gateway Status failure", func() {
			t.awaitGatewayUpdated(t.expectedGateway)
		})
	})
}

func testGatewayLatencyInfo() {
	var t *testDriver

	BeforeEach(func() {
		t = newTestDriver()
	})

	JustBeforeEach(func() {
		t.run()
	})

	AfterEach(func() {
		t.stop()
	})

	When("the health checker provides latency info", func() {
		It("should correctly update the Gateway Status information", func() {
			t.awaitGatewayUpdated(t.expectedGateway)

			endpointSpec := &submarinerv1.EndpointSpec{
				ClusterID:     "north",
				CableName:     "submariner-cable-north-192-68-1-20",
				PrivateIP:     "192-68-1-20",
				HealthCheckIP: t.pinger.GetIP(),
			}

			endpointName, err := util.GetEndpointCRDNameFromParams(endpointSpec.ClusterID, endpointSpec.CableName)
			Expect(err).To(Succeed())

			test.CreateResource(t.endpoints, &submarinerv1.Endpoint{
				ObjectMeta: metav1.ObjectMeta{
					Name: endpointName,
				},
				Spec: *endpointSpec,
			})

			t.engine.Lock()

			t.expectedGateway.Status.HAStatus = submarinerv1.HAStatusActive
			t.engine.HAStatus = t.expectedGateway.Status.HAStatus

			t.expectedGateway.Status.Connections = []submarinerv1.Connection{
				{
					Status:   submarinerv1.Connected,
					Endpoint: *endpointSpec,
				},
			}

			t.engine.Connections = []submarinerv1.Connection{t.expectedGateway.Status.Connections[0]}

			t.expectedGateway.Status.Connections[0].LatencyRTT = &submarinerv1.LatencyRTTSpec{
				Last:    "93ms",
				Min:     "90ms",
				Average: "95ms",
				Max:     "100ms",
				StdDev:  "94ms",
			}

			t.pinger.SetLatencyInfo(&healthchecker.LatencyInfo{
				ConnectionStatus: healthchecker.Connected,
				Spec:             t.expectedGateway.Status.Connections[0].LatencyRTT,
			})

			t.engine.Unlock()

			t.awaitGatewayUpdated(t.expectedGateway)

			t.expectedGateway.Status.Connections[0].Status = submarinerv1.ConnectionError
			t.expectedGateway.Status.Connections[0].StatusMessage = "Ping failed"

			t.pinger.SetLatencyInfo(&healthchecker.LatencyInfo{
				ConnectionStatus: healthchecker.ConnectionError,
				ConnectionError:  t.expectedGateway.Status.Connections[0].StatusMessage,
				Spec:             t.expectedGateway.Status.Connections[0].LatencyRTT,
			})

			t.awaitGatewayUpdated(t.expectedGateway)

			t.expectedGateway.Status.Connections[0].Status = submarinerv1.Connected
			t.expectedGateway.Status.Connections[0].StatusMessage = ""

			t.pinger.SetLatencyInfo(&healthchecker.LatencyInfo{
				ConnectionStatus: healthchecker.Connected,
				Spec:             t.expectedGateway.Status.Connections[0].LatencyRTT,
			})

			t.awaitGatewayUpdated(t.expectedGateway)
		})
	})
}

type testDriver struct {
	engine               *fakeEngine.Engine
	gateways             *fakeClientsetv1.FailingGateways
	syncer               *syncer.GatewaySyncer
	healthChecker        healthchecker.Interface
	pinger               *fake.Pinger
	endpoints            dynamic.ResourceInterface
	expectedGateway      *submarinerv1.Gateway
	expectedDeletedAfter *submarinerv1.Gateway
	gatewayUpdated       chan *submarinerv1.Gateway
	gatewayDeleted       chan *submarinerv1.Gateway
	stopSyncer           chan struct{}
	stopInformer         chan struct{}
	savedErrorHandlers   []func(error)
	handledError         chan error
}

func newTestDriver() *testDriver {
	t := &testDriver{
		engine:             fakeEngine.New(),
		gateways:           &fakeClientsetv1.FailingGateways{},
		gatewayUpdated:     make(chan *submarinerv1.Gateway, 10),
		gatewayDeleted:     make(chan *submarinerv1.Gateway, 10),
		stopSyncer:         make(chan struct{}),
		stopInformer:       make(chan struct{}),
		savedErrorHandlers: utilruntime.ErrorHandlers,
		handledError:       make(chan error, 10),
	}

	t.engine.LocalEndPoint = &types.SubmarinerEndpoint{Spec: submarinerv1.EndpointSpec{
		ClusterID: "east",
		CableName: "submariner-cable-east-192-68-1-2",
		Hostname:  "redsox",
		PrivateIP: "192.6.1.3",
		Backend:   "libreswan",
	}}

	t.expectedGateway = &submarinerv1.Gateway{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.engine.LocalEndPoint.Spec.Hostname,
		},
		Status: submarinerv1.GatewayStatus{
			Version:       "1",
			HAStatus:      t.engine.GetHAStatus(),
			LocalEndpoint: t.engine.LocalEndPoint.Spec,
			Connections:   t.engine.Connections,
		},
	}

	t.expectedDeletedAfter = t.expectedGateway

	return t
}

func (t *testDriver) run() {
	utilruntime.ErrorHandlers = append(utilruntime.ErrorHandlers, func(err error) {
		t.handledError <- err
	})

	Expect(submarinerv1.AddToScheme(kubeScheme.Scheme)).To(Succeed())

	scheme := runtime.NewScheme()
	Expect(submarinerv1.AddToScheme(scheme)).To(Succeed())

	dynamicClient := fakeClient.NewSimpleDynamicClient(scheme)
	restMapper := test.GetRESTMapperFor(&submarinerv1.Endpoint{})

	client := fakeClientset.NewSimpleClientset()
	t.gateways.GatewayInterface = client.SubmarinerV1().Gateways(namespace)

	t.pinger = fake.NewPinger("10.130.2.2")

	t.healthChecker, _ = healthchecker.New(&healthchecker.Config{
		WatcherConfig: &watcher.Config{
			RestMapper: restMapper,
			Client:     dynamicClient,
			Scheme:     scheme,
		},
		EndpointNamespace: namespace,
		ClusterID:         t.engine.LocalEndPoint.Spec.ClusterID,
		NewPinger: func(ip string, i time.Duration, m uint) healthchecker.PingerInterface {
			defer GinkgoRecover()
			Expect(ip).To(Equal(t.pinger.GetIP()))
			return t.pinger
		},
	})

	t.endpoints = dynamicClient.Resource(*test.GetGroupVersionResourceFor(restMapper, &submarinerv1.Endpoint{})).Namespace(namespace)

	t.syncer = syncer.NewGatewaySyncer(t.engine, t.gateways, t.expectedGateway.Status.Version, t.healthChecker)

	informerFactory := submarinerInformers.NewSharedInformerFactory(client, 0)
	informer := informerFactory.Submariner().V1().Gateways().Informer()

	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			t.gatewayUpdated <- obj.(*submarinerv1.Gateway)
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			t.gatewayUpdated <- newObj.(*submarinerv1.Gateway)
		},
		DeleteFunc: func(obj interface{}) {
			t.gatewayDeleted <- obj.(*submarinerv1.Gateway)
		},
	})

	go informer.Run(t.stopInformer)
	Expect(cache.WaitForCacheSync(t.stopInformer, informer.HasSynced)).To(BeTrue())

	t.syncer.Run(t.stopSyncer)

	Expect(t.healthChecker.Start(t.stopSyncer)).To(Succeed())
}

func (t *testDriver) stop() {
	close(t.stopSyncer)

	if t.expectedDeletedAfter != nil {
		t.awaitGatewayDeleted(t.expectedDeletedAfter)
	}

	close(t.stopInformer)
	utilruntime.ErrorHandlers = t.savedErrorHandlers
}

func (t *testDriver) awaitGatewayUpdated(expected *submarinerv1.Gateway) {
	t.awaitGateway(t.gatewayUpdated, expected)
}

func (t *testDriver) awaitNoGatewayUpdated() {
	Consistently(t.gatewayUpdated, syncer.GatewayUpdateInterval+50).ShouldNot(Receive(), "Gateway was unexpectedly received")
}

func (t *testDriver) awaitGatewayDeleted(expected *submarinerv1.Gateway) {
	t.awaitGateway(t.gatewayDeleted, expected)
}

func (t *testDriver) awaitNoGatewayDeleted() {
	Consistently(t.gatewayDeleted, syncer.GatewayUpdateInterval+50).ShouldNot(Receive(), "Gateway was unexpectedly deleted")
}

func (t *testDriver) awaitGateway(gatewayChan chan *submarinerv1.Gateway, expected *submarinerv1.Gateway) {
	var last *submarinerv1.Gateway

	Eventually(func() *submarinerv1.Gateway {
		select {
		case gw := <-gatewayChan:
			last = gw
			return gw
		default:
			return last
		}
	}, 5).Should(equalGateway(expected))
}

func TestSyncer(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Cable engine syncer Suite")
}

type equalGatewayMatcher struct {
	expected *submarinerv1.Gateway
}

func equalGateway(expected *submarinerv1.Gateway) gomegaTypes.GomegaMatcher {
	return &equalGatewayMatcher{expected}
}

func (m *equalGatewayMatcher) Match(x interface{}) (bool, error) {
	actual := x.(*submarinerv1.Gateway)
	if actual == nil {
		return false, nil
	}

	if actual.Name != m.expected.Name {
		return false, nil
	}

	if m.expected.Status.StatusFailure != "" {
		if !strings.Contains(actual.Status.StatusFailure, m.expected.Status.StatusFailure) {
			return false, nil
		}

		actual = actual.DeepCopy()
		actual.Status.StatusFailure = m.expected.Status.StatusFailure
	}

	return reflect.DeepEqual(actual.Status, m.expected.Status), nil
}

func (m *equalGatewayMatcher) FailureMessage(actual interface{}) string {
	return format.Message(actual, "to equal", m.expected)
}

func (m *equalGatewayMatcher) NegatedFailureMessage(actual interface{}) (message string) {
	return format.Message(actual, "not to equal", m.expected)
}
