// Copyright 2015 Google Inc. 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.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 samples

import (
	"bytes"
	"context"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path"
	"sync"

	"github.com/jacobsa/ogletest"
)

var fToolPath = flag.String(
	"mount_sample",
	"",
	"Path to the mount_sample tool. If unset, we will compile it.")

var fDebug = flag.Bool("debug", false, "If true, print fuse debug info.")

// A struct that implements common behavior needed by tests in the samples/
// directory where the file system is mounted by a subprocess. Use it as an
// embedded field in your test fixture, calling its SetUp method from your
// SetUp method after setting the MountType and MountFlags fields.
type SubprocessTest struct {
	// The type of the file system to mount. Must be recognized by mount_sample.
	MountType string

	// Additional flags to be passed to the mount_sample tool.
	MountFlags []string

	// A list of files to pass to mount_sample. The given string flag will be
	// used to pass the file descriptor number.
	MountFiles map[string]*os.File

	// A context object that can be used for long-running operations.
	Ctx context.Context

	// The directory at which the file system is mounted.
	Dir string

	// Anothing non-nil in this slice will be closed by TearDown. The test will
	// fail if closing fails.
	ToClose []io.Closer

	mountSampleErr <-chan error
}

// Mount the file system and initialize the other exported fields of the
// struct. Panics on error.
func (t *SubprocessTest) SetUp(ti *ogletest.TestInfo) {
	err := t.initialize(ti.Ctx)
	if err != nil {
		panic(err)
	}
}

// Private state for getToolPath.
var getToolContents_Contents []byte
var getToolContents_Err error
var getToolContents_Once sync.Once

// Implementation detail of getToolPath.
func getToolContentsImpl() ([]byte, error) {
	// Fast path: has the user set the flag?
	if *fToolPath != "" {
		contents, err := ioutil.ReadFile(*fToolPath)
		if err != nil {
			return nil, fmt.Errorf("Reading mount_sample contents: %v", err)
		}

		return contents, err
	}

	// Create a temporary directory into which we will compile the tool.
	tempDir, err := ioutil.TempDir("", "sample_test")
	if err != nil {
		return nil, fmt.Errorf("TempDir: %v", err)
	}

	toolPath := path.Join(tempDir, "mount_sample")

	// Ensure that we kill the temporary directory when we're finished here.
	defer os.RemoveAll(tempDir)

	// Run "go build".
	cmd := exec.Command(
		"go",
		"build",
		"-o",
		toolPath,
		"github.com/jacobsa/fuse/samples/mount_sample")

	output, err := cmd.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf(
			"mount_sample exited with %v, output:\n%s",
			err,
			string(output))
	}

	// Slurp the tool contents.
	contents, err := ioutil.ReadFile(toolPath)
	if err != nil {
		return nil, fmt.Errorf("ReadFile: %v", err)
	}

	return contents, nil
}

// Build the mount_sample tool if it has not yet been built for this process.
// Return its contents.
func getToolContents() ([]byte, error) {
	// Get hold of the binary contents, if we haven't yet.
	getToolContents_Once.Do(func() {
		getToolContents_Contents, getToolContents_Err = getToolContentsImpl()
	})

	return getToolContents_Contents, getToolContents_Err
}

func waitForMountSample(
	cmd *exec.Cmd,
	errChan chan<- error,
	stderr *bytes.Buffer) {
	// However we exit, write the error to the channel.
	var err error
	defer func() {
		errChan <- err
	}()

	// Wait for the command.
	err = cmd.Wait()
	if err == nil {
		return
	}

	// Make exit errors nicer.
	if exitErr, ok := err.(*exec.ExitError); ok {
		err = fmt.Errorf(
			"mount_sample exited with %v. Stderr:\n%s",
			exitErr,
			stderr.String())

		return
	}

	err = fmt.Errorf("Waiting for mount_sample: %v", err)
}

func waitForReady(readyReader *os.File, c chan<- struct{}) {
	_, err := readyReader.Read(make([]byte, 1))
	if err != nil {
		log.Printf("Readying from ready pipe: %v", err)
		return
	}

	c <- struct{}{}
}

