package proxyprocess

import (
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/consul/agent/local"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/sdk/testutil/retry"
	"github.com/stretchr/testify/require"
)

func TestManagerClose_noRun(t *testing.T) {
	t.Parallel()

	// Really we're testing that it doesn't deadlock here.
	m, closer := testManager(t)
	defer closer()
	require.NoError(t, m.Close())

	// Close again for sanity
	require.NoError(t, m.Close())
}

// Test that Run performs an initial sync (if local.State is already set)
// rather than waiting for a notification from the local state.
func TestManagerRun_initialSync(t *testing.T) {
	t.Parallel()

	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Add the proxy before we start the manager to verify initial sync
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	// Start the manager
	go m.Run()

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})
}

func TestManagerRun_syncNew(t *testing.T) {
	t.Parallel()

	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Start the manager
	go m.Run()

	// Sleep a bit, this is just an attempt for Run to already be running.
	// Its not a big deal if this sleep doesn't happen (slow CI).
	time.Sleep(100 * time.Millisecond)

	// Add the first proxy
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Add another proxy
	path = path + "2"

	cmd, destroy = helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "db", cmd)
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})
}

func TestManagerRun_syncDelete(t *testing.T) {
	t.Parallel()

	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Start the manager
	go m.Run()

	// Add the first proxy
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("restart", path)
	defer destroy()

	id := testStateProxy(t, state, "web", cmd)

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Remove the proxy
	_, err := state.RemoveProxy(id)
	require.NoError(t, err)

	// File should disappear as process is killed
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			r.Fatalf("path exists")
		}
	})
}

func TestManagerRun_syncUpdate(t *testing.T) {
	t.Parallel()

	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Start the manager
	go m.Run()

	// Add the first proxy
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Update the proxy with a new path
	oldPath := path
	path = path + "2"

	cmd, destroy = helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Old path should be gone
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(oldPath)
		if err == nil {
			r.Fatalf("old path exists")
		}
	})
}

func TestManagerRun_daemonLogs(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Configure a log dir so that we can read the logs
	logDir := filepath.Join(m.DataDir, "logs")

	// Create the service and calculate the log paths
	path := filepath.Join(m.DataDir, "notify")

	cmd, destroy := helperProcess("output", path)
	defer destroy()

	id := testStateProxy(t, state, "web", cmd)
	stdoutPath := logPath(logDir, id, "stdout")
	stderrPath := logPath(logDir, id, "stderr")

	// Start the manager
	go m.Run()

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		if _, err := os.Stat(path); err != nil {
			r.Fatalf("error waiting for stdout path: %s", err)
		}
	})

	expectedOut := "hello stdout\n"
	actual, err := ioutil.ReadFile(stdoutPath)
	require.NoError(err)
	require.Equal([]byte(expectedOut), actual)

	expectedErr := "hello stderr\n"
	actual, err = ioutil.ReadFile(stderrPath)
	require.NoError(err)
	require.Equal([]byte(expectedErr), actual)
}

func TestManagerRun_daemonPid(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Configure a log dir so that we can read the logs
	pidDir := filepath.Join(m.DataDir, "pids")

	// Create the service and calculate the log paths
	path := filepath.Join(m.DataDir, "notify")

	cmd, destroy := helperProcess("output", path)
	defer destroy()

	id := testStateProxy(t, state, "web", cmd)
	pidPath := pidPath(pidDir, id)

	// Start the manager
	go m.Run()

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		if _, err := os.Stat(path); err != nil {
			r.Fatalf("error waiting for stdout path: %s", err)
		}
	})

	// Verify the pid file is not empty
	pidRaw, err := ioutil.ReadFile(pidPath)
	require.NoError(err)
	require.NotEmpty(pidRaw)
}

// Test to check if the parent and the child processes
// have the same environmental variables

func TestManagerPassesEnvironment(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Add Proxy for the test
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "env-variables")

	cmd, destroy := helperProcess("environ", path)
	defer destroy()

	testStateProxy(t, state, "environTest", cmd)

	//Run the manager
	go m.Run()

	//Get the environmental variables from the OS
	var fileContent []byte
	var err error
	var data []byte
	envData := os.Environ()
	sort.Strings(envData)
	for _, envVariable := range envData {
		if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
			continue
		}
		data = append(data, envVariable...)
		data = append(data, "\n"...)
	}

	// Check if the file written to from the spawned process
	// has the necessary environmental variable data
	retry.Run(t, func(r *retry.R) {
		if fileContent, err = ioutil.ReadFile(path); err != nil {
			r.Fatalf("No file ya dummy")
		}
	})

	require.Equal(data, fileContent)
}

