package eventloop

import (
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/projectdiscovery/goja"

	"go.uber.org/goleak"
)

func TestRun(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	var calledAt;
	setTimeout(function() {
		calledAt = now();
	}, 1000);
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	startTime := time.Now()
	loop.Run(func(vm *goja.Runtime) {
		vm.Set("now", time.Now)
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal(err)
	}
	var calledAt time.Time
	loop.Run(func(vm *goja.Runtime) {
		err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
	})
	if err != nil {
		t.Fatal(err)
	}
	if calledAt.IsZero() {
		t.Fatal("Not called")
	}
	if dur := calledAt.Sub(startTime); dur < time.Second {
		t.Fatal(dur)
	}
}

func TestStart(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	var calledAt;
	setTimeout(function() {
		calledAt = now();
	}, 1000);
	`

	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}

	loop := NewEventLoop()
	startTime := time.Now()
	loop.Start()

	loop.RunOnLoop(func(vm *goja.Runtime) {
		vm.Set("now", time.Now)
		vm.RunProgram(prg)
	})

	time.Sleep(2 * time.Second)
	if remainingJobs := loop.Stop(); remainingJobs != 0 {
		t.Fatal(remainingJobs)
	}

	var calledAt time.Time
	loop.Run(func(vm *goja.Runtime) {
		err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
	})
	if err != nil {
		t.Fatal(err)
	}
	if calledAt.IsZero() {
		t.Fatal("Not called")
	}
	if dur := calledAt.Sub(startTime); dur < time.Second {
		t.Fatal(dur)
	}
}

func TestStartInForeground(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	var calledAt;
	setTimeout(function() {
		calledAt = now();
	}, 1000);
	`

	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}

	loop := NewEventLoop()
	startTime := time.Now()
	go loop.StartInForeground()

	loop.RunOnLoop(func(vm *goja.Runtime) {
		vm.Set("now", time.Now)
		vm.RunProgram(prg)
	})

	time.Sleep(2 * time.Second)
	if remainingJobs := loop.Stop(); remainingJobs != 0 {
		t.Fatal(remainingJobs)
	}

	var calledAt time.Time
	loop.Run(func(vm *goja.Runtime) {
		err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
	})
	if err != nil {
		t.Fatal(err)
	}
	if calledAt.IsZero() {
		t.Fatal("Not called")
	}
	if dur := calledAt.Sub(startTime); dur < time.Second {
		t.Fatal(dur)
	}
}

func TestInterval(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	var count = 0;
	var t = setInterval(function(times) {
		console.log("tick");
		if (++count > times) {
			clearInterval(t);
		}
	}, 1000, 2);
	console.log("Started");
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}

	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal(err)
	}

	var count int64
	loop.Run(func(vm *goja.Runtime) {
		count = vm.Get("count").ToInteger()
	})
	if count != 3 {
		t.Fatal(count)
	}
}

func TestImmediate(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	let log = [];
	function cb(arg) {
		log.push(arg);
	}
	var i;
	var t = setImmediate(function() {
		cb("tick");
		setImmediate(cb, "tick 2");
		i = setImmediate(cb, "should not run")
	});
	setImmediate(function() {
		clearImmediate(i);
	});
	cb("Started");
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal(err)
	}

	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunString(`
		if (log.length != 3) {
			throw new Error("Invalid log length: " + log);
		}
		if (log[0] !== "Started" || log[1] !== "tick" || log[2] !== "tick 2") {
			throw new Error("Invalid log: " + log);
		}
		`)
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestRunNoSchedule(t *testing.T) {
	loop := NewEventLoop()
	fired := false
	loop.Run(func(vm *goja.Runtime) { // should not hang
		fired = true
		// do not schedule anything
	})

	if !fired {
		t.Fatal("Not fired")
	}
}

