package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"

	"github.com/influxdata/flux"
	"github.com/influxdata/flux/ast"
	"github.com/influxdata/flux/cmd/flux/cmd"
	"github.com/influxdata/flux/lang"
	"github.com/influxdata/flux/parser"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/cmd/influxd/launcher"
	"github.com/influxdata/influxdb/v2/query"
	"github.com/spf13/cobra"
)

type testFlags struct {
	wait bool
}

var flags testFlags

type testExecutor struct {
	ctx         context.Context
	l           *launcher.TestLauncher
	writeOptAST *ast.File
	readOptAST  *ast.File
	errOutput   bytes.Buffer
	i           int
	failed      bool
}

func NewTestExecutor(ctx context.Context) (cmd.TestExecutor, error) {
	e := &testExecutor{ctx: ctx}
	e.init()

	e.l = launcher.NewTestLauncher()
	if err := e.l.Run(e, ctx); err != nil {
		_ = e.l.Shutdown(context.Background())
		return nil, err
	}

	if err := e.l.Setup(); err != nil {
		_ = e.l.Shutdown(context.Background())
		return nil, err
	}
	return e, nil
}

func (t *testExecutor) init() {
	t.writeOptAST = prepareOptions(writeOptSource)
	t.readOptAST = prepareOptions(readOptSource)
}

func (t *testExecutor) Close() error {
	if t.l == nil {
		return nil
	}

	if err := t.l.Shutdown(context.Background()); err != nil {
		return err
	}
	t.l = nil

	if t.Failed() {
		_, _ = io.Copy(os.Stdout, &t.errOutput)
	}
	return nil
}