// Test to check if the parent and the child processes
// have the same environmental variables
func TestManagerPassesProxyEnv(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	penv := make([]string, 0, 2)
	penv = append(penv, "HTTP_ADDR=127.0.0.1:8500")
	penv = append(penv, "HTTP_SSL=false")
	m.ProxyEnv = penv

	// Add Proxy for the test
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "env-variables")

	cmd, destroy := helperProcess("environ", path)
	defer destroy()

	testStateProxy(t, state, "environTest", cmd)

	//Run the manager
	go m.Run()

	//Get the environmental variables from the OS
	var fileContent []byte
	var err error
	var data []byte
	envData := os.Environ()
	envData = append(envData, "HTTP_ADDR=127.0.0.1:8500")
	envData = append(envData, "HTTP_SSL=false")
	sort.Strings(envData)
	for _, envVariable := range envData {
		if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
			continue
		}
		data = append(data, envVariable...)
		data = append(data, "\n"...)
	}

	// Check if the file written to from the spawned process
	// has the necessary environmental variable data
	retry.Run(t, func(r *retry.R) {
		if fileContent, err = ioutil.ReadFile(path); err != nil {
			r.Fatalf("No file ya dummy")
		}
	})

	require.Equal(data, fileContent)
}

// Test the Snapshot/Restore works.
func TestManagerRun_snapshotRestore(t *testing.T) {
	t.Parallel()

	require := require.New(t)
	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Add the proxy
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("start-stop", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	// Set a low snapshot period so we get a snapshot
	m.SnapshotPeriod = 10 * time.Millisecond

	// Start the manager
	go m.Run()

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Wait for the snapshot
	snapPath := m.SnapshotPath()
	retry.Run(t, func(r *retry.R) {
		raw, err := ioutil.ReadFile(snapPath)
		if err != nil {
			r.Fatalf("error waiting for path: %s", err)
		}
		if len(raw) < 30 {
			r.Fatalf("snapshot too small")
		}
	})

	// Stop the sync
	require.NoError(m.Close())

	// File should still exist
	_, err := os.Stat(path)
	require.NoError(err)

	// Restore a manager from a snapshot
	m2, closer := testManager(t)
	m2.State = state
	defer closer()
	defer m2.Kill()
	require.NoError(m2.Restore(snapPath))

	// Start
	go m2.Run()

	// Add a second proxy so that we can determine when we're up
	// and running.
	path2 := filepath.Join(td, "file2")

	cmd, destroy = helperProcess("start-stop", path2)
	defer destroy()

	testStateProxy(t, state, "db", cmd)

	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path2)
		if err == nil {
			return
		}
		r.Fatalf("error waiting for path: %s", err)
	})

	// Kill m2, which should kill our main process
	require.NoError(m2.Kill())

	// File should no longer exist
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err != nil {
			return
		}
		r.Fatalf("file still exists: %s", path)
	})
}

// Manager should not run any proxies if we're running as root. Tests
// stub the value.
func TestManagerRun_rootDisallow(t *testing.T) {
	// Pretend we are root
	defer testSetRootValue(true)()

	state := local.TestState(t)
	m, closer := testManager(t)
	defer closer()
	m.State = state
	defer m.Kill()

	// Add the proxy before we start the manager to verify initial sync
	td, closer := testTempDir(t)
	defer closer()
	path := filepath.Join(td, "file")

	cmd, destroy := helperProcess("restart", path)
	defer destroy()

	testStateProxy(t, state, "web", cmd)

	// Start the manager
	go m.Run()

	// Sleep a bit just to verify
	time.Sleep(100 * time.Millisecond)

	// We should see the path appear shortly
	retry.Run(t, func(r *retry.R) {
		_, err := os.Stat(path)
		if err != nil {
			return
		}

		r.Fatalf("path exists")
	})
}

func testManager(t *testing.T) (*Manager, func()) {
	m := NewManager()

	// Setup a default state
	m.State = local.TestState(t)

	// Set these periods low to speed up tests
	m.CoalescePeriod = 1 * time.Millisecond
	m.QuiescentPeriod = 1 * time.Millisecond

	// Setup a temporary directory for logs
	td, closer := testTempDir(t)
	m.DataDir = td

	return m, func() { closer() }
}

// testStateProxy registers a proxy with the given local state and the command
// (expected to be from the helperProcess function call). It returns the
// ID for deregistration.
func testStateProxy(t *testing.T, state *local.State, service string, cmd *exec.Cmd) string {
	// *exec.Cmd must manually set args[0] to the binary. We automatically
	// set this when constructing the command for the proxy, so we must strip
	// the zero index. We do this unconditionally (anytime len is > 0) because
	// index zero should ALWAYS be the binary.
	if len(cmd.Args) > 0 {
		cmd.Args = cmd.Args[1:]
	}

	command := []string{cmd.Path}
	command = append(command, cmd.Args...)

	require.NoError(t, state.AddService(&structs.NodeService{
		Service: service,
	}, "token"))

	p, err := state.AddProxy(&structs.ConnectManagedProxy{
		ExecMode:        structs.ProxyExecModeDaemon,
		Command:         command,
		TargetServiceID: service,
	}, "token", "")
	require.NoError(t, err)

	return p.Proxy.ProxyService.ID
}
