// Copyright 2018 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package vm

import (
	"bytes"
	"fmt"
	"testing"
	"time"

	"github.com/google/syzkaller/pkg/mgrconfig"
	"github.com/google/syzkaller/pkg/report"
	"github.com/google/syzkaller/sys/targets"
	"github.com/google/syzkaller/vm/vmimpl"
)

type testPool struct {
}

func (pool *testPool) Count() int {
	return 1
}

func (pool *testPool) Create(workdir string, index int) (vmimpl.Instance, error) {
	return &testInstance{
		outc: make(chan []byte, 10),
		errc: make(chan error, 1),
	}, nil
}

func (pool *testPool) Close() error {
	return nil
}

type testInstance struct {
	outc           chan []byte
	errc           chan error
	diagnoseBug    bool
	diagnoseNoWait bool
}

func (inst *testInstance) Copy(hostSrc string) (string, error) {
	return "", nil
}

func (inst *testInstance) Forward(port int) (string, error) {
	return "", nil
}

func (inst *testInstance) Run(timeout time.Duration, stop <-chan bool, command string) (
	outc <-chan []byte, errc <-chan error, err error) {
	return inst.outc, inst.errc, nil
}

func (inst *testInstance) Diagnose(rep *report.Report) ([]byte, bool) {
	var diag []byte
	if inst.diagnoseBug {
		diag = []byte("BUG: DIAGNOSE\n")
	} else {
		diag = []byte("DIAGNOSE\n")
	}

	if inst.diagnoseNoWait {
		return diag, false
	}

	inst.outc <- diag
	return nil, true
}

func (inst *testInstance) Close() {
}

func init() {
	beforeContextDefault = maxErrorLength + 100
	tickerPeriod = 1 * time.Second
	waitForOutputTimeout = 3 * time.Second

	ctor := func(env *vmimpl.Env) (vmimpl.Pool, error) {
		return &testPool{}, nil
	}
	vmimpl.Register("test", ctor, false)
}

type Test struct {
	Name           string
	Exit           ExitCondition
	DiagnoseBug    bool // Diagnose produces output that is detected as kernel crash.
	DiagnoseNoWait bool // Diagnose returns output directly rather than to console.
	Body           func(outc chan []byte, errc chan error)
	Report         *report.Report
}

var tests = []*Test{
	{
		Name: "program-exits-normally",
		Exit: ExitNormal,
		Body: func(outc chan []byte, errc chan error) {
			time.Sleep(time.Second)
			errc <- nil
		},
	},
	{
		Name: "program-exits-when-it-should-not",
		Body: func(outc chan []byte, errc chan error) {
			time.Sleep(time.Second)
			errc <- nil
		},
		Report: &report.Report{
			Title: lostConnectionCrash,
		},
	},
	{
		Name:        "#875-diagnose-bugs",
		Exit:        ExitNormal,
		DiagnoseBug: true,
		Body: func(outc chan []byte, errc chan error) {
			errc <- nil
		},
	},
	{
		Name: "#875-diagnose-bugs-2",
		Body: func(outc chan []byte, errc chan error) {
			errc <- nil
		},
		Report: &report.Report{
			Title: lostConnectionCrash,
			Output: []byte(
				"DIAGNOSE\n",
			),
		},
	},
	{
		Name: "diagnose-no-wait",
		Body: func(outc chan []byte, errc chan error) {
			errc <- nil
		},
		DiagnoseNoWait: true,
		Report: &report.Report{
			Title: lostConnectionCrash,
			Output: []byte(
				"\n" +
					"VM DIAGNOSIS:\n" +
					"DIAGNOSE\n",
			),
		},
	},
	{
		Name: "diagnose-bug-no-wait",
		Body: func(outc chan []byte, errc chan error) {
			outc <- []byte("BUG: bad\n")
			time.Sleep(time.Second)
			outc <- []byte("other output\n")
		},
		DiagnoseNoWait: true,
		Report: &report.Report{
			Title: "BUG: bad",
			Report: []byte(
				"BUG: bad\n" +
					"other output\n",
			),
			Output: []byte(
				"BUG: bad\n" +
					"other output\n" +
					"\n" +
					"VM DIAGNOSIS:\n" +
					"DIAGNOSE\n",
			),
		},
	},
	{
		Name: "kernel-crashes",
		Body: func(outc chan []byte, errc chan error) {
			outc <- []byte("BUG: bad\n")
			time.Sleep(time.Second)
			outc <- []byte("other output\n")
		},
		Report: &report.Report{
			Title: "BUG: bad",
			Report: []byte(
				"BUG: bad\n" +
					"DIAGNOSE\n" +
					"other output\n",
			),
		},
	},
	{
		Name: "fuzzer-is-preempted",
		Body: func(outc chan []byte, errc chan error) {
			outc <- []byte("BUG: bad\n")
			outc <- []byte(fuzzerPreemptedStr + "\n")
		},
	},
	{
		Name: "program-exits-but-kernel-crashes-afterwards",
		Exit: ExitNormal,
		Body: func(outc chan []byte, errc chan error) {
			errc <- nil
			time.Sleep(time.Second)
			outc <- []byte("BUG: bad\n")
		},
		Report: &report.Report{
			Title: "BUG: bad",
			Report: []byte(
				"BUG: bad\n" +
					"DIAGNOSE\n",
			),
		},
	},
	{
		Name: "timeout",
		Exit: ExitTimeout,
		Body: func(outc chan []byte, errc chan error) {
			errc <- vmimpl.ErrTimeout
		},
	},
	{
		Name: "bad-timeout",
		Body: func(outc chan []byte, errc chan error) {
			errc <- vmimpl.ErrTimeout
		},
		Report: &report.Report{
			Title: timeoutCrash,
		},
	},
	{
		Name: "program-crashes",
		Body: func(outc chan []byte, errc chan error) {
			errc <- fmt.Errorf("error")
		},
		Report: &report.Report{
			Title: lostConnectionCrash,
		},
	},
	{
		Name: "program-crashes-expected",
		Exit: ExitError,
		Body: func(outc chan []byte, errc chan error) {
			errc <- fmt.Errorf("error")
		},
	},
	{
		Name: "no-output-1",
		Body: func(outc chan []byte, errc chan error) {
		},
		Report: &report.Report{
			Title: noOutputCrash,
		},
	},
	{
		Name: "no-output-2",
		Body: func(outc chan []byte, errc chan error) {
			for i := 0; i < 5; i++ {
				time.Sleep(time.Second)
				outc <- []byte("something\n")
			}
		},
		Report: &report.Report{
			Title: noOutputCrash,
		},
	},
	{
		Name: "no-no-output-1",
		Exit: ExitNormal,
		Body: func(outc chan []byte, errc chan error) {
			for i := 0; i < 5; i++ {
				time.Sleep(time.Second)
				outc <- append(executingProgram1, '\n')
			}
			errc <- nil
		},
	},
	{
		Name: "no-no-output-2",
		Exit: ExitNormal,
		Body: func(outc chan []byte, errc chan error) {
			for i := 0; i < 5; i++ {
				time.Sleep(time.Second)
				outc <- append(executingProgram2, '\n')
			}
			errc <- nil
		},
	},
	{
		Name: "outc-closed",
		Exit: ExitTimeout,
		Body: func(outc chan []byte, errc chan error) {
			close(outc)
			time.Sleep(time.Second)
			errc <- vmimpl.ErrTimeout
		},
	},
	{
		Name: "lots-of-output",
		Exit: ExitTimeout,
		Body: func(outc chan []byte, errc chan error) {
			for i := 0; i < 100; i++ {
				outc <- []byte("something\n")
			}
			time.Sleep(time.Second)
			errc <- vmimpl.ErrTimeout
		},
	},
	{
		Name: "split-line",
		Exit: ExitNormal,
		Body: func(outc chan []byte, errc chan error) {
			// "ODEBUG:" lines should be ignored, however the matchPos logic
			// used to trim the lines so that we could see just "BUG:" later
			// and detect it as crash.
			buf := new(bytes.Buffer)
			for i := 0; i < 50; i++ {
				buf.WriteString("[ 2886.597572] ODEBUG: Out of memory. ODEBUG disabled\n")
				buf.Write(bytes.Repeat([]byte{'-'}, i))
				buf.WriteByte('\n')
			}
			output := buf.Bytes()
			for i := range output {
				outc <- output[i : i+1]
			}
			errc <- nil
		},
	},
}

