package iomodule

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"testing"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	contextmocks "github.com/aws/amazon-ssm-agent/agent/mocks/context"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestFileS3Read(t *testing.T) {
	var TestInputCases = [...]string{
		"Test input text.",
		"A sample \ninput text.",
		"\b5Ὂg̀9! ℃ᾭG",
		"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In fermentum cursus mi, sed placerat tellus condimentum non. " +
			"Pellentesque vel volutpat velit. Sed eget varius nibh. Sed quis nisl enim. Nulla faucibus nisl a massa fermentum porttitor. " +
			"Integer at massa blandit, congue ligula ut, vulputate lacus. Morbi tempor tellus a tempus sodales. Nam at placerat odio, " +
			"ut placerat purus. Donec imperdiet venenatis orci eu mollis. Phasellus rhoncus bibendum lacus sit amet cursus. Aliquam erat" +
			" volutpat. Phasellus auctor ipsum vel efficitur interdum. Duis sed elit tempor, convallis lacus sed, accumsan mi. Integer" +
			" porttitor a nunc in porttitor. Vestibulum felis enim, pretium vel nulla vel, commodo mollis ex. Sed placerat mollis leo, " +
			"at varius eros elementum vitae. Nunc aliquet velit quis dui facilisis elementum. Etiam interdum lobortis nisi, vitae " +
			"convallis libero tincidunt at. Nam eu velit et velit dignissim aliquet facilisis id ipsum. Vestibulum hendrerit, arcu " +
			"id gravida facilisis, felis leo malesuada eros, non dignissim quam turpis a massa. ",
	}

	i := 0

	for _, testCase := range TestInputCases {
		file := File{
			FileName:               "file" + strconv.Itoa(i),
			OrchestrationDirectory: "testdata",
			OutputS3BucketName:     "s3Bucket",
			OutputS3KeyPrefix:      "s3KeyPrefix",
			LogGroupName:           "LogGroup",
			LogStreamName:          "LogStream",
		}
		outputFileExists := testFileS3Read(appconfig.DefaultPluginOutputRetention, testCase, file)
		assert.True(t, outputFileExists)
		i++
	}

}

func TestFileS3CleansUpAfterS3Upload(t *testing.T) {
	file := File{
		FileName:               "TestFileS3CleansUpAfterS3Upload",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "bucket-to-upload-to",
		OutputS3KeyPrefix:      "s3KeyPrefix",
		LogGroupName:           "",
		LogStreamName:          "",
	}

	outputFileExists := testFileS3Read(appconfig.PluginLocalOutputCleanupAfterUpload, "Test input text.", file)
	assert.False(t, outputFileExists)

}

func TestFileS3DoesntCleanUpAfterS3Upload(t *testing.T) {
	file := File{
		FileName:               "TestFileS3DoesntCleanUpAfterS3Upload",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "bucket-to-upload-to",
		OutputS3KeyPrefix:      "s3KeyPrefix",
		LogGroupName:           "",
		LogStreamName:          "",
	}

	r, w := io.Pipe()
	wg := new(sync.WaitGroup)
	var mockS3Util = &s3UtilMock{}
	config := appconfig.SsmagentConfig{}
	config.Ssm.PluginLocalOutputCleanup = appconfig.DefaultPluginOutputRetention
	var context = contextmocks.NewMockDefaultWithConfig(config)
	s3Key := fileutil.BuildS3Path(file.OutputS3KeyPrefix, file.FileName)
	filePath := filepath.Join(file.OrchestrationDirectory, file.FileName)
	mockS3Util.On("S3Upload", mock.AnythingOfType("*log.Mock"), file.OutputS3BucketName, s3Key, filePath).Return(fmt.Errorf("upload error"))

	var s3RetrieverMock = &s3LogsServiceRetrieverMock{}
	s3RetrieverMock.On("NewAmazonS3Util", mock.AnythingOfType("*context.Mock"), file.OutputS3BucketName).Return(mockS3Util, nil)
	s3ServiceRetriever = s3RetrieverMock

	var mockCWLoggingService = &cloudWatchLoggingServiceMock{}

	var cwRetrieverMock = &cloudWatchServiceRetrieverMock{}
	cwRetrieverMock.On("NewCloudWatchLogsService", mock.AnythingOfType("*context.Mock")).Return(mockCWLoggingService)
	cloudWatchServiceRetriever = cwRetrieverMock

	wg.Add(1)

	go func() {
		defer wg.Done()
		file.Read(context, r, appconfig.SuccessExitCode)
	}()

	w.Write([]byte("Test input text."))
	w.Close()
	wg.Wait()
	outputFileExists, _ := fileutil.LocalFileExist(filePath)
	if outputFileExists {
		os.Remove(filePath)
	}

	assert.True(t, outputFileExists)
}