func (t *testExecutor) Run(pkg *ast.Package, fn cmd.TestResultFunc) error {
	l := t.l.Launcher
	b := &influxdb.Bucket{
		OrgID: t.l.Org.ID,
		Name:  fmt.Sprintf("%04d", t.i),
	}
	t.i++

	s := l.BucketService()
	if err := s.CreateBucket(t.ctx, b); err != nil {
		return err
	}
	defer func() { _ = s.DeleteBucket(t.ctx, b.ID) }()

	// Define bucket and org options
	bucketOpt := &ast.OptionStatement{
		Assignment: &ast.VariableAssignment{
			ID:   &ast.Identifier{Name: "bucket"},
			Init: &ast.StringLiteral{Value: b.Name},
		},
	}
	orgOpt := &ast.OptionStatement{
		Assignment: &ast.VariableAssignment{
			ID:   &ast.Identifier{Name: "org"},
			Init: &ast.StringLiteral{Value: t.l.Org.Name},
		},
	}

	// During the first execution, we are performing the writes
	// that are in the testcase.
	err := t.executeWithOptions(bucketOpt, orgOpt, t.writeOptAST, pkg, func(ctx context.Context, results flux.ResultIterator) error {
		for results.More() {
			res := results.Next()
			if err := res.Tables().Do(func(table flux.Table) error {
				table.Done()
				return nil
			}); err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		// Some test assertions can fail in the first pass, so those errors do not fail the test case.
		// However those errors can be useful when the error is unexpected, therefore we simply log the error here.
		fmt.Printf("Error from write pass: %s\n", err)
	}

	// Execute the read pass.
	err = t.executeWithOptions(bucketOpt, orgOpt, t.readOptAST, pkg, fn)
	if flags.wait {
		// TODO(nathanielc): When the executor is given access to the test name,
		// make the configName a function of the test name.
		configName := "flux-test"
		err := createInfluxDBConfig(t.ctx, configName, t.l.URL().String(), t.l.Org.Name, t.l.Auth.Token)
		if err != nil {
			return err
		}
		fmt.Printf("Use: `influx -c %s` to connect to the running test instance.\n", configName)
		fmt.Printf("Test bucket is: %q\n", b.Name)
		fmt.Println("Waiting, press enter to continue.")
		wait := make(chan struct{})
		go func() {
			scanner := bufio.NewScanner(os.Stdin)
			scanner.Scan()
			close(wait)
		}()

		// wait for input on stdin or context cancelled
		select {
		case <-wait:
		case <-t.ctx.Done():
		}
		err = rmInfluxDBConfig(t.ctx, configName)
		if err != nil {
			return err
		}
	}
	return err
}

func (t *testExecutor) executeWithOptions(bucketOpt, orgOpt *ast.OptionStatement, optionsAST *ast.File, pkg *ast.Package, fn cmd.TestResultFunc) error {
	options := optionsAST.Copy().(*ast.File)
	options.Body = append([]ast.Statement{bucketOpt, orgOpt}, options.Body...)

	// Add options to pkg
	pkg = pkg.Copy().(*ast.Package)
	pkg.Files = append([]*ast.File{options}, pkg.Files...)

	bs, err := json.Marshal(pkg)
	if err != nil {
		return err
	}

	req := &query.Request{
		OrganizationID: t.l.Org.ID,
		Compiler:       lang.ASTCompiler{AST: bs},
	}

	r, err := t.l.FluxQueryService().Query(t.ctx, req)
	if err != nil {
		return err
	}
	defer r.Release()

	return fn(t.ctx, r)
}

// This options definition puts to() in the path of the CSV input. The tests
// get run in this case and they would normally pass, if we checked the
// results, but don't look at them.
const writeOptSource = `
import "testing"
import c "csv"

option testing.load = (tables=<-) => {
	return tables |> to(bucket: bucket, org: org)
}
`

// This options definition is for the second run, the test run. It loads the
// data from previously written bucket. We check the results after running this
// second pass and report on them.
const readOptSource = `
import "testing"
import c "csv"

option testing.load = (tables=<-) => {
	return from(bucket: bucket)
}
`

func prepareOptions(optionsSource string) *ast.File {
	pkg := parser.ParseSource(optionsSource)
	if ast.Check(pkg) > 0 {
		panic(ast.GetError(pkg))
	}
	return pkg.Files[0]
}

func (t *testExecutor) Logf(s string, i ...interface{}) {
	_, _ = fmt.Fprintf(&t.errOutput, s, i...)
	_, _ = fmt.Fprintln(&t.errOutput)
}

func (t *testExecutor) Errorf(s string, i ...interface{}) {
	t.Logf(s, i...)
	t.Fail()
}

func (t *testExecutor) Fail() {
	t.failed = true
}

func (t *testExecutor) Failed() bool {
	return t.failed
}

func (t *testExecutor) Name() string {
	return "flux"
}

func (t *testExecutor) FailNow() {
	t.Fail()
	panic(errors.New("abort"))
}

func tryExec(cmd *cobra.Command) (err error) {
	defer func() {
		if e := recover(); e != nil {
			var ok bool
			err, ok = e.(error)
			if !ok {
				err = errors.New(fmt.Sprint(e))
			}
		}
	}()
	err = cmd.Execute()
	return
}

func main() {
	c := cmd.TestCommand(NewTestExecutor)
	c.Use = "fluxtest-harness-influxdb"
	c.Flags().BoolVarP(&flags.wait, "wait", "w", false, "Wait for a kill signal before shutting down the InfluxDB test instances.")
	if err := tryExec(c); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func createInfluxDBConfig(ctx context.Context, name, host, org, token string) error {
	cmd := exec.CommandContext(ctx, "influx", "config", "create", "-n", name, "-u", host, "-o", org, "-t", token)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("Failed to create influx config. Error: %s Output: %s", err, string(out))
	}
	return nil
}
func rmInfluxDBConfig(ctx context.Context, name string) error {
	cmd := exec.CommandContext(ctx, "influx", "config", "rm", name)
	out, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("Failed to remove influx config. Error: %s Output: %s", err, string(out))
	}
	return nil
}
