package middleware

import (
	"context"
	"net/http"
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/smithy-go/middleware"
	smithyhttp "github.com/aws/smithy-go/transport/http"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

var expectedAgent = aws.SDKName + "/" + aws.SDKVersion + " os/" + getNormalizedOSName() + " lang/go#" + languageVersion + " md/GOOS#" + runtime.GOOS + " md/GOARCH#" + runtime.GOARCH

func TestRequestUserAgent_HandleBuild(t *testing.T) {
	cases := map[string]struct {
		Env    map[string]string
		In     middleware.BuildInput
		Next   func(*testing.T, middleware.BuildInput) middleware.BuildHandler
		Expect middleware.BuildInput
		Err    bool
	}{
		"adds product information": {
			In: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{}},
			}},
			Expect: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{
					"User-Agent": {expectedAgent},
					//"X-Amz-User-Agent": {expectedSDKAgent},
				}},
			}},
			Next: func(t *testing.T, expect middleware.BuildInput) middleware.BuildHandler {
				return middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
					if diff := cmp.Diff(input, expect, cmpopts.IgnoreUnexported(http.Request{}, smithyhttp.Request{})); len(diff) > 0 {
						t.Error(diff)
					}
					return o, m, err
				})
			},
		},
		"appends to existing": {
			In: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{
					"User-Agent": {"previously set"},
					//"X-Amz-User-Agent": {"previously set"},
				}},
			}},
			Expect: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{
					"User-Agent": {expectedAgent + " previously set"},
					//"X-Amz-User-Agent": {expectedSDKAgent + " previously set"},
				}},
			}},
			Next: func(t *testing.T, expect middleware.BuildInput) middleware.BuildHandler {
				return middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
					if diff := cmp.Diff(input, expect, cmpopts.IgnoreUnexported(http.Request{}, smithyhttp.Request{})); len(diff) > 0 {
						t.Error(diff)
					}
					return o, m, err
				})
			},
		},
		"adds exec-env if present": {
			Env: map[string]string{
				execEnvVar: "TestCase",
			},
			In: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{}},
			}},
			Expect: middleware.BuildInput{Request: &smithyhttp.Request{
				Request: &http.Request{Header: map[string][]string{
					"User-Agent": {expectedAgent + " exec-env/TestCase"},
					//"X-Amz-User-Agent": {expectedSDKAgent + " exec-env/TestCase"},
				}},
			}},
			Next: func(t *testing.T, expect middleware.BuildInput) middleware.BuildHandler {
				return middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
					if diff := cmp.Diff(input, expect, cmpopts.IgnoreUnexported(http.Request{}, smithyhttp.Request{})); len(diff) > 0 {
						t.Error(diff)
					}
					return o, m, err
				})
			},
		},
		"errors for unknown type": {
			In: middleware.BuildInput{Request: struct{}{}},
			Next: func(t *testing.T, input middleware.BuildInput) middleware.BuildHandler {
				return nil
			},
			Err: true,
		},
	}

	for name, tt := range cases {
		t.Run(name, func(t *testing.T) {
			restoreEnv := clearEnv()
			defer restoreEnv()
			for k, v := range tt.Env {
				os.Setenv(k, v)
			}

			b := newRequestUserAgent()
			_, _, err := b.HandleBuild(context.Background(), tt.In, tt.Next(t, tt.Expect))
			if (err != nil) != tt.Err {
				t.Errorf("error %v, want error %v", err, tt.Err)
				return
			}
		})
	}
}

func clearEnv() func() {
	environ := os.Environ()
	os.Clearenv()
	return func() {
		os.Clearenv()
		for _, v := range environ {
			split := strings.SplitN(v, "=", 2)
			key, value := split[0], split[1]
			os.Setenv(key, value)
		}
	}
}

func TestAddUserAgentKey(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		Key    string
		Expect string
	}{
		"Simple key": {
			Key:    "baz",
			Expect: expectedAgent + " baz",
		},
		"Key containing slash": {
			Key:    "baz/ba",
			Expect: expectedAgent + " baz-ba",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			b := newRequestUserAgent()
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			err := stack.Build.Add(b, middleware.After)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			err = AddUserAgentKey(c.Key)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			_, _, err = b.HandleBuild(context.Background(), bi, middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return o, m, err
			}))
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}