func TestFileS3DoesntCleanUpAfterRebootExitCode(t *testing.T) {
	file := File{
		FileName:               "TestFileS3DoesntCleanUpAfterS3Upload",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "bucket-to-upload-to",
		OutputS3KeyPrefix:      "s3KeyPrefix",
		LogGroupName:           "",
		LogStreamName:          "",
	}

	config := appconfig.SsmagentConfig{}
	config.Ssm.PluginLocalOutputCleanup = appconfig.DefaultPluginOutputRetention
	var context = contextmocks.NewMockDefaultWithConfig(config)

	r, w := io.Pipe()
	wg := new(sync.WaitGroup)
	var mockS3Util = &s3UtilMock{}
	s3Key := fileutil.BuildS3Path(file.OutputS3KeyPrefix, file.FileName)
	filePath := filepath.Join(file.OrchestrationDirectory, file.FileName)
	mockS3Util.On("S3Upload", mock.AnythingOfType("*log.Mock"), file.OutputS3BucketName, s3Key, filePath).Return(nil)

	var s3RetrieverMock = &s3LogsServiceRetrieverMock{}
	s3RetrieverMock.On("NewAmazonS3Util", mock.AnythingOfType("*context.Mock"), file.OutputS3BucketName).Return(mockS3Util, nil)
	s3ServiceRetriever = s3RetrieverMock

	var mockCWLoggingService = &cloudWatchLoggingServiceMock{}

	var cwRetrieverMock = &cloudWatchServiceRetrieverMock{}
	cwRetrieverMock.On("NewCloudWatchLogsService", mock.AnythingOfType("*context.Mock")).Return(mockCWLoggingService)
	cloudWatchServiceRetriever = cwRetrieverMock

	wg.Add(1)

	go func() {
		defer wg.Done()
		file.Read(context, r, appconfig.RebootExitCode)
	}()

	w.Write([]byte("Test input text."))
	w.Close()
	wg.Wait()
	outputFileExists, _ := fileutil.LocalFileExist(filePath)
	if outputFileExists {
		os.Remove(filePath)
	}

	assert.True(t, outputFileExists)
}

func TestFileS3CleanUpAfterCloudWatchUpload(t *testing.T) {
	file := File{
		FileName:               "TestFileS3CleanUpAfterS3CloudwatchUpload",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "",
		OutputS3KeyPrefix:      "",
		LogGroupName:           "log-group",
		LogStreamName:          "log-stream",
	}

	outputFileExists := testFileS3Read(appconfig.PluginLocalOutputCleanupAfterUpload, "Test input text.", file)
	assert.False(t, outputFileExists)
}

func TestFileS3DoesntCleanUpAfterS3CloudwatchUpload(t *testing.T) {
	config := appconfig.SsmagentConfig{}
	config.Ssm.PluginLocalOutputCleanup = appconfig.PluginLocalOutputCleanupAfterUpload
	var context = contextmocks.NewMockDefaultWithConfig(config)

	file := File{
		FileName:               "TestFileS3CleanUpAfterS3CloudwatchUpload",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "",
		OutputS3KeyPrefix:      "",
		LogGroupName:           "log-group",
		LogStreamName:          "log-stream",
	}

	r, w := io.Pipe()
	wg := new(sync.WaitGroup)
	filePath := filepath.Join(file.OrchestrationDirectory, file.FileName)

	var mockCWLoggingService = &cloudWatchLoggingServiceMock{}
	mockCWLoggingService.On("StreamData",
		file.LogGroupName,
		file.LogStreamName,
		filePath,
		false,
		false,
		mock.AnythingOfType("chan bool"),
		false,
		false).Return(true)

	mockCWLoggingService.On("SetIsFileComplete", mock.AnythingOfType("bool")).Return()
	mockCWLoggingService.On("GetIsUploadComplete").Return(false).Times(62)

	var cwRetrieverMock = &cloudWatchServiceRetrieverMock{}
	cwRetrieverMock.On("NewCloudWatchLogsService", mock.AnythingOfType("*context.Mock")).Return(mockCWLoggingService)
	cloudWatchServiceRetriever = cwRetrieverMock

	wg.Add(1)

	go func() {
		defer wg.Done()
		file.Read(context, r, appconfig.SuccessExitCode)
	}()

	w.Write([]byte("Test input text."))
	w.Close()
	wg.Wait()
	outputFileExists, _ := fileutil.LocalFileExist(filePath)
	if outputFileExists {
		os.Remove(filePath)
	}

	assert.True(t, outputFileExists)
}

