// Copyright 2016 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 custom contains a gatherer for collecting custom inventory items
package custom

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/mocks/context"
	"github.com/aws/amazon-ssm-agent/agent/plugins/inventory/model"
	"github.com/stretchr/testify/assert"
)

type MockFileInfo struct {
	name    string
	size    int64
	mode    os.FileMode
	modTime time.Time
	isDir   bool
}

var mockMachineIDProvider = func() (string, error) { return "i-12345678", nil }

func (m MockFileInfo) Name() string {
	return m.name
}

func (m MockFileInfo) Size() int64 {
	return m.size
}

func (m MockFileInfo) Mode() os.FileMode {
	return m.mode
}

func (m MockFileInfo) ModTime() time.Time {
	return m.modTime
}

func (m MockFileInfo) IsDir() bool {
	return m.isDir
}

func (m MockFileInfo) Sys() interface{} {
	return nil
}

func MockCustomInventoryItem() model.CustomInventoryItem {

	var attributes = map[string]interface{}{
		"Name": "filename",
		"Size": "100",
		"Time": "2016-10-10T01:01:01Z",
	}

	return model.CustomInventoryItem{
		TypeName:      "Custom:MyFile",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemNoTypeName() model.CustomInventoryItem {

	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemLongTypeName() model.CustomInventoryItem {

	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName:      strings.Repeat("a", TypeNameLengthLimit+1),
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemTypeNameInvalidPrefix() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName:      "Test:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemMissedSchemaVersion() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName: "Custom:123",
		Content:  attributes,
	}
}

func MockItemInvalidSchemaVersion() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "123",
		Content:       attributes,
	}
}

func MockItemNoContent() model.CustomInventoryItem {
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.1",
	}
}

func MockItemContentIsArray() model.CustomInventoryItem {
	var attributes1 = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	var attributes2 = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8080",
		"Version":    "1.2.3.5",
	}
	var entries = []map[string]interface{}{
		attributes1, attributes2,
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       entries,
	}
}

