package eval_test

import (
	"strconv"
	"sync"
	"syscall"
	"testing"

	. "src.elv.sh/pkg/eval"
	"src.elv.sh/pkg/prog"

	. "src.elv.sh/pkg/eval/evaltest"
	"src.elv.sh/pkg/eval/vals"
	"src.elv.sh/pkg/eval/vars"
	"src.elv.sh/pkg/parse"
	"src.elv.sh/pkg/testutil"
)

func TestPid(t *testing.T) {
	pid := strconv.Itoa(syscall.Getpid())
	Test(t, That("put $pid").Puts(pid))
}

func TestNumBgJobs(t *testing.T) {
	Test(t,
		That("put $num-bg-jobs").Puts("0"),
		// TODO(xiaq): Test cases where $num-bg-jobs > 0. This cannot be done
		// with { put $num-bg-jobs }& because the output channel may have
		// already been closed when the closure is run.
	)
}

func TestArgs(t *testing.T) {
	Test(t,
		That("put $args").Puts(vals.EmptyList))
	TestWithSetup(t,
		func(ev *Evaler) { ev.Args = vals.MakeList("foo", "bar") },
		That("put $args").Puts(vals.MakeList("foo", "bar")))
}

func TestEvalTimeDeprecate(t *testing.T) {
	testutil.Set(t, &prog.DeprecationLevel, 42)
	testutil.InTempDir(t)

	TestWithSetup(t, func(ev *Evaler) {
		ev.ExtendGlobal(BuildNs().AddGoFn("dep", func(fm *Frame) {
			fm.Deprecate("deprecated", nil, 42)
		}))
	},
		That("dep").PrintsStderrWith("deprecated"),
		// Deprecation message from the same location is only shown once.
		That("fn f { dep }", "f 2> tmp.txt; f").DoesNothing(),
	)
}

func TestMultipleEval(t *testing.T) {
	Test(t,
		That("var x = hello").Then("put $x").Puts("hello"),

		// Shadowing with fn. Regression test for #1213.
		That("fn f { put old }").Then("fn f { put new }").Then("f").
			Puts("new"),
		// Variable deletion. Regression test for #1213.
		That("var x = foo").Then("del x").Then("put $x").DoesNotCompile(),
	)
}

func TestEval_AlternativeGlobal(t *testing.T) {
	ev := NewEvaler()
	g := BuildNs().AddVar("a", vars.NewReadOnly("")).Ns()
	err := ev.Eval(parse.Source{Name: "[test]", Code: "nop $a"}, EvalCfg{Global: g})
	if err != nil {
		t.Errorf("got error %v, want nil", err)
	}
	// Regression test for #1223
	if ev.Global().HasKeyString("a") {
		t.Errorf("$a from alternative global leaked into Evaler global")
	}
}

func TestEval_Concurrent(t *testing.T) {
	ev := NewEvaler()

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		ev.Eval(parse.Source{Name: "[test]", Code: "var a"}, EvalCfg{})
		wg.Done()
	}()
	go func() {
		ev.Eval(parse.Source{Name: "[test]", Code: "var b"}, EvalCfg{})
		wg.Done()
	}()
	wg.Wait()
	g := ev.Global()
	if !g.HasKeyString("a") {
		t.Errorf("variable $a not created")
	}
	if !g.HasKeyString("b") {
		t.Errorf("variable $b not created")
	}
}

type fooOpts struct{ Opt string }

func (*fooOpts) SetDefaultOptions() {}

func TestCall(t *testing.T) {
	ev := NewEvaler()
	var gotOpt, gotArg string
	fn := NewGoFn("foo", func(fm *Frame, opts fooOpts, arg string) {
		gotOpt = opts.Opt
		gotArg = arg
	})

	passedArg := "arg value"
	passedOpt := "opt value"
	ev.Call(fn,
		CallCfg{
			Args: []any{passedArg},
			Opts: map[string]any{"opt": passedOpt},
			From: "[TestCall]"},
		EvalCfg{})

	if gotArg != passedArg {
		t.Errorf("got arg %q, want %q", gotArg, passedArg)
	}
	if gotOpt != passedOpt {
		t.Errorf("got opt %q, want %q", gotOpt, passedOpt)
	}
}

var checkTests = []struct {
	name           string
	code           string
	wantParseErr   bool
	wantCompileErr bool
}{
	{name: "no error", code: "put $nil"},
	{name: "parse error only", code: "put [",
		wantParseErr: true},
	{name: "compile error only", code: "put $x",
		wantCompileErr: true},
	{name: "both parse and compile error", code: "put [$x",
		wantParseErr: true, wantCompileErr: true},
}

func TestCheck(t *testing.T) {
	ev := NewEvaler()
	for _, test := range checkTests {
		t.Run(test.name, func(t *testing.T) {
			parseErr, compileErr := ev.Check(parse.Source{Name: "[test]", Code: test.code}, nil)
			if (parseErr != nil) != test.wantParseErr {
				t.Errorf("got parse error %v, when wantParseErr = %v",
					parseErr, test.wantParseErr)
			}
			if (compileErr != nil) != test.wantCompileErr {
				t.Errorf("got compile error %v, when wantCompileErr = %v",
					compileErr, test.wantCompileErr)
			}
		})
	}
}