// Like SetUp, but doens't panic.
func (t *SubprocessTest) initialize(ctx context.Context) error {
	// Initialize the context.
	t.Ctx = ctx

	// Set up a temporary directory.
	var err error
	t.Dir, err = ioutil.TempDir("", "sample_test")
	if err != nil {
		return fmt.Errorf("TempDir: %v", err)
	}

	// Build/read the mount_sample tool.
	toolContents, err := getToolContents()
	if err != nil {
		return fmt.Errorf("getTooltoolContents: %v", err)
	}

	// Create a temporary file to hold the contents of the tool.
	toolFile, err := ioutil.TempFile("", "sample_test")
	if err != nil {
		return fmt.Errorf("TempFile: %v", err)
	}

	defer toolFile.Close()

	// Ensure that it is deleted when we leave.
	toolPath := toolFile.Name()
	defer os.Remove(toolPath)

	// Write out the tool contents and make them executable.
	if _, err = toolFile.Write(toolContents); err != nil {
		return fmt.Errorf("toolFile.Write: %v", err)
	}

	if err = toolFile.Chmod(0500); err != nil {
		return fmt.Errorf("toolFile.Chmod: %v", err)
	}

	// Close the tool file to prevent "text file busy" errors below.
	err = toolFile.Close()
	toolFile = nil
	if err != nil {
		return fmt.Errorf("toolFile.Close: %v", err)
	}

	// Set up basic args for the subprocess.
	args := []string{
		"--type",
		t.MountType,
		"--mount_point",
		t.Dir,
	}

	args = append(args, t.MountFlags...)

	// Set up a pipe for the "ready" status.
	readyReader, readyWriter, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("Pipe: %v", err)
	}

	defer readyReader.Close()
	defer readyWriter.Close()

	t.MountFiles["ready_file"] = readyWriter

	// Set up inherited files and appropriate flags.
	var extraFiles []*os.File
	for flag, file := range t.MountFiles {
		// Cf. os/exec.Cmd.ExtraFiles
		fd := 3 + len(extraFiles)

		extraFiles = append(extraFiles, file)
		args = append(args, "--"+flag)
		args = append(args, fmt.Sprintf("%d", fd))
	}

	// Set up a command.
	var stderr bytes.Buffer
	mountCmd := exec.Command(toolPath, args...)
	mountCmd.Stderr = &stderr
	mountCmd.ExtraFiles = extraFiles

	// Handle debug mode.
	if *fDebug {
		mountCmd.Stderr = os.Stderr
		mountCmd.Args = append(mountCmd.Args, "--debug")
	}

	// Start the command.
	if err := mountCmd.Start(); err != nil {
		return fmt.Errorf("mountCmd.Start: %v", err)
	}

	// Launch a goroutine that waits for it and returns its status.
	mountSampleErr := make(chan error, 1)
	go waitForMountSample(mountCmd, mountSampleErr, &stderr)

	// Wait for the tool to say the file system is ready. In parallel, watch for
	// the tool to fail.
	readyChan := make(chan struct{}, 1)
	go waitForReady(readyReader, readyChan)

	select {
	case <-readyChan:
	case err := <-mountSampleErr:
		return err
	}

	// TearDown is no responsible for joining.
	t.mountSampleErr = mountSampleErr

	return nil
}

// Unmount the file system and clean up. Panics on error.
func (t *SubprocessTest) TearDown() {
	err := t.destroy()
	if err != nil {
		panic(err)
	}
}

// Like TearDown, but doesn't panic.
func (t *SubprocessTest) destroy() (err error) {
	// Make sure we clean up after ourselves after everything else below.

	// Close what is necessary.
	for _, c := range t.ToClose {
		if c == nil {
			continue
		}

		ogletest.ExpectEq(nil, c.Close())
	}

	// If we didn't try to mount the file system, there's nothing further to do.
	if t.mountSampleErr == nil {
		return nil
	}

	// In the background, initiate an unmount.
	unmountErrChan := make(chan error)
	go func() {
		unmountErrChan <- unmount(t.Dir)
	}()

	// Make sure we wait for the unmount, even if we've already returned early in
	// error. Return its error if we haven't seen any other error.
	defer func() {
		// Wait.
		unmountErr := <-unmountErrChan
		if unmountErr != nil {
			if err != nil {
				log.Println("unmount:", unmountErr)
				return
			}

			err = fmt.Errorf("unmount: %v", unmountErr)
			return
		}

		// Clean up.
		ogletest.ExpectEq(nil, os.Remove(t.Dir))
	}()

	// Wait for the subprocess.
	if err := <-t.mountSampleErr; err != nil {
		return err
	}

	return nil
}