func TestRunWithConsole(t *testing.T) {
	const SCRIPT = `
	console.log("Started");
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal("Call to console.log generated an error", err)
	}

	loop = NewEventLoop(EnableConsole(true))
	prg, err = goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal("Call to console.log generated an error", err)
	}
}

func TestRunNoConsole(t *testing.T) {
	const SCRIPT = `
	console.log("Started");
	`

	loop := NewEventLoop(EnableConsole(false))
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err == nil {
		t.Fatal("Call to console.log did not generate an error", err)
	}
}

func TestClearIntervalRace(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	console.log("calling setInterval");
	var t = setInterval(function() {
		console.log("tick");
	}, 500);
	console.log("calling sleep");
	sleep(2000);
	console.log("calling clearInterval");
	clearInterval(t);
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	// Should not hang
	loop.Run(func(vm *goja.Runtime) {
		vm.Set("sleep", func(ms int) {
			<-time.After(time.Duration(ms) * time.Millisecond)
		})
		vm.RunProgram(prg)
	})
}

func TestNativeTimeout(t *testing.T) {
	t.Parallel()
	fired := false
	loop := NewEventLoop()
	loop.SetTimeout(func(*goja.Runtime) {
		fired = true
	}, 1*time.Second)
	loop.Run(func(*goja.Runtime) {
		// do not schedule anything
	})
	if !fired {
		t.Fatal("Not fired")
	}
}

func TestNativeClearTimeout(t *testing.T) {
	t.Parallel()
	fired := false
	loop := NewEventLoop()
	timer := loop.SetTimeout(func(*goja.Runtime) {
		fired = true
	}, 2*time.Second)
	loop.SetTimeout(func(*goja.Runtime) {
		loop.ClearTimeout(timer)
	}, 1*time.Second)
	loop.Run(func(*goja.Runtime) {
		// do not schedule anything
	})
	if fired {
		t.Fatal("Cancelled timer fired!")
	}
}

func TestNativeInterval(t *testing.T) {
	t.Parallel()
	count := 0
	loop := NewEventLoop()
	var i *Interval
	i = loop.SetInterval(func(*goja.Runtime) {
		t.Log("tick")
		count++
		if count > 2 {
			loop.ClearInterval(i)
		}
	}, 1*time.Second)
	loop.Run(func(*goja.Runtime) {
		// do not schedule anything
	})
	if count != 3 {
		t.Fatal("Expected interval to fire 3 times, got", count)
	}
}

func TestNativeClearInterval(t *testing.T) {
	t.Parallel()
	count := 0
	loop := NewEventLoop()
	loop.Run(func(*goja.Runtime) {
		i := loop.SetInterval(func(*goja.Runtime) {
			t.Log("tick")
			count++
		}, 500*time.Millisecond)
		<-time.After(2 * time.Second)
		loop.ClearInterval(i)
	})
	if count != 0 {
		t.Fatal("Expected interval to fire 0 times, got", count)
	}
}

func TestSetAndClearOnStoppedLoop(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	timeout := loop.SetTimeout(func(runtime *goja.Runtime) {
		panic("must not run")
	}, 1*time.Millisecond)
	loop.ClearTimeout(timeout)
	loop.Start()
	time.Sleep(10 * time.Millisecond)
	loop.Terminate()
}

func TestSetTimeoutConcurrent(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	loop.Start()
	ch := make(chan struct{}, 1)
	loop.SetTimeout(func(*goja.Runtime) {
		ch <- struct{}{}
	}, 100*time.Millisecond)
	<-ch
	loop.Stop()
}

func TestClearTimeoutConcurrent(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	loop.Start()
	timer := loop.SetTimeout(func(*goja.Runtime) {
	}, 100*time.Millisecond)
	loop.ClearTimeout(timer)
	loop.Stop()
	if c := loop.jobCount; c != 0 {
		t.Fatalf("jobCount: %d", c)
	}
}

func TestClearIntervalConcurrent(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	loop.Start()
	ch := make(chan struct{}, 1)
	i := loop.SetInterval(func(*goja.Runtime) {
		ch <- struct{}{}
	}, 500*time.Millisecond)

	<-ch
	loop.ClearInterval(i)
	loop.Stop()
	if c := loop.jobCount; c != 0 {
		t.Fatalf("jobCount: %d", c)
	}
}

func TestRunOnStoppedLoop(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	var failed int32
	done := make(chan struct{})
	go func() {
		for atomic.LoadInt32(&failed) == 0 {
			loop.Start()
			time.Sleep(10 * time.Millisecond)
			loop.Stop()
		}
	}()
	go func() {
		for atomic.LoadInt32(&failed) == 0 {
			loop.RunOnLoop(func(*goja.Runtime) {
				if !loop.running {
					atomic.StoreInt32(&failed, 1)
					close(done)
					return
				}
			})
			time.Sleep(10 * time.Millisecond)
		}
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
	}
	if atomic.LoadInt32(&failed) != 0 {
		t.Fatal("running job on stopped loop")
	}
}

func TestPromise(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	let result;
	const p = new Promise((resolve, reject) => {
		setTimeout(() => {resolve("passed")}, 500);
	});
	p.then(value => {
		result = value;
	});
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})
	if err != nil {
		t.Fatal(err)
	}
	loop.Run(func(vm *goja.Runtime) {
		result := vm.Get("result")
		if !result.SameAs(vm.ToValue("passed")) {
			err = fmt.Errorf("unexpected result: %v", result)
		}
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestPromiseNative(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	let result;
	p.then(value => {
		result = value;
		done();
	});
	`

	loop := NewEventLoop()
	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}
	ch := make(chan error)
	loop.Start()
	defer loop.Stop()

	loop.RunOnLoop(func(vm *goja.Runtime) {
		vm.Set("done", func() {
			ch <- nil
		})
		p, resolve, _ := vm.NewPromise()
		vm.Set("p", p)
		_, err = vm.RunProgram(prg)
		if err != nil {
			ch <- err
			return
		}
		go func() {
			time.Sleep(500 * time.Millisecond)
			loop.RunOnLoop(func(*goja.Runtime) {
				resolve("passed")
			})
		}()
	})
	err = <-ch
	if err != nil {
		t.Fatal(err)
	}
	loop.RunOnLoop(func(vm *goja.Runtime) {
		result := vm.Get("result")
		if !result.SameAs(vm.ToValue("passed")) {
			ch <- fmt.Errorf("unexpected result: %v", result)
		} else {
			ch <- nil
		}
	})
	err = <-ch
	if err != nil {
		t.Fatal(err)
	}
}

