package public

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"testing"

	"github.com/golang/protobuf/proto"
	pb "github.com/linkerd/linkerd2/controller/gen/public"
	"github.com/linkerd/linkerd2/pkg/protohttp"
)

type mockTransport struct {
	responseToReturn *http.Response
	requestSent      *http.Request
	errorToReturn    error
}

func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	m.requestSent = req
	return m.responseToReturn, m.errorToReturn
}

func TestNewInternalClient(t *testing.T) {
	t.Run("Makes a well-formed request over the Kubernetes public API", func(t *testing.T) {
		mockTransport := &mockTransport{}
		mockTransport.responseToReturn = &http.Response{
			StatusCode: 200,
			Body:       ioutil.NopCloser(bufferedReader(t, &pb.Empty{})),
		}
		mockHTTPClient := &http.Client{
			Transport: mockTransport,
		}

		apiURL := &url.URL{
			Scheme: "http",
			Host:   "some-hostname",
			Path:   "/",
		}
		client, err := newClient(apiURL, mockHTTPClient, "linkerd")
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}

		_, err = client.Version(context.Background(), &pb.Empty{})
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}

		expectedURLRequested := "http://some-hostname/api/v1/Version"
		actualURLRequested := mockTransport.requestSent.URL.String()
		if actualURLRequested != expectedURLRequested {
			t.Fatalf("Expected request to URL [%v], but got [%v]", expectedURLRequested, actualURLRequested)
		}
	})
}

func TestFromByteStreamToProtocolBuffers(t *testing.T) {
	t.Run("Correctly marshalls an valid object", func(t *testing.T) {
		versionInfo := pb.VersionInfo{
			GoVersion:      "1.9.1",
			BuildDate:      "2017.11.17",
			ReleaseVersion: "1.2.3",
		}

		var protobufMessageToBeFilledWithData pb.VersionInfo
		reader := bufferedReader(t, &versionInfo)

		err := protohttp.FromByteStreamToProtocolBuffers(reader, &protobufMessageToBeFilledWithData)
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}

		if !proto.Equal(&protobufMessageToBeFilledWithData, &versionInfo) {
			t.Fatalf("mismatch, %+v != %+v", protobufMessageToBeFilledWithData, versionInfo)
		}
	})

	t.Run("Correctly marshalls a large byte array", func(t *testing.T) {
		rows := make([]*pb.StatTable_PodGroup_Row, 0)

		numberOfResourcesInMessage := 400
		for i := 0; i < numberOfResourcesInMessage; i++ {
			rows = append(rows, &pb.StatTable_PodGroup_Row{
				Resource: &pb.Resource{
					Namespace: "default",
					Name:      fmt.Sprintf("deployment%d", i),
					Type:      "deployment",
				},
			})
		}

		msg := pb.StatSummaryResponse{
			Response: &pb.StatSummaryResponse_Ok_{
				Ok: &pb.StatSummaryResponse_Ok{
					StatTables: []*pb.StatTable{
						{
							Table: &pb.StatTable_PodGroup_{
								PodGroup: &pb.StatTable_PodGroup{
									Rows: rows,
								},
							},
						},
					},
				},
			},
		}

		reader := bufferedReader(t, &msg)

		protobufMessageToBeFilledWithData := &pb.StatSummaryResponse{}
		err := protohttp.FromByteStreamToProtocolBuffers(reader, protobufMessageToBeFilledWithData)
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}
	})

	t.Run("When byte array contains error, treats stream as regular protobuf object", func(t *testing.T) {
		apiError := pb.ApiError{Error: "an error occurred"}

		var protobufMessageToBeFilledWithData pb.ApiError
		reader := bufferedReader(t, &apiError)
		err := protohttp.FromByteStreamToProtocolBuffers(reader, &protobufMessageToBeFilledWithData)
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}

		expectedErrorMessage := apiError.Error
		actualErrorMessage := protobufMessageToBeFilledWithData.Error
		if actualErrorMessage != expectedErrorMessage {
			t.Fatalf("Expected object to contain message [%s], but got [%s]", expectedErrorMessage, actualErrorMessage)
		}
	})

	t.Run("Returns error if byte stream contains wrong object", func(t *testing.T) {
		versionInfo := &pb.VersionInfo{
			GoVersion:      "1.9.1",
			BuildDate:      "2017.11.17",
			ReleaseVersion: "1.2.3",
		}

		reader := bufferedReader(t, versionInfo)

		protobufMessageToBeFilledWithData := &pb.StatSummaryResponse{}
		err := protohttp.FromByteStreamToProtocolBuffers(reader, protobufMessageToBeFilledWithData)
		if err == nil {
			t.Fatal("Expecting error, got nothing")
		}
	})
}

func bufferedReader(t *testing.T, msg proto.Message) *bufio.Reader {
	msgBytes, err := proto.Marshal(msg)
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	payload := protohttp.SerializeAsPayload(msgBytes)

	return bufio.NewReader(bytes.NewReader(payload))
}
