package metrics

import (
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"github.com/stretchr/testify/require"
)

const (
	MetricTypeSuccess     = "Success"
	MetricTypeServerError = "ServerError"
	MetricTypeClientError = "ClientError"
)

const (
	clientServiceName   = "Users Service"
	clientOperationName = "GetUser"
	serverOperationName = "Host"
)

type testCase struct {
	description                  string
	statusCode                   int
	expectedThroughputMetricType string
}

func TestRequests(t *testing.T) {
	testCases := []*testCase{
		{
			description:                  "should handle successful requests",
			statusCode:                   http.StatusOK,
			expectedThroughputMetricType: MetricTypeSuccess,
		}, {
			description:                  "should handle client errors",
			statusCode:                   http.StatusBadRequest,
			expectedThroughputMetricType: MetricTypeClientError,
		}, {
			description:                  "should handle unprocessable entity errors (422s)",
			statusCode:                   http.StatusUnprocessableEntity,
			expectedThroughputMetricType: MetricTypeClientError,
		}, {
			description:                  "should handle server errors",
			statusCode:                   http.StatusInternalServerError,
			expectedThroughputMetricType: MetricTypeServerError,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			responseText := "response body"

			// Set up: Spin up a test http server that represent's the dependency's server. (E.g. users service.)
			dependencyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(tc.statusCode)
				fmt.Fprintln(w, responseText)
			}))
			defer dependencyServer.Close()

			// Set up: Create a client that will communicate with the dependency server.
			sampleObserver := &StubSampleObserver{}
			client, err := newStubTwitchClient(sampleObserver, dependencyServer.URL)
			require.NoError(t, err)

			// A client operation is done in the context of a larger server operation.
			// Our server is expected to set that operation on the context, and twitch telemetry
			// will include it on the metrics it sends.
			ctx := telemetry.ContextWithOperationName(context.Background(), serverOperationName)

			// Do the client operation.
			resp, statusCode, sendErr := client.GetUser(ctx)

			// Verify that the response is what we expect.
			// (Our middleware should not prevent the request from being sent, and the response from being read.)
			require.NoError(t, sendErr)
			require.Equal(t, tc.statusCode, statusCode)
			require.Equal(t, responseText, resp)

			// Verify that the middleware emitted the metrics that we expect.
			requireCorrectSamplesSent(t, sampleObserver, tc.expectedThroughputMetricType)
		})
	}
}

func TestRequestTimeout(t *testing.T) {
	ctx := telemetry.ContextWithOperationName(context.Background(), serverOperationName)
	expiredCtx, cancel := context.WithTimeout(ctx, time.Duration(0))
	defer cancel()

	// Set up: Spin up a test http server that will successfully process requests.
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "response body")
	}))
	defer ts.Close()

	// Set up: Create a client that will communicate with the server.
	sampleObserver := &StubSampleObserver{}
	client, err := newStubTwitchClient(sampleObserver, ts.URL)
	require.NoError(t, err)

	// Send the request
	_, _, sendErr := client.GetUser(expiredCtx)

	// Verify that waiting for a response failed with a timeout error.
	require.Error(t, sendErr)
	require.Contains(t, sendErr.Error(), context.DeadlineExceeded.Error())

	// Verify that the middleware emitted the metrics that we expect.
	requireCorrectSamplesSent(t, sampleObserver, MetricTypeServerError)
}

func requireCorrectSamplesSent(
	t *testing.T,
	sampleObserver *StubSampleObserver,
	expectedLatencyMetricType string) {

	successValue := 0.0
	serverErrorValue := 0.0
	clientErrorValue := 0.0
	switch expectedLatencyMetricType {
	case MetricTypeSuccess:
		successValue = 1.0
	case MetricTypeServerError:
		serverErrorValue = 1.0
	case MetricTypeClientError:
		clientErrorValue = 1.0
	}

	expectedDims := map[string]string{
		telemetry.DimensionOperation:  serverOperationName,
		telemetry.DimensionDependency: fmt.Sprintf("twitchclient/%s:%s", clientServiceName, clientOperationName),
	}
	samples, err := sampleObserver.GetSamplesWithValue(telemetry.MetricDependencySuccess, expectedDims, successValue)
	require.NoError(t, err, sampleObserver.String())
	require.Len(t, samples, 1)

	samples, err = sampleObserver.GetSamplesWithValue(telemetry.MetricDependencyServerError, expectedDims, serverErrorValue)
	require.NoError(t, err, sampleObserver.String())
	require.Len(t, samples, 1)

	samples, err = sampleObserver.GetSamplesWithValue(telemetry.MetricDependencyClientError, expectedDims, clientErrorValue)
	require.NoError(t, err, sampleObserver.String())
	require.Len(t, samples, 1)

	samples, err = sampleObserver.GetSamplesWithPositiveValue(telemetry.MetricDependencyDuration, expectedDims)
	require.NoError(t, err, sampleObserver.String())
	require.Len(t, samples, 1)
}

// A TwitchClient-like (legacy) client that is set up with the TwitchClientMiddleware.
type stubTwitchClient struct {
	url    *url.URL
	client *http.Client
}

func newStubTwitchClient(sampleObserver *StubSampleObserver, endpoint string) (*stubTwitchClient, error) {
	// Set up an HTTP client that is instrumented using the middleware.
	sampleReporter := NewSampleReporter(&SampleReporterConfig{
		Environment:    "development",
		Region:         "us-west-2",
		ServiceName:    "Autohost Server",
		OverrideSender: sampleObserver,
	})
	wrapper := NewTwitchClientMiddlewareWrapper(&TwitchClientMiddlewareWrapperConfig{
		SampleReporter: sampleReporter,
		AutoFlush:      true,
	})
	wrappedTransport := wrapper(http.DefaultTransport)
	httpClient := &http.Client{
		Transport: wrappedTransport,
	}

	u, err := url.Parse(endpoint)
	if err != nil {
		return nil, err
	}

	return &stubTwitchClient{
		url:    u,
		client: httpClient,
	}, nil
}

func (c *stubTwitchClient) GetUser(ctx context.Context) (string, int, error) {
	// Pass in the service name and operation name to the middleware as values on the context.
	ctx = WithTwitchClientOperation(ctx, clientServiceName, clientOperationName)

	// Twitch clients usually have code that prepares an HTTP request, sends the request,
	// and reads and parses the response.
	request := &http.Request{
		Method: http.MethodGet,
		URL:    c.url,
	}
	request = request.WithContext(ctx)

	resp, err := c.client.Do(request)
	if err != nil {
		return "", 0, err
	}

	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", resp.StatusCode, err
	}

	body := strings.TrimSpace(string(bodyBytes))
	return body, resp.StatusCode, nil
}