func TestEventLoop_StopNoWait(t *testing.T) {
	t.Parallel()
	loop := NewEventLoop()
	var ran int32
	loop.Run(func(runtime *goja.Runtime) {
		loop.SetTimeout(func(*goja.Runtime) {
			atomic.StoreInt32(&ran, 1)
		}, 5*time.Second)

		loop.SetTimeout(func(*goja.Runtime) {
			loop.StopNoWait()
		}, 500*time.Millisecond)
	})

	if atomic.LoadInt32(&ran) != 0 {
		t.Fatal("ran != 0")
	}
}

func TestEventLoop_ClearRunningTimeout(t *testing.T) {
	t.Parallel()
	const SCRIPT = `
	var called = 0;
	let aTimer;
	function a() {
		if (++called > 5) {
			return;
		}
		if (aTimer) {
			clearTimeout(aTimer);
		}
		console.log("ok");
		aTimer = setTimeout(a, 500);
	}
	a();`

	prg, err := goja.Compile("main.js", SCRIPT, false)
	if err != nil {
		t.Fatal(err)
	}

	loop := NewEventLoop()

	loop.Run(func(vm *goja.Runtime) {
		_, err = vm.RunProgram(prg)
	})

	if err != nil {
		t.Fatal(err)
	}

	var called int64
	loop.Run(func(vm *goja.Runtime) {
		called = vm.Get("called").ToInteger()
	})
	if called != 6 {
		t.Fatal(called)
	}
}

func TestEventLoop_Terminate(t *testing.T) {
	defer goleak.VerifyNone(t)

	loop := NewEventLoop()
	loop.Start()
	interval := loop.SetInterval(func(vm *goja.Runtime) {}, 10*time.Millisecond)
	time.Sleep(500 * time.Millisecond)
	loop.ClearInterval(interval)
	loop.Terminate()

	if loop.SetTimeout(func(*goja.Runtime) {}, time.Millisecond) != nil {
		t.Fatal("was able to SetTimeout()")
	}
	if loop.SetInterval(func(*goja.Runtime) {}, time.Millisecond) != nil {
		t.Fatal("was able to SetInterval()")
	}
	if loop.RunOnLoop(func(*goja.Runtime) {}) {
		t.Fatal("was able to RunOnLoop()")
	}

	ch := make(chan struct{})
	loop.Start()
	if !loop.RunOnLoop(func(runtime *goja.Runtime) {
		close(ch)
	}) {
		t.Fatal("RunOnLoop() has failed after restart")
	}
	<-ch
	loop.Terminate()
}
