// +build ignore
//
// Build multiple configurations of MCUboot for Zephyr, making sure
// that they run properly.
//
// Run as:
//
//    go run run-tests.go [flags]
//
// Add -help as a flag to get help.  See comment below for logIn on
// how to configure terminal output to a file so this program can see
// the output of the Zephyr device.

package main

import (
	"archive/zip"
	"bufio"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"
	"time"

	"github.com/mcu-tools/mcuboot/samples/zephyr/mcutests"
)

// logIn gives the pathname of the log output from the Zephyr device.
// In order to see the serial output, but still be useful for human
// debugging, the output of the terminal emulator should be teed to a
// file that this program will read from.  This can be done with
// something like:
//
//     picocom -b 115200 /dev/ttyACM0 | tee /tmp/zephyr.out
//
// Other terminal programs should also have logging options.
var logIn = flag.String("login", "/tmp/zephyr.out", "File name of terminal log from Zephyr device")

// Output from this test run is written to the given log file.
var logOut = flag.String("logout", "tests.log", "Log file to write to")

var preBuilt = flag.String("prebuilt", "", "Name of file with prebuilt tests")

func main() {
	err := run()
	if err != nil {
		log.Fatal(err)
	}
}

func run() error {
	flag.Parse()

	lines := make(chan string, 30)
	go readLog(lines)

	// Write output to a log file
	logFile, err := os.Create(*logOut)
	if err != nil {
		return err
	}
	defer logFile.Close()
	lg := bufio.NewWriter(logFile)
	defer lg.Flush()

	var extractor *Extractor

	if *preBuilt != "" {
		// If there are pre-built images, open them.
		extractor, err = NewExtractor(*preBuilt)
		if err != nil {
			return err
		}
		defer extractor.Close()
	}

	for _, group := range mcutests.Tests {
		fmt.Printf("Running %q\n", group.Name)
		fmt.Fprintf(lg, "-------------------------------------\n")
		fmt.Fprintf(lg, "---- Running %q\n", group.Name)

		for _, test := range group.Tests {
			if *preBuilt == "" {
				// No prebuilt, build the tests
				// ourselves.
				err = runCommands(test.Build, lg)
				if err != nil {
					return err
				}
			} else {
				// Extract the build artifacts from
				// the zip file.
				err = extractor.Extract(group.ShortName)
				if err != nil {
					return err
				}
			}

			err = runCommands(test.Commands, lg)
			if err != nil {
				return err
			}

			err = expect(lg, lines, test.Expect)
			if err != nil {
				return err
			}

			fmt.Fprintf(lg, "---- Passed\n")
		}
		fmt.Printf("    Passed!\n")
	}

	return nil
}

// Run a set of commands
func runCommands(cmds [][]string, lg io.Writer) error {
	for _, cmd := range cmds {
		fmt.Printf("    %s\n", cmd)
		fmt.Fprintf(lg, "---- Run: %s\n", cmd)
		err := runCommand(cmd, lg)
		if err != nil {
			return err
		}
	}

	return nil
}

// Run a single command.
func runCommand(cmd []string, lg io.Writer) error {
	c := exec.Command(cmd[0], cmd[1:]...)
	c.Stdout = lg
	c.Stderr = lg
	return c.Run()
}

// Expect the given string.
func expect(lg io.Writer, lines <-chan string, exp string) error {
	// Read lines, and if we hit a timeout before seeing our
	// expected line, then consider that an error.
	fmt.Fprintf(lg, "---- expect: %q\n", exp)

	stopper := time.NewTimer(10 * time.Second)
	defer stopper.Stop()
outer:
	for {
		select {
		case line := <-lines:
			fmt.Fprintf(lg, "---- target: %q\n", line)
			if strings.Contains(line, exp) {
				break outer
			}
		case <-stopper.C:
			fmt.Fprintf(lg, "timeout, didn't receive output\n")
			return fmt.Errorf("timeout, didn't receive expected string: %q", exp)
		}
	}

	return nil
}

// Read things from the log file, discarding everything already there.
func readLog(sink chan<- string) {
	file, err := os.Open(*logIn)
	if err != nil {
		log.Fatal(err)
	}

	_, err = file.Seek(0, 2)
	if err != nil {
		log.Fatal(err)
	}

	prefix := ""
	for {
		// Read lines until EOF, then delay a bit, and do it
		// all again.
		rd := bufio.NewReader(file)

		for {
			line, err := rd.ReadString('\n')
			if err == io.EOF {
				// A partial line can happen because
				// we are racing with the writer.
				if line != "" {
					prefix = line
				}
				break
			}
			if err != nil {
				log.Fatal(err)
			}

			line = prefix + line
			prefix = ""
			sink <- line
			// fmt.Printf("line: %q\n", line)
		}

		// Pause a little
		time.Sleep(250 * time.Millisecond)
	}
}

// An Extractor holds an opened Zip file, and is able to extract files
// based on the directory name.
type Extractor struct {
	file *os.File
	zip  *zip.Reader
}

// NewExtractor returns an Extractor based on the contents of a zip
// file.
func NewExtractor(name string) (*Extractor, error) {
	f, err := os.Open(name)
	if err != nil {
		return nil, err
	}
	size, err := f.Seek(0, 2)
	if err != nil {
		f.Close()
		return nil, err
	}

	rd, err := zip.NewReader(f, size)
	if err != nil {
		f.Close()
		return nil, err
	}

	return &Extractor{
		file: f,
		zip:  rd,
	}, nil
}

func (e *Extractor) Close() error {
	return e.file.Close()
}

// Extract extracts the files of the given directory name into the
// current directory.  These files will overwrite any files of these
// names that already exist (presumably from previous extractions).
func (e *Extractor) Extract(dir string) error {
	prefix := dir + "/"

	count := 0
	for _, file := range e.zip.File {
		if len(file.Name) > len(prefix) && strings.HasPrefix(file.Name, prefix) {
			outName := file.Name[len(prefix):len(file.Name)]
			fmt.Printf("->%q\n", outName)

			err := e.single(file, outName)
			if err != nil {
				return err
			}

			count += 1
		}
	}

	if count == 0 {
		return fmt.Errorf("File for %s missing from archive", dir)
	}

	return nil
}

// single extracts a single file from the zip archive, writing the
// results to a file 'outName'.
func (e *Extractor) single(file *zip.File, outName string) error {
	inf, err := file.Open()
	if err != nil {
		return err
	}

	outf, err := os.Create(outName)
	if err != nil {
		return err
	}
	defer outf.Close()

	_, err = io.Copy(outf, inf)
	return err
}