package agent

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/user"
	"path"
	"runtime"
	"strconv"
	"strings"
	"time"

	"code.justin.tv/systems/sandstorm/manager"
	"github.com/aws/aws-sdk-go/service/sqs"
)

const (
	testPIDPath     = "sandstorm-agent.pid"
	testRoleArn     = "arn:aws:iam::734326455073:role/sandstorm-agent-testing-rw"
	testMaxWaitTime = 1
	testDummyString = "test_string"
	testDummyInt    = 69
)

func mockAlwaysTrueFileComparer(new, old *Template) bool {
	return true
}

func newTestQueueMessage(secretName string, updatedAt time.Time) string {
	var msg = `{
	"Type": "Notification",
	"MessageId": "4e862591-7390-5d7e-abdb-6a0dec285b2b",
	"TopicArn": "arn:aws:sns:us-west-2:734326455073:sandstorm-production",
	"Message": "{\"updated_at\":{\"N\":\"%d\"},\"name\":{\"S\":\"%s\"}}",
	"Timestamp": "%s",
	"Signature": "",
	"SigningCertURL": "",
	"UnsubscribeURL": ""
}`
	tsLayout := "2006-01-02T15:04:05.999Z"
	return fmt.Sprintf(msg, updatedAt.UTC().Unix(), secretName, updatedAt.UTC().Format(tsLayout))
}

func newTemplateFileTest() (tmpfile *os.File, old *Template, err error) {
	tmpfile, err = ioutil.TempFile("", "tmp")
	if err != nil {
		return
	}
	currentUser, err := user.Current()
	if err != nil {
		return
	}

	currentUserID, err := strconv.Atoi(currentUser.Uid)
	if err != nil {
		return
	}

	currentGroupID, err := strconv.Atoi(currentUser.Gid)
	if err != nil {
		return
	}

	const fileMode os.FileMode = 0640
	err = tmpfile.Chmod(fileMode)
	if err != nil {
		return
	}
	old = &Template{
		Destination: tmpfile.Name(),
		UserID:      currentUserID,
		GroupID:     currentGroupID,
		FileMode:    fileMode,
	}
	return
}

var sampleQueueMessage = "{  \"Type\" : \"Notification\",  \"MessageId\" : \"4e862591-7390-5d7e-abdb-6a0dec285b2b\",  \"TopicArn\" : \"arn:aws:sns:us-west-2:734326455073:sandstorm-production\",  \"Message\" : \"{\\\"updated_at\\\":{\\\"N\\\":\\\"1461886327\\\"},\\\"name\\\":{\\\"S\\\":\\\"systems/sandstorm-agent/development/poll_test_\\\"}}\",  \"Timestamp\" : \"2016-04-28T23:32:08.522Z\",  \"SignatureVersion\" : \"1\",  \"Signature\" : \"FkJsFzYRjYAchgr/Ox4c2wj8QLd9wGSyEDj6I1kU1WCxeR4cRmjbSlPbOdFRkt/QiZyr6km+vKVW+EatBUVjWmfx20c90zUR3unDhqpZ65kMn6W82ObaJ4d4MgJR2306WDYaC341eSgzAQKYipggZrR7wKP5+LbeBliRTuUeBDxwf9siJLUuXN+Z6wex6UpqArtN/to+OWfJHGcbjDobdgxVpH1LGivytc4gKu2qZdBK9DmkjPpmF2dP61hl/wpdl7qFfHGe1uvCd8Y8b71bHfKGS8K7A4cDcefgPfCeIstUKedTS9+LVeqd6UrQYmLc/KctvqvZcEf1CyK9OND0XQ==\",  \"SigningCertURL\" : \"https://sns.us-west-2.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem\",  \"UnsubscribeURL\" : \"https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:734326455073:sandstorm-production:3d8dacf6-548c-4a02-80ce-d9261eb20981\"}"
var testReceiptHandle = "testReceiptHandle"

// init handles some small runtime changes to make local testing possible
func init() {

	// if on Apple OSX, we assume local development rather than manta and use the
	// current user's info for test file permissions
	if runtime.GOOS == "darwin" {
		user, err := user.Current()
		if err != nil {
			log.Fatal(err.Error())
		}

		uid, err := strconv.Atoi(user.Uid)
		if err != nil {
			log.Fatal(err.Error())
		}
		DefaultUserID = uid

		gid, err := strconv.Atoi(user.Gid)
		if err != nil {
			log.Fatal(err.Error())
		}
		DefaultGroupID = gid
	}
}

var testSecrets = []*manager.Secret{
	&manager.Secret{
		Name:      "systems/sandstorm-agent/development/test_secret",
		Plaintext: []byte("foooooo"),
	},
	&manager.Secret{
		Name:      "systems/sandstorm-agent/development/test_secret_2",
		Plaintext: []byte("baaaaaar"),
	},
}

// prepare creates a new test output folder a test to use and returns the path to it
// also does some additional cleaning/preparation.
func prepareTestPath(testName string) string {

	testFolderPath := path.Join(getBaseTestFolderPath(), "output", testName)

	if fileExists(testFolderPath) {
		err := os.RemoveAll(testFolderPath)
		if err != nil {
			log.Fatal(err.Error())
		}
	}

	err := os.MkdirAll(testFolderPath, 0777)
	if err != nil {
		log.Fatal(err.Error())
	}

	cleanAgentFiles(testFolderPath)

	return testFolderPath
}

type testBlocker []struct{}

// Block implements Block for testing
func (b testBlocker) Block() {
	b = append(b, struct{}{})
}

// Unblock implements Unblock for testing
func (b testBlocker) Unblock() {
	if len(b) == 0 {
		return
	}
	b = b[:len(b)-1]
}

// Returns a pre-built agent from the test folder configuration and templates
func getTestAgent(testFolderPath string, cleanFolder bool, makeTestSecrets bool) *Agent {

	if cleanFolder {
		cleanAgentFiles(testFolderPath)
	}

	agent, err := Build(getTestConfigPath(), true, false)
	if err != nil {
		log.Fatal(err.Error())
	}

	// Update template destinations so the output to the proper folder
	for _, template := range agent.State.Templates {
		templateName := path.Base(template.Source)
		template.Destination = path.Join(testFolderPath, templateName)
		template.Command = strings.Replace(template.Command, path.Join(getTestConfigPath(), "output"), testFolderPath, 1)
	}

	// Set these real low so tests are way faster
	agent.Splay = 1
	agent.Dwell = 1
	agent.Logging = false
	agent.blocker = testBlocker{}

	if makeTestSecrets {
		createTestSecrets(agent.manager.(*manager.Manager))
	}

	return agent
}

func cleanAgentFiles(testFolderPath string) {

	files := []string{StateFileName}

	for _, name := range files {
		filePath := path.Join(getTestConfigPath(), name)
		if fileExists(filePath) {
			err := os.Remove(filePath)
			if err != nil {
				log.Fatal(err.Error())
			}
		}
	}
}

func createTestSecrets(mgr manager.API, secrets ...*manager.Secret) {
	var secretsToMake []*manager.Secret
	if len(secrets) > 0 {
		secretsToMake = secrets
	} else {
		secretsToMake = testSecrets
	}
	for _, secret := range secretsToMake {

		var plaintext []byte

		// Wipe the secret if it already exists
		oldSecret, err := mgr.Get(secret.Name)
		if oldSecret != nil {

			// store plaintext, so that when we re-create we can add it back
			plaintext = oldSecret.Plaintext

			err := mgr.Delete(oldSecret.Name)
			if err != nil {
				log.Fatal(err.Error())
			}
		}

		// Recreate
		err = mgr.Post(secret)
		if err != nil {
			log.Fatal(err.Error())
		}

		secret.Plaintext = plaintext
	}
}

func getTestTemplate(testFolderPath string) *Template {

	source := "/build/go/src/code.justin.tv/systems/sandstorm/agent/test/config/templates.d/sample_template2"
	destination := path.Join(testFolderPath, "sample_template2")
	command := fmt.Sprintf("touch %s", path.Join(testFolderPath, "sample_template_2_touch"))

	template := NewTemplate(source, destination, command)
	err := template.Initialize()
	if err != nil {
		log.Fatal(err.Error())
	}

	template.Secrets[testSecrets[0].Name] = testSecrets[0]

	return template
}

func getBaseTestFolderPath() string {
	return fmt.Sprintf("%s/src/code.justin.tv/systems/sandstorm/agent/test", getRootPath())
}

func getTestConfigPath() string {
	return fmt.Sprintf("%s/src/code.justin.tv/systems/sandstorm/agent/test/config", getRootPath())
}

func getRootPath() string {
	goPath := os.Getenv("GOPATH")
	if len(goPath) == 0 {
		goPath = "/build/go"
	}
	return goPath
}

// createStateFileFromAgent uses an agent's templates to create a mock state file
// for testing purposes
func createStateFileFromAgent(agent *Agent, testFolderPath string) error {
	stateFilePath := path.Join(testFolderPath, StateFileName)
	stateFile := newStateFile(agent.State.Templates)
	return stateFile.ToFile(stateFilePath)
}

// MockSQS mocks SQS
type MockSQS struct {
	FailError error
	Message   []*sqs.Message
	Sent      bool
}

// ReceiveMessage implements SQS.ReceiveMessage
func (m *MockSQS) ReceiveMessage(input *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
	if !m.Sent && len(m.Message) > 0 {
		m.Sent = true
		return &sqs.ReceiveMessageOutput{
			Messages: m.Message,
		}, nil
	}
	return &sqs.ReceiveMessageOutput{
		Messages: []*sqs.Message{},
	}, nil
}

// CreateQueue implements SQS.CreateQueue
func (m *MockSQS) CreateQueue(input *sqs.CreateQueueInput) (*sqs.CreateQueueOutput, error) {
	return nil, nil
}

// SetQueueAttributes implements SQS.SetQueueAttributes
func (m *MockSQS) SetQueueAttributes(input *sqs.SetQueueAttributesInput) (*sqs.SetQueueAttributesOutput, error) {
	return nil, nil
}

// GetQueueAttributes implements SQS.GetQueueAttributes
func (m *MockSQS) GetQueueAttributes(input *sqs.GetQueueAttributesInput) (*sqs.GetQueueAttributesOutput, error) {
	return nil, nil
}

// DeleteQueue implements SQS.DeleteQueue
func (m *MockSQS) DeleteQueue(input *sqs.DeleteQueueInput) (*sqs.DeleteQueueOutput, error) {
	return nil, nil
}

// PurgeQueue implements SQS.PurgeQueue
func (m *MockSQS) PurgeQueue(input *sqs.PurgeQueueInput) (*sqs.PurgeQueueOutput, error) {
	return nil, nil
}

// DeleteMessage implements SQS.DeleteMessage
func (m *MockSQS) DeleteMessage(input *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) {
	return nil, nil
}
