/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You may not
 * use this file except in compliance with the License. A copy of the
 * License is located at
 *
 * http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package httpresource

import (
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"reflect"
	"testing"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	filemock "github.com/aws/amazon-ssm-agent/agent/fileutil/filemanager/mock"
	"github.com/aws/amazon-ssm-agent/agent/mocks/context"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/httpresource/handler"
	httpMock "github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/httpresource/handler/mock"
	"github.com/aws/amazon-ssm-agent/agent/plugins/downloadcontent/types"
	bridgemock "github.com/aws/amazon-ssm-agent/agent/ssm/ssmparameterresolver/mock"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

var contextMock = context.NewMockDefault()
var logMock = contextMock.Log()
var bm = bridgemock.GetSsmParamResolverBridge(map[string]string{})

func getString(obj interface{}) string {
	return fmt.Sprintf("%v", obj)
}

func getHttpInfo(url, authMethod, user, password string, allowInsecureDownload bool) HTTPInfo {
	return HTTPInfo{
		URL:                   types.NewTrimmedString(url),
		AuthMethod:            types.NewTrimmedString(authMethod),
		Username:              types.NewTrimmedString(user),
		Password:              types.NewTrimmedString(password),
		AllowInsecureDownload: allowInsecureDownload,
	}
}

func getTestResource(url url.URL, authMethod, user, password string, allowInsecureDownload bool) HTTPResource {
	httpClient := http.Client{}
	httpClient.CloseIdleConnections()

	return HTTPResource{
		context: contextMock,
		Handler: handler.NewHTTPHandler(httpClient, url, allowInsecureDownload, handler.HTTPAuthConfig{
			AuthMethod: types.NewTrimmedString(authMethod),
			Username:   types.NewTrimmedString(user),
			Password:   types.NewTrimmedString(password),
		}, bm),
	}
}

func TestNewHTTPResource(t *testing.T) {
	testResource := getTestResource(url.URL{
		Scheme: "http",
		Host:   "example.com",
	}, "Basic", "admin", "pwd", true)

	tests := []struct {
		sourceInfo string
		resource   *HTTPResource
		err        error
	}{
		{
			`{
				"url": "http://example.com",
				"authMethod": "Basic",
				"username": "admin",
				"password": "pwd",
				"allowInsecureDownload": true
			}`,
			&testResource,
			nil,
		},
		{
			`{
				"url": "http://
			}`,
			nil,
			errors.New("SourceInfo could not be unmarshalled for source type HTTP: " +
				"invalid character '\\n' in string literal"),
		},
		{
			`{
				"url": "http:// invalid-url"
			}`,
			nil,
			errors.New("Invalid URL format: " +
				"parse \"http:// invalid-url\": invalid character \" \" in host name"),
		},
	}

	for _, test := range tests {
		resource, err := NewHTTPResource(contextMock, test.sourceInfo, bm)

		if test.err != nil {
			assert.Error(t, err, getString(test))
			assert.EqualError(t, err, test.err.Error(), getString(test))
			assert.Nil(t, resource, getString(test))
		} else {
			assert.NoError(t, err, getString(test))
			assert.True(t, reflect.DeepEqual(test.resource, resource), getString(test))
		}
	}
}

func TestHTTPResource_parseSourceInfo(t *testing.T) {
	tests := []struct {
		sourceInfo string
		httpInfo   HTTPInfo
		err        error
	}{
		{
			`{
				"url": "http://",
				"authMethod": " Basic",
				"username": "admin ",
				"password": " pwd ",
				"allowInsecureDownload": false
			}`,
			getHttpInfo("http://", "Basic", "admin", "pwd", false),
			nil,
		},
		{
			`{
				"url": "http://",
				"allowInsecureDownload": true
			}`,
			getHttpInfo("http://", "", "", "", true),
			nil,
		},
		{
			`{
				"url": "http://"
			}`,
			getHttpInfo("http://", "", "", "", false),
			nil,
		},
		{
			`{
				"url": "http://
			}`,
			HTTPInfo{},
			errors.New("SourceInfo could not be unmarshalled for source type HTTP: " +
				"invalid character '\\n' in string literal"),
		},
	}

	for _, test := range tests {
		httpInfo, err := parseSourceInfo(test.sourceInfo)

		if test.err != nil {
			assert.Equal(t, HTTPInfo{}, httpInfo, getString(test))
			assert.Error(t, err, getString(test), getString(test))
			assert.EqualError(t, err, test.err.Error(), getString(test))
		} else {
			assert.NoError(t, err, getString(test), getString(test))
			assert.Equal(t, test.httpInfo, httpInfo, getString(test), getString(test))
		}
	}
}

func TestHTTPResource_adjustDownloadPath(t *testing.T) {
	testResource := getTestResource(url.URL{}, "", "", "", false)

	tests := []struct {
		givenPath    string
		fileSuffix   string
		downloadPath string
		pathExists   bool
		isDirectory  bool
	}{
		{
			"",
			"",
			filepath.Join(appconfig.DownloadRoot, "download"),
			true,
			false,
		},
		{
			"",
			"123",
			filepath.Join(appconfig.DownloadRoot, "download123"),
			false,
			true,
		},
		{
			filepath.Join("/tmp", "download.txt"),
			"",
			filepath.Join("/tmp", "download.txt"),
			false,
			false,
		},
		{
			"/tmp/",
			"123",
			filepath.Join("/tmp", "download123"),
			false,
			false,
		},
		{
			"/tmp/",
			"123",
			filepath.Join("/tmp", "download123"),
			true,
			true,
		},
		{
			"/tmp",
			"123",
			filepath.Join("/tmp", "download123"),
			true,
			true,
		},

		{
			"/tmp",
			"123",
			filepath.Join("/tmp"),
			false,
			false,
		},
	}

	for _, test := range tests {
		fileSystemMock := filemock.FileSystemMock{}
		fileSystemMock.On("Exists", mock.Anything).Return(test.pathExists)
		fileSystemMock.On("IsDirectory", mock.Anything).Return(test.isDirectory)

		downloadPath := testResource.adjustDownloadPath(test.givenPath, test.fileSuffix, &fileSystemMock)
		assert.Equal(t, test.downloadPath, filepath.Join(downloadPath), getString(test))
	}
}

func TestHTTPResource_ValidateLocationInfo(t *testing.T) {
	httpHandlerMock := httpMock.HTTPHandlerMock{}
	httpHandlerMock.On("Validate").Return(true, nil).Once()

	resource := HTTPResource{
		context: contextMock,
		Handler: &httpHandlerMock,
	}

	_, _ = resource.ValidateLocationInfo()
	httpHandlerMock.AssertExpectations(t)
}

func TestHTTPResource_DownloadRemoteResource(t *testing.T) {
	destPath := filepath.Join(os.TempDir(), "testFile")

	fileSystemMock := &filemock.FileSystemMock{}
	fileSystemMock.On("MakeDirs", filepath.Dir(destPath)).Return(nil)
	fileSystemMock.On("Exists", destPath).Return(true)
	fileSystemMock.On("IsDirectory", destPath).Return(false)

	httpHandlerMock := httpMock.HTTPHandlerMock{}
	httpHandlerMock.On("Download", logMock, fileSystemMock, destPath).Return(destPath, nil).Once()

	resource := HTTPResource{
		context: contextMock,
		Handler: &httpHandlerMock,
	}
	err, result := resource.DownloadRemoteResource(fileSystemMock, destPath)

	assert.NoError(t, err)
	assert.Equal(t, []string{destPath}, result.Files)

	fileSystemMock.AssertExpectations(t)
	httpHandlerMock.AssertExpectations(t)
}