func TestMonitorExecution(t *testing.T) {
	for _, test := range tests {
		test := test
		t.Run(test.Name, func(t *testing.T) {
			t.Parallel()
			testMonitorExecution(t, test)
		})
	}
}

func testMonitorExecution(t *testing.T, test *Test) {
	dir := t.TempDir()
	cfg := &mgrconfig.Config{
		Derived: mgrconfig.Derived{
			TargetOS:     targets.Linux,
			TargetArch:   targets.AMD64,
			TargetVMArch: targets.AMD64,
			Timeouts: targets.Timeouts{
				Scale:    1,
				Slowdown: 1,
				NoOutput: 5 * time.Second,
			},
		},
		Workdir: dir,
		Type:    "test",
	}
	pool, err := Create(cfg, false)
	if err != nil {
		t.Fatal(err)
	}
	defer pool.Close()
	reporter, err := report.NewReporter(cfg)
	if err != nil {
		t.Fatal(err)
	}
	inst, err := pool.Create(0)
	if err != nil {
		t.Fatal(err)
	}
	defer inst.Close()
	outc, errc, err := inst.Run(time.Second, nil, "")
	if err != nil {
		t.Fatal(err)
	}
	testInst := inst.impl.(*testInstance)
	testInst.diagnoseBug = test.DiagnoseBug
	testInst.diagnoseNoWait = test.DiagnoseNoWait
	done := make(chan bool)
	go func() {
		test.Body(testInst.outc, testInst.errc)
		done <- true
	}()
	rep := inst.MonitorExecution(outc, errc, reporter, test.Exit)
	<-done
	if test.Report != nil && rep == nil {
		t.Fatalf("got no report")
	}
	if test.Report == nil && rep != nil {
		t.Fatalf("got unexpected report: %v", rep.Title)
	}
	if test.Report == nil {
		return
	}
	if test.Report.Title != rep.Title {
		t.Fatalf("want title %q, got title %q", test.Report.Title, rep.Title)
	}
	if !bytes.Equal(test.Report.Report, rep.Report) {
		t.Fatalf("want report:\n%s\n\ngot report:\n%s\n", test.Report.Report, rep.Report)
	}
	if test.Report.Output != nil && !bytes.Equal(test.Report.Output, rep.Output) {
		t.Fatalf("want output:\n%s\n\ngot output:\n%s\n", test.Report.Output, rep.Output)
	}
}

func TestVMType(t *testing.T) {
	testCases := []struct {
		in   string
		want string
	}{
		{"gvisor", "gvisor"},
		{"proxyapp:android", "proxyapp"},
	}

	for _, tc := range testCases {
		if got := vmType(tc.in); got != tc.want {
			t.Errorf("vmType(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}
