package main

import (
	"container/list"
	"context"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"time"

	"github.com/fatih/color"
	"github.com/pkg/errors"

	"github.com/zrepl/zrepl/daemon/logging/trace"

	"github.com/zrepl/zrepl/config"
	"github.com/zrepl/zrepl/daemon/logging"
	"github.com/zrepl/zrepl/logger"
	"github.com/zrepl/zrepl/platformtest"
	"github.com/zrepl/zrepl/platformtest/tests"
)

var bold = color.New(color.Bold)
var boldRed = color.New(color.Bold, color.FgHiRed)
var boldGreen = color.New(color.Bold, color.FgHiGreen)

const DefaultPoolImageSize = 200 * (1 << 20)

func main() {

	var args HarnessArgs

	flag.StringVar(&args.CreateArgs.PoolName, "poolname", "", "")
	flag.StringVar(&args.CreateArgs.ImagePath, "imagepath", "", "")
	flag.Int64Var(&args.CreateArgs.ImageSize, "imagesize", DefaultPoolImageSize, "")
	flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
	flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
	flag.StringVar(&args.Run, "run", "", "")
	flag.DurationVar(&platformtest.ZpoolExportTimeout, "zfs.zpool-export-timeout", platformtest.ZpoolExportTimeout, "")
	flag.Parse()

	if err := HarnessRun(args); err != nil {
		os.Exit(1)
	}
}

var exitWithErr = fmt.Errorf("exit with error")

type HarnessArgs struct {
	CreateArgs            platformtest.ZpoolCreateArgs
	StopAndKeepPoolOnFail bool
	Run                   string
}

type invocation struct {
	runFunc  tests.Case
	idstring string
	result   *testCaseResult
	children map[string]*invocation
}

func newInvocation(runFunc tests.Case, id string) *invocation {
	return &invocation{
		runFunc:  runFunc,
		idstring: id,
		children: make(map[string]*invocation),
	}
}

func (i *invocation) String() string {
	idsuffix := ""
	if i.idstring != "" {
		idsuffix = fmt.Sprintf(": %s", i.idstring)
	}
	return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
}

func (i *invocation) RegisterChild(c *invocation) error {
	if c.idstring == "" {
		return fmt.Errorf("child must have id string")
	}
	if oc := i.children[c.idstring]; oc != nil {
		return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
	}
	i.children[c.idstring] = c
	return nil
}

func HarnessRun(args HarnessArgs) error {

	runRE := regexp.MustCompile(args.Run)

	outlets := logger.NewOutlets()
	outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
		LoggingOutletCommon: config.LoggingOutletCommon{
			Level:  "debug",
			Format: "human",
		},
	}})
	if err != nil {
		panic(err)
	}
	outlets.Add(outlet, level)
	logger := logger.NewLogger(outlets, 1*time.Second)

	if err := args.CreateArgs.Validate(); err != nil {
		logger.Error(err.Error())
		panic(err)
	}
	ctx := context.Background()
	defer trace.WithTaskFromStackUpdateCtx(&ctx)()
	ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
	ex := platformtest.NewEx(logger)

	testQueue := list.New()
	for _, c := range tests.Cases {
		if runRE.MatchString(c.String()) {
			testQueue.PushBack(newInvocation(c, ""))
		}
	}

	completedTests := list.New()

	for testQueue.Len() > 0 {
		inv := testQueue.Remove(testQueue.Front()).(*invocation)

		bold.Printf("BEGIN TEST CASE %s\n", inv)

		pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
		if err != nil {
			panic(errors.Wrap(err, "create test pool"))
		}

		ctx := &platformtest.Context{
			Context:     ctx,
			RootDataset: filepath.Join(pool.Name(), "rootds"),
			QueueSubtest: func(id string, stf func(*platformtest.Context)) {
				stinv := newInvocation(stf, id)
				err := inv.RegisterChild(stinv)
				if err != nil {
					panic(err)
				}
				bold.Printf(" QUEUING SUBTEST %q\n", id)
				testQueue.PushFront(stinv)
			},
		}

		res := runTestCase(ctx, ex, inv.runFunc)
		inv.result = res
		if res.failed {
			fmt.Printf("%+v\n", res.failedStack) // print with stack trace
		}

		if res.failed && args.StopAndKeepPoolOnFail {
			boldRed.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
			return exitWithErr
		}

		if err := pool.Destroy(ctx, ex); err != nil {
			panic(fmt.Sprintf("error destroying test pool: %s", err))
		}

		completedTests.PushBack(inv)

		if res.failed {
			boldRed.Printf("TEST FAILED\n")
		} else if res.skipped {
			bold.Printf("TEST SKIPPED\n")
		} else if res.succeeded {
			boldGreen.Printf("TEST PASSED\n")
		} else {
			panic("unreachable")
		}

		fmt.Println()
	}

	var summary struct {
		succ, fail, skip []*invocation
	}
	for completedTests.Len() > 0 {
		inv := completedTests.Remove(completedTests.Front()).(*invocation)
		var bucket *[]*invocation
		if inv.result.failed {
			bucket = &summary.fail
		} else if inv.result.skipped {
			bucket = &summary.skip
		} else if inv.result.succeeded {
			bucket = &summary.succ
		} else {
			panic("unreachable")
		}
		*bucket = append(*bucket, inv)
	}
	printBucket := func(bucketName string, c *color.Color, bucket []*invocation) {
		c.Printf("%s:", bucketName)
		if len(bucket) == 0 {
			fmt.Printf(" []\n")
			return
		}
		fmt.Printf("\n")
		for _, inv := range bucket {
			fmt.Printf("  %s\n", inv)
		}
	}
	printBucket("PASSING TESTS", boldGreen, summary.succ)
	printBucket("SKIPPED TESTS", bold, summary.skip)
	printBucket("FAILED TESTS", boldRed, summary.fail)

	if len(summary.fail) > 0 {
		return errors.New("at least one test failed")
	}
	return nil
}

type testCaseResult struct {
	// oneof
	failed, skipped, succeeded bool

	failedStack error // has stack inside, valid if failed=true
}

func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {

	// run case
	var panicked = false
	var panicValue interface{} = nil
	var panicStack error
	func() {
		defer func() {
			if item := recover(); item != nil {
				panicValue = item
				panicked = true
				panicStack = errors.Errorf("panic while running test: %v", panicValue)
			}
		}()
		c(ctx)
	}()

	if panicked {
		switch panicValue {
		case platformtest.SkipNowSentinel:
			return &testCaseResult{skipped: true}
		case platformtest.FailNowSentinel:
			return &testCaseResult{failed: true, failedStack: panicStack}
		default:
			return &testCaseResult{failed: true, failedStack: panicStack}
		}
	} else {
		return &testCaseResult{succeeded: true}
	}

}