package main

import (
	"bytes"
	"errors"
	"io/ioutil"
	"os"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestExec_success(t *testing.T) {
	v := new(mockFile)
	ec, err := Exec("date", "/", v, v)
	require.Nil(t, err, "err: %v -- out: %s", err, v.b.Bytes())
	require.EqualValues(t, 0, ec)
}

func TestExec_success_redirectsStdStreams_closesFds(t *testing.T) {
	o, e := new(mockFile), new(mockFile)
	require.False(t, o.closed, "stdout open")
	require.False(t, e.closed, "stderr open")

	_, err := Exec("/bin/echo 'I am stdout!'>&1; /bin/echo 'I am stderr!'>&2", "/", o, e)
	require.Nil(t, err, "err: %v -- stderr: %s", err, e.b.Bytes())
	require.Equal(t, "I am stdout!\n", string(o.b.Bytes()))
	require.Equal(t, "I am stderr!\n", string(e.b.Bytes()))
	require.True(t, o.closed, "stdout closed")
	require.True(t, e.closed, "stderr closed")
}

func TestExec_failure_exitError(t *testing.T) {
	ec, err := Exec("exit 12", "/", new(mockFile), new(mockFile))
	require.NotNil(t, err)
	require.EqualError(t, err, "command terminated with exit status=12") // error is customized
	require.EqualValues(t, 12, ec)
}

func TestExec_failure_genericError(t *testing.T) {
	_, err := Exec("date", "/non-existing-path", new(mockFile), new(mockFile))
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "failed to execute command:") // error is wrapped
}

func TestExec_failure_fdClosed(t *testing.T) {
	out := new(mockFile)
	require.Nil(t, out.Close())

	_, err := Exec("date", "/", out, out)
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "file closed") // error is wrapped
}

func TestExec_failure_redirectsStdStreams_closesFds(t *testing.T) {
	o, e := new(mockFile), new(mockFile)
	require.False(t, o.closed, "stdout open")
	require.False(t, e.closed, "stderr open")

	_, err := Exec(`/bin/echo 'I am stdout!'>&1; /bin/echo 'I am stderr!'>&2; exit 12`, "/", o, e)
	require.NotNil(t, err)
	require.Equal(t, "I am stdout!\n", string(o.b.Bytes()))
	require.Equal(t, "I am stderr!\n", string(e.b.Bytes()))
	require.True(t, o.closed, "stdout closed")
	require.True(t, e.closed, "stderr closed")
}

func TestExecCmdInDir(t *testing.T) {
	dir, err := ioutil.TempDir("", "")
	require.Nil(t, err)
	defer os.RemoveAll(dir)

	err = ExecCmdInDir("/bin/echo 'Hello world'", dir)
	require.Nil(t, err)
	require.True(t, fileExists(t, filepath.Join(dir, "stdout")), "stdout file should be created")
	require.True(t, fileExists(t, filepath.Join(dir, "stderr")), "stderr file should be created")

	b, err := ioutil.ReadFile(filepath.Join(dir, "stdout"))
	require.Nil(t, err)
	require.Equal(t, "Hello world\n", string(b))

	b, err = ioutil.ReadFile(filepath.Join(dir, "stderr"))
	require.Nil(t, err)
	require.EqualValues(t, 0, len(b), "stderr file must be empty")
}

func TestExecCmdInDir_cantOpenError(t *testing.T) {
	err := ExecCmdInDir("/bin/echo 'Hello world'", "/non-existing-dir")
	require.Contains(t, err.Error(), "failed to open stdout file")
}

func TestExecCmdInDir_truncates(t *testing.T) {
	dir, err := ioutil.TempDir("", "")
	require.Nil(t, err)
	defer os.RemoveAll(dir)

	require.Nil(t, ExecCmdInDir("/bin/echo '1:out'; /bin/echo '1:err'>&2", dir))
	require.Nil(t, ExecCmdInDir("/bin/echo '2:out'; /bin/echo '2:err'>&2", dir))

	b, err := ioutil.ReadFile(filepath.Join(dir, "stdout"))
	require.Nil(t, err)
	require.Equal(t, "2:out\n", string(b), "stdout did not truncate")

	b, err = ioutil.ReadFile(filepath.Join(dir, "stderr"))
	require.Nil(t, err)
	require.Equal(t, "2:err\n", string(b), "stderr did not truncate")
}

func Test_logPaths(t *testing.T) {
	stdout, stderr := logPaths("/tmp")
	require.Equal(t, "/tmp/stdout", stdout)
	require.Equal(t, "/tmp/stderr", stderr)
}

// Test utilities

type mockFile struct {
	b      bytes.Buffer
	closed bool
}

func (m *mockFile) Write(p []byte) (n int, err error) {
	if m.closed {
		return 0, errors.New("file closed")
	}
	return m.b.Write(p)
}

func (m *mockFile) Close() error {
	m.closed = true
	return nil
}

func fileExists(t *testing.T, path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	t.Fatalf("failed to check if %s exists: %v", path, err)
	return false
}
