package tap

import (
	"context"
	"fmt"
	"net"
	"reflect"
	"strconv"
	"testing"

	proxy "github.com/linkerd/linkerd2-proxy-api/go/tap"
	"github.com/linkerd/linkerd2/controller/api/util"
	"github.com/linkerd/linkerd2/controller/gen/public"
	"github.com/linkerd/linkerd2/controller/k8s"
	"github.com/linkerd/linkerd2/pkg/addr"
	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

type tapExpected struct {
	err       error
	k8sRes    []string
	req       public.TapByResourceRequest
	requireID string
}

// mockTapByResourceServer satisfies controller.tap.Tap_TapByResourceServer
type mockTapByResourceServer struct {
	util.MockServerStream
}

func (m *mockTapByResourceServer) Send(event *public.TapEvent) error {
	return nil
}

// mockProxyTapServer satisfies proxy.tap.TapServer
type mockProxyTapServer struct {
	mockControllerServer mockTapByResourceServer // for cancellation
	ctx                  context.Context
}

func (m *mockProxyTapServer) Observe(req *proxy.ObserveRequest, obsSrv proxy.Tap_ObserveServer) error {
	m.ctx = obsSrv.Context()
	m.mockControllerServer.Cancel()
	return nil
}

func TestTapByResource(t *testing.T) {
	expectations := []tapExpected{
		{
			err:    status.Error(codes.InvalidArgument, "TapByResource received nil target ResourceSelection"),
			k8sRes: []string{},
			req:    public.TapByResourceRequest{},
		},
		{
			err: status.Errorf(codes.Unimplemented, "unexpected match specified: any:<> "),
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
    linkerd.io/control-plane-ns: controller-ns
  annotations:
    linkerd.io/proxy-version: testinjectversion
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed",
					},
				},
				Match: &public.TapByResourceRequest_Match{
					Match: &public.TapByResourceRequest_Match_Any{
						Any: &public.TapByResourceRequest_Match_Seq{},
					},
				},
			},
		},
		{
			err: status.Errorf(codes.NotFound, "no pods found for pod/emojivoto-not-meshed"),
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-not-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-not-meshed",
					},
				},
			},
		},
		{
			err:    status.Errorf(codes.Unimplemented, "unimplemented resource type: bad-type"),
			k8sRes: []string{},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      "bad-type",
						Name:      "emojivoto-meshed-not-found",
					},
				},
			},
		},
		{
			err: status.Errorf(codes.NotFound, "pod \"emojivoto-meshed-not-found\" not found"),
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  annotations:
    linkerd.io/proxy-version: testinjectversion
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed-not-found",
					},
				},
			},
		},
		{
			err: status.Errorf(codes.NotFound, "no pods found for pod/emojivoto-meshed"),
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  annotations:
    linkerd.io/proxy-version: testinjectversion
status:
  phase: Finished
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed",
					},
				},
			},
		},
		{
			err: status.Errorf(codes.NotFound, "all pods found for pod/emojivoto-meshed-tap-disabled have tapping disabled"),
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-tap-disabled
  namespace: emojivoto
  labels:
    app: emoji-svc
    linkerd.io/control-plane-ns: controller-ns
  annotations:
    config.linkerd.io/disable-tap: "true"
    linkerd.io/proxy-version: testinjectversion
status:
  phase: Running
  podIP: 127.0.0.1
    `,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed-tap-disabled",
					},
				},
				Match: &public.TapByResourceRequest_Match{
					Match: &public.TapByResourceRequest_Match_All{
						All: &public.TapByResourceRequest_Match_Seq{},
					},
				},
			},
		},
		{
			// success, underlying tap events tested in http_server_test.go
			err: nil,
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
    linkerd.io/control-plane-ns: controller-ns
  annotations:
    linkerd.io/proxy-version: testinjectversion
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed",
					},
				},
				Match: &public.TapByResourceRequest_Match{
					Match: &public.TapByResourceRequest_Match_All{
						All: &public.TapByResourceRequest_Match_Seq{},
					},
				},
			},
			requireID: ".emojivoto.serviceaccount.identity.controller-ns.cluster.local",
		},
		{
			err: nil,
			k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
    linkerd.io/control-plane-ns: controller-ns
  annotations:
    linkerd.io/proxy-version: testinjectversion
spec:
  serviceAccountName: emojivoto-meshed-sa
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "emojivoto",
						Type:      pkgK8s.Pod,
						Name:      "emojivoto-meshed",
					},
				},
				Match: &public.TapByResourceRequest_Match{
					Match: &public.TapByResourceRequest_Match_All{
						All: &public.TapByResourceRequest_Match_Seq{},
					},
				},
			},
			requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
		},
		{
			err: nil,
			k8sRes: []string{`
apiVersion: v1
kind: Namespace
metadata:
  name: emojivoto
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
    linkerd.io/control-plane-ns: controller-ns
  annotations:
    linkerd.io/proxy-version: testinjectversion
spec:
  serviceAccountName: emojivoto-meshed-sa
status:
  phase: Running
  podIP: 127.0.0.1
`,
			},
			req: public.TapByResourceRequest{
				Target: &public.ResourceSelection{
					Resource: &public.Resource{
						Namespace: "",
						Type:      pkgK8s.Namespace,
						Name:      "emojivoto",
					},
				},
				Match: &public.TapByResourceRequest_Match{
					Match: &public.TapByResourceRequest_Match_All{
						All: &public.TapByResourceRequest_Match_Seq{},
					},
				},
			},
			requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
		},
	}

	for i, exp := range expectations {
		exp := exp // pin
		t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {

			k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
			if err != nil {
				t.Fatalf("NewFakeAPI returned an error: %s", err)
			}

			stream := mockTapByResourceServer{
				MockServerStream: util.NewMockServerStream(),
			}

			s := grpc.NewServer()

			mockProxyTapServer := mockProxyTapServer{
				mockControllerServer: stream,
			}
			proxy.RegisterTapServer(s, &mockProxyTapServer)

			lis, err := net.Listen("tcp", ":0")
			if err != nil {
				t.Fatalf("Failed to listen")
			}

			// TODO: mock out the underlying grpc tap events
			errChan := make(chan error, 1)
			go func() {
				errChan <- s.Serve(lis)
			}()

			defer func() {
				if err := <-errChan; err != nil {
					t.Fatalf("Failed to serve on %+v: %s", lis, err)
				}
			}()

			defer s.GracefulStop()

			_, port, err := net.SplitHostPort(lis.Addr().String())
			if err != nil {
				t.Fatal(err.Error())
			}

			tapPort, err := strconv.ParseUint(port, 10, 32)
			if err != nil {
				t.Fatalf("Invalid port: %s", port)
			}

			fakeGrpcServer := newGRPCTapServer(uint(tapPort), "controller-ns", "cluster.local", k8sAPI)

			k8sAPI.Sync(nil)

			err = fakeGrpcServer.TapByResource(&exp.req, &stream)
			if !reflect.DeepEqual(err, exp.err) {
				t.Fatalf("TapByResource returned unexpected: [%s], expected: [%s]", err, exp.err)
			}

			if exp.requireID != "" {
				md, ok := metadata.FromIncomingContext(mockProxyTapServer.ctx)
				if !ok {
					t.Fatalf("FromIncomingContext failed given: %+v", mockProxyTapServer.ctx)
				}

				if !reflect.DeepEqual(md.Get(requireIDHeader), []string{exp.requireID}) {
					t.Fatalf("Unexpected l5d-require-id header [%+v] expected [%+v]", md.Get(requireIDHeader), []string{exp.requireID})
				}
			}

		})
	}
}

func TestHydrateIPLabels(t *testing.T) {
	expectations := []struct {
		k8sRes      []string
		requestedIP string
		labels      map[string]string
	}{
		{
			// Requested IP that doesn't match node or any pod
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`,
			},
			requestedIP: "10.20.30.40",
			labels:      map[string]string{},
		},
		{
			// Requested IP that matches node only
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`,
			},
			requestedIP: "1.2.3.4",
			labels:      map[string]string{"node": "node1"},
		},
		{
			// Requested IP that matches node and pod
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 1.2.3.4
`,
			},
			requestedIP: "1.2.3.4",
			labels:      map[string]string{"node": "node1"},
		},
		{
			// Requested IP that doesn't match node and matches exactly one pod
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`,
			},
			requestedIP: "5.6.7.8",
			labels: map[string]string{
				"namespace":      "emojivoto",
				"pod":            "emojivoto-meshed",
				"serviceaccount": "default",
			},
		},
		{
			// Requested IP that doesn't match node and matches exactly one running pod and one finished pod
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-2
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Finished
  podIP: 5.6.7.8
`,
			},
			requestedIP: "5.6.7.8",
			labels: map[string]string{
				"namespace":      "emojivoto",
				"pod":            "emojivoto-meshed",
				"serviceaccount": "default",
			},
		},
		{
			// Requested IP that doesn't match node and matches two running pods
			k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
  name: node1
status:
  addresses:
  - address: 1.2.3.4
    type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`, `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-2
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running
  podIP: 5.6.7.8
`,
			},
			requestedIP: "5.6.7.8",
			labels:      map[string]string{},
		},
	}

	for i, exp := range expectations {
		exp := exp // pin
		t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {
			k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
			if err != nil {
				t.Fatalf("NewFakeAPI returned an error: %s", err)
			}
			s := NewGrpcTapServer(4190, "controller-ns", "cluster.local", k8sAPI)
			k8sAPI.Sync(nil)

			labels := make(map[string]string)
			ip, err := addr.ParsePublicIPV4(exp.requestedIP)
			if err != nil {
				t.Fatalf("Error parsing IP %s: %s", exp.requestedIP, err)
			}
			s.hydrateIPLabels(ip, labels)
			if !reflect.DeepEqual(labels, exp.labels) {
				t.Fatalf("Unexpected labels: [%#v], expected: [%#v]", labels, exp.labels)
			}
		})
	}
}