func MockItemContentAttributeCountExceedLimit() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"WebServer":  "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	for i := 0; i < AttributeCountLimit; i++ {
		attributeName := fmt.Sprintf("attr%d", i)
		attributes[attributeName] = "1"
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemEmptyAttributeName() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"":           "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemLongAttributeName() model.CustomInventoryItem {
	longAttrName := strings.Repeat("a", AttributeNameLengthLimit+1)
	var attributes = map[string]interface{}{
		longAttrName: "corp.amazing.com",
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemIntTypeAttributeValue() model.CustomInventoryItem {
	var attributes = map[string]interface{}{
		"PortNumber": "8081",
		"Version":    "1.2.3.4",
		"MyName":     1234,
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockItemLongAttributeValue() model.CustomInventoryItem {
	longAttrValue := strings.Repeat("a", AttributeValueLengthLimit+1)
	var attributes = map[string]interface{}{
		"PortNumber": "",
		"Version":    "1.2.3.4",
		"MyName":     longAttrValue,
	}
	return model.CustomInventoryItem{
		TypeName:      "Custom:123",
		SchemaVersion: "1.0",
		Content:       attributes,
	}
}

func MockReadFile(filename string) ([]byte, error) {
	return json.Marshal(MockCustomInventoryItem())
}

func MockReadFileAccessDenied(filename string) ([]byte, error) {
	return nil, errors.New("Access Denied")
}

func MockReadFileInvalidJSON(filename string) ([]byte, error) {
	return []byte("{Invalid JSON}"), nil
}

func MockReadFileNoTypeName(filename string) ([]byte, error) {
	return json.Marshal(MockItemNoTypeName())
}

func MockReadFileLongTypeName(filename string) ([]byte, error) {
	return json.Marshal(MockItemLongTypeName())
}

func MockReadFileTypeNameInvalidPrefix(filename string) ([]byte, error) {
	return json.Marshal(MockItemTypeNameInvalidPrefix())
}

func MockReadFileNoSchemaVersion(filename string) ([]byte, error) {
	return json.Marshal(MockItemMissedSchemaVersion())
}

func MockReadFileInvalidSchemaVersion(filename string) ([]byte, error) {
	return json.Marshal(MockItemInvalidSchemaVersion())
}

func MockReadFileNoContentProperty(filename string) ([]byte, error) {
	return json.Marshal(MockItemNoContent())
}

func MockReadFileContentIsArray(filename string) ([]byte, error) {
	return json.Marshal(MockItemContentIsArray())
}

func MockReadFileContentAttributeCountExceedLimit(filename string) ([]byte, error) {
	return json.Marshal(MockItemContentAttributeCountExceedLimit())
}

func MockReadFileContentEmptyAttributeName(filename string) ([]byte, error) {
	return json.Marshal(MockItemEmptyAttributeName())
}

func MockReadFileContentLongAttributeName(filename string) ([]byte, error) {
	return json.Marshal(MockItemLongAttributeName())
}

func MockReadFileContentNonStringTypeAttributeValue(filename string) ([]byte, error) {
	return json.Marshal(MockItemIntTypeAttributeValue())
}

func MockReadFileContentLongAttributeValue(filename string) ([]byte, error) {
	return json.Marshal(MockItemLongAttributeValue())
}

func MockReadDir(dirname string) (files []os.FileInfo, err error) {
	mfi := MockFileInfo{
		name:    "abc.json",
		size:    1024,
		mode:    0,
		modTime: time.Now(),
		isDir:   true,
	}
	files = append(files, mfi)
	return
}

func MockReadDirNotExist(dirname string) (files []os.FileInfo, err error) {
	err = errors.New("Folder " + dirname + " does not exist.")
	return
}

func MockReadDirCustomInventoryFileCountExceed(dirname string) (files []os.FileInfo, err error) {
	for i := 0; i < CustomInventoryCountLimit+1; i++ {
		filename := fmt.Sprintf("abc%d.json", i)
		files = append(files, MockFileInfo{
			name:    filename,
			size:    1024,
			mode:    0,
			modTime: time.Now(),
			isDir:   true,
		})
	}
	return
}

func MockReadDirDuplicateType(dirname string) (files []os.FileInfo, err error) {
	for i := 0; i < 2; i++ {
		filename := fmt.Sprintf("abc%d.json", i)
		files = append(files, MockFileInfo{
			name:    filename,
			size:    1024,
			mode:    0,
			modTime: time.Now(),
			isDir:   true,
		})
	}
	return
}

func TestGatherer(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFile
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "Unexpected error thrown")
	assert.Equal(t, 1, len(items), "Custom Gather should return 1 inventory type data.")
}

func TestReadFileAccessDenied(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileAccessDenied
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestCustomInventoryDirNotExist(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFile
	readDirFunc = MockReadDirNotExist

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as we should ignore if folder does not exist")
	assert.Nil(t, items, "Items should be nil")
}

func TestCustomInventoryCountExceed(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFile
	readDirFunc = MockReadDirCustomInventoryFileCountExceed

	items, err := g.Run(c, model.Config{})
	assert.NotNil(t, err, "err shoud be nil as we should ignore if folder does not exist")
	assert.Contains(t, err.Error(), "Total custom inventory file count")
	assert.Nil(t, items, "Items should be nil")
}

func TestDuplicateTypeName(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFile
	readDirFunc = MockReadDirDuplicateType

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil")
	assert.NotNil(t, items, "Items should not be nil")
}

func TestInvalidJson(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileInvalidJSON
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestMissingTypeName(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileNoTypeName
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestLongTypeName(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileLongTypeName
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestTypeNameInvalidPrefix(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileTypeNameInvalidPrefix
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestMissingSchemaVersion(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileNoSchemaVersion
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestInvalidSchemaVersion(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileInvalidSchemaVersion
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestMissingContent(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileNoContentProperty
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestContentIsArray(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentIsArray
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Equal(t, 1, len(items), "Custom Gather should return 1 inventory type data.")
	entries, ok := items[0].Content.([]map[string]interface{})
	assert.Equal(t, ok, true)
	assert.Equal(t, 2, len(entries), "Custom Gather should return 2 entries.")
}

func TestContentAttributeCountExceedLimit(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentAttributeCountExceedLimit
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestContentEmptyAttributeName(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentEmptyAttributeName
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestContentLongAttributeName(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentLongAttributeName
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestContentNonStringTypeAttributeValue(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentNonStringTypeAttributeValue
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}

func TestContentLongAttributeValue(t *testing.T) {
	c := context.NewMockDefault()
	g := Gatherer(c)
	readFileFunc = MockReadFileContentLongAttributeValue
	readDirFunc = MockReadDir

	items, err := g.Run(c, model.Config{})
	assert.Nil(t, err, "err shoud be nil as gather continues to load other custom inventory files")
	assert.Nil(t, items, "Items should be nil")
}