func TestFileS3CleansUpAfterExecution(t *testing.T) {
	file := File{
		FileName:               "TestFileS3CleansUpAfterExecution",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "",
		OutputS3KeyPrefix:      "",
		LogGroupName:           "",
		LogStreamName:          "",
	}

	outputFileExists := testFileS3Read(appconfig.PluginLocalOutputCleanupAfterExecution, "Test input text.", file)

	assert.False(t, outputFileExists)
}

func TestFileS3DefaultPluginOutputRetention(t *testing.T) {
	file := File{
		FileName:               "TestFileS3DefaultPluginOutputRetention",
		OrchestrationDirectory: "testdata",
		OutputS3BucketName:     "s3Bucket",
		OutputS3KeyPrefix:      "s3KeyPrefix",
		LogGroupName:           "LogGroup",
		LogStreamName:          "LogStream",
	}

	outputFileExists := testFileS3Read(appconfig.DefaultPluginOutputRetention, "Test input text.", file)

	assert.True(t, outputFileExists)
}

func testFileS3Read(pluginLocalOutputCleanupPref string, pipeTestCase string, file File) bool {
	config := appconfig.SsmagentConfig{}
	config.Ssm.PluginLocalOutputCleanup = pluginLocalOutputCleanupPref
	var context = contextmocks.NewMockDefaultWithConfig(config)

	r, w := io.Pipe()
	wg := new(sync.WaitGroup)
	var mockS3Util = &s3UtilMock{}
	s3Key := fileutil.BuildS3Path(file.OutputS3KeyPrefix, file.FileName)
	filePath := filepath.Join(file.OrchestrationDirectory, file.FileName)
	mockS3Util.On("S3Upload", mock.AnythingOfType("*log.Mock"), file.OutputS3BucketName, s3Key, filePath).Return(nil)

	var s3RetrieverMock = &s3LogsServiceRetrieverMock{}
	s3RetrieverMock.On("NewAmazonS3Util", mock.AnythingOfType("*context.Mock"), file.OutputS3BucketName).Return(mockS3Util, nil)
	s3ServiceRetriever = s3RetrieverMock

	var mockCWLoggingService = &cloudWatchLoggingServiceMock{}
	mockCWLoggingService.On("StreamData",
		file.LogGroupName,
		file.LogStreamName,
		filePath,
		false,
		false,
		mock.AnythingOfType("chan bool"),
		false,
		false).Return(true)

	mockCWLoggingService.On("SetIsFileComplete", mock.AnythingOfType("bool")).Return()
	mockCWLoggingService.On("GetIsUploadComplete").Return(true).Times(2)

	var cwRetrieverMock = &cloudWatchServiceRetrieverMock{}
	cwRetrieverMock.On("NewCloudWatchLogsService", mock.AnythingOfType("*context.Mock")).Return(mockCWLoggingService)
	cloudWatchServiceRetriever = cwRetrieverMock

	wg.Add(1)

	go func() {
		defer wg.Done()
		file.Read(context, r, appconfig.SuccessExitCode)
	}()

	w.Write([]byte(pipeTestCase))
	w.Close()
	wg.Wait()
	outputFileExists, _ := fileutil.LocalFileExist(filePath)
	if outputFileExists {
		os.Remove(filePath)
	}

	return outputFileExists
}
