package sandbox

import (
	"context"
	"strings"
	"testing"
	"time"

	"go.starlark.net/starlark"
)

func TestExecBasic(t *testing.T) {
	sb := New(DefaultConfig())
	res, err := sb.Exec(context.Background(), "test.star", `
x = 1 + 2
name = "hello"
`, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if v, ok := res.Globals["x"]; !ok {
		t.Fatal("missing global 'x'")
	} else if v.String() != "3" {
		t.Fatalf("expected x=3, got %s", v.String())
	}
	if v, ok := res.Globals["name"]; !ok {
		t.Fatal("missing global 'name'")
	} else if v.(starlark.String).GoString() != "hello" {
		t.Fatalf("expected name='hello', got %s", v.String())
	}
}

func TestExecPrintCapture(t *testing.T) {
	sb := New(DefaultConfig())
	res, err := sb.Exec(context.Background(), "test.star", `
print("line one")
print("line two")
`, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	expected := "line one\nline two\n"
	if res.Output != expected {
		t.Fatalf("expected output %q, got %q", expected, res.Output)
	}
}

func TestExecStepsTracked(t *testing.T) {
	sb := New(DefaultConfig())
	res, err := sb.Exec(context.Background(), "test.star", `
def work():
    x = 0
    for i in range(100):
        x += i
    return x
result = work()
`, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if res.Steps == 0 {
		t.Fatal("expected steps > 0")
	}
}

func TestExecStepLimitExceeded(t *testing.T) {
	sb := New(Config{MaxSteps: 100, Name: "limited"})
	_, err := sb.Exec(context.Background(), "test.star", `
def work():
    x = 0
    for i in range(10000):
        x += i
    return x
result = work()
`, nil)
	if err == nil {
		t.Fatal("expected step limit error")
	}
	// Error message varies: "Starlark computation cancelled: exceeded ..."
	// or "context canceled" — just verify it failed.
}

func TestExecContextTimeout(t *testing.T) {
	// Use step limit of 0 (unlimited) so only context timeout triggers
	sb := New(Config{MaxSteps: 0, Name: "timeout"})
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	_, err := sb.Exec(ctx, "test.star", `
def work():
    x = 0
    for i in range(100000000):
        x += i
    return x
result = work()
`, nil)
	if err == nil {
		t.Fatal("expected timeout error")
	}
}

func TestExecLoadDisabled(t *testing.T) {
	sb := New(DefaultConfig())
	_, err := sb.Exec(context.Background(), "test.star", `
load("other.star", "foo")
`, nil)
	if err == nil {
		t.Fatal("expected load error")
	}
	if !strings.Contains(err.Error(), "disabled") {
		t.Fatalf("expected 'disabled' in error, got: %v", err)
	}
}

func TestExecModuleInjection(t *testing.T) {
	getFn := starlark.NewBuiltin("test.get", func(
		thread *starlark.Thread, b *starlark.Builtin,
		args starlark.Tuple, kwargs []starlark.Tuple,
	) (starlark.Value, error) {
		return starlark.String("secret_value"), nil
	})

	mod := MakeModule("test", starlark.StringDict{
		"get": getFn,
	})

	sb := New(DefaultConfig())
	res, err := sb.Exec(context.Background(), "test.star", `
result = test.get()
`, map[string]starlark.Value{"test": mod})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	v, ok := res.Globals["result"]
	if !ok {
		t.Fatal("missing global 'result'")
	}
	if v.(starlark.String).GoString() != "secret_value" {
		t.Fatalf("expected 'secret_value', got %s", v.String())
	}
}

func TestExecSyntaxError(t *testing.T) {
	sb := New(DefaultConfig())
	_, err := sb.Exec(context.Background(), "bad.star", `
def broken(
`, nil)
	if err == nil {
		t.Fatal("expected syntax error")
	}
	se, ok := err.(*SandboxError)
	if !ok {
		t.Fatalf("expected *SandboxError, got %T", err)
	}
	_ = se // syntax errors may not have backtrace
}

func TestExecRuntimeError(t *testing.T) {
	sb := New(DefaultConfig())
	_, err := sb.Exec(context.Background(), "runtime.star", `
x = 1 / 0
`, nil)
	if err == nil {
		t.Fatal("expected runtime error")
	}
	se, ok := err.(*SandboxError)
	if !ok {
		t.Fatalf("expected *SandboxError, got %T", err)
	}
	if se.Backtrace == "" {
		t.Fatal("expected backtrace for runtime error")
	}
}

func TestCallFunction(t *testing.T) {
	sb := New(DefaultConfig())

	res, err := sb.Exec(context.Background(), "funcs.star", `
def greet(name):
    return "hello, " + name
`, nil)
	if err != nil {
		t.Fatalf("exec error: %v", err)
	}

	fn := res.Globals["greet"].(starlark.Callable)
	val, _, err := sb.Call(context.Background(), fn,
		starlark.Tuple{starlark.String("world")}, nil)
	if err != nil {
		t.Fatalf("call error: %v", err)
	}
	if val.(starlark.String).GoString() != "hello, world" {
		t.Fatalf("expected 'hello, world', got %s", val.String())
	}
}

func TestCallWithPrint(t *testing.T) {
	sb := New(DefaultConfig())

	res, err := sb.Exec(context.Background(), "funcs.star", `
def run():
    print("running")
    return 42
`, nil)
	if err != nil {
		t.Fatalf("exec error: %v", err)
	}

	fn := res.Globals["run"].(starlark.Callable)
	val, output, err := sb.Call(context.Background(), fn, nil, nil)
	if err != nil {
		t.Fatalf("call error: %v", err)
	}
	if val.String() != "42" {
		t.Fatalf("expected 42, got %s", val.String())
	}
	if !strings.Contains(output, "running") {
		t.Fatalf("expected 'running' in output, got %q", output)
	}
}

func TestCallRuntimeError(t *testing.T) {
	sb := New(DefaultConfig())

	res, err := sb.Exec(context.Background(), "funcs.star", `
def boom():
    return 1 / 0
`, nil)
	if err != nil {
		t.Fatalf("exec error: %v", err)
	}

	fn := res.Globals["boom"].(starlark.Callable)
	_, _, err = sb.Call(context.Background(), fn, nil, nil)
	if err == nil {
		t.Fatal("expected runtime error from Call")
	}
	se, ok := err.(*SandboxError)
	if !ok {
		t.Fatalf("expected *SandboxError, got %T", err)
	}
	if se.Backtrace == "" {
		t.Fatal("expected backtrace")
	}
}

func TestNewDefaults(t *testing.T) {
	sb := New(Config{})
	if sb.config.MaxSteps != 0 {
		t.Fatalf("expected MaxSteps=0 (unlimited), got %d", sb.config.MaxSteps)
	}
	if sb.config.Name != "extension" {
		t.Fatalf("expected default Name='extension', got %q", sb.config.Name)
	}
}

func TestNewWithDefaultConfig(t *testing.T) {
	sb := New(DefaultConfig())
	if sb.config.MaxSteps != 1_000_000 {
		t.Fatalf("expected MaxSteps=1000000, got %d", sb.config.MaxSteps)
	}
}

func TestMakeModule(t *testing.T) {
	mod := MakeModule("mymod", starlark.StringDict{
		"version": starlark.String("1.0"),
	})
	if mod.Name != "mymod" {
		t.Fatalf("expected name 'mymod', got %q", mod.Name)
	}
	v, ok := mod.Members["version"]
	if !ok {
		t.Fatal("missing 'version' member")
	}
	if v.(starlark.String).GoString() != "1.0" {
		t.Fatalf("expected '1.0', got %s", v.String())
	}
}

func TestSandboxErrorUnwrap(t *testing.T) {
	inner := starlark.String("test").Truth() // just need any value
	_ = inner
	se := &SandboxError{
		Err:       context.DeadlineExceeded,
		Backtrace: "",
	}
	if se.Unwrap() != context.DeadlineExceeded {
		t.Fatal("Unwrap should return inner error")
	}
	if se.Error() != "context deadline exceeded" {
		t.Fatalf("unexpected error string: %q", se.Error())
	}
}

func TestSandboxErrorWithBacktrace(t *testing.T) {
	se := &SandboxError{
		Err:       context.DeadlineExceeded,
		Backtrace: "  funcs.star:3:5: in boom",
	}
	if !strings.Contains(se.Error(), "context deadline exceeded") {
		t.Fatal("should contain original error")
	}
	if !strings.Contains(se.Error(), "funcs.star:3:5") {
		t.Fatal("should contain backtrace")
	}
}