func TestAddUserAgentKeyValue(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		Key    string
		Value  string
		Expect string
	}{
		"Simple key value pair": {
			Key:    "foo",
			Value:  "a+b-C$4'5.6",
			Expect: expectedAgent + " foo/a+b-C$4'5.6",
		},
		"Value containing invalid rune": {
			Key:    "foo",
			Value:  "1(2)3",
			Expect: expectedAgent + " foo/1-2-3",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			b := newRequestUserAgent()
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			err := stack.Build.Add(b, middleware.After)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			err = AddUserAgentKeyValue(c.Key, c.Value)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			_, _, err = b.HandleBuild(context.Background(), bi, middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return o, m, err
			}))
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}

func TestAddSDKAgentKey(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		KeyType SDKAgentKeyType
		Key     string
		Expect  string
	}{
		"Additional metadata key type": {
			KeyType: AdditionalMetadata,
			Key:     "baz",
			Expect:  expectedAgent + " md/baz",
		},
		"Config metadata key type": {
			KeyType: ConfigMetadata,
			Key:     "foo",
			Expect:  expectedAgent + " cfg/foo",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			b := newRequestUserAgent()
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			err := stack.Build.Add(b, middleware.After)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			err = AddSDKAgentKey(c.KeyType, c.Key)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			_, _, err = b.HandleBuild(context.Background(), bi, middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return o, m, err
			}))
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}

func TestAddSDKAgentKeyValue(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		KeyType SDKAgentKeyType
		Key     string
		Value   string
		Expect  string
	}{
		"Value containing valid chars": {
			KeyType: AdditionalMetadata,
			Key:     "baz",
			Value:   "a+b-C$4'5.6",
			Expect:  expectedAgent + " md/baz#a+b-C$4'5.6",
		},
		"Value containing invalid chars": {
			KeyType: ConfigMetadata,
			Key:     "foo",
			Value:   "1(2)3",
			Expect:  expectedAgent + " cfg/foo#1-2-3",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			b := newRequestUserAgent()
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			err := stack.Build.Add(b, middleware.After)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			err = AddSDKAgentKeyValue(c.KeyType, c.Key, c.Value)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			_, _, err = b.HandleBuild(context.Background(), bi, middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return o, m, err
			}))
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}

func TestAddUserAgentKey_AddToStack(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		Key    string
		Expect string
	}{
		"Simple key": {
			Key:    "baz",
			Expect: expectedAgent + " baz",
		},
		"Key containing slash": {
			Key:    "baz/ba",
			Expect: expectedAgent + " baz-ba",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			stack.Build.Add(middleware.BuildMiddlewareFunc("testInit", func(ctx context.Context, input middleware.BuildInput, handler middleware.BuildHandler) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return handler.HandleBuild(ctx, bi)
			}), middleware.After)
			err := AddUserAgentKey(c.Key)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			_, _, err = middleware.DecorateHandler(middleware.HandlerFunc(func(ctx context.Context, input interface{}) (output interface{}, metadata middleware.Metadata, err error) {
				return output, metadata, err
			}), stack).Handle(context.Background(), nil)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}

func TestAddUserAgentKeyValue_AddToStack(t *testing.T) {
	restoreEnv := clearEnv()
	defer restoreEnv()

	cases := map[string]struct {
		Key    string
		Value  string
		Expect string
	}{
		"Simple key value pair": {
			Key:    "foo",
			Value:  "a+b-C$4'5.6",
			Expect: expectedAgent + " foo/a+b-C$4'5.6",
		},
		"Value containing invalid rune": {
			Key:    "foo",
			Value:  "1(2)3",
			Expect: expectedAgent + " foo/1-2-3",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {
			stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
			bi := middleware.BuildInput{Request: &smithyhttp.Request{Request: &http.Request{Header: map[string][]string{}}}}
			stack.Build.Add(middleware.BuildMiddlewareFunc("testInit", func(ctx context.Context, input middleware.BuildInput, handler middleware.BuildHandler) (o middleware.BuildOutput, m middleware.Metadata, err error) {
				return handler.HandleBuild(ctx, bi)
			}), middleware.After)
			err := AddUserAgentKeyValue(c.Key, c.Value)(stack)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}

			_, _, err = middleware.DecorateHandler(middleware.HandlerFunc(func(ctx context.Context, input interface{}) (output interface{}, metadata middleware.Metadata, err error) {
				return output, metadata, err
			}), stack).Handle(context.Background(), nil)
			if err != nil {
				t.Fatalf("expect no error, got %v", err)
			}
			ua, ok := bi.Request.(*smithyhttp.Request).Header["User-Agent"]
			if !ok {
				t.Fatalf("expect User-Agent to be present")
			}
			if ua[0] != c.Expect {
				t.Error("User-Agent did not match expected")
			}
		})
	}
}
