package cron

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

// Many tests schedule a job for every second, and then wait at most a second
// for it to run.  This amount is just slightly larger than 1 second to
// compensate for a few milliseconds of runtime.
const ONE_SECOND = 1*time.Second + 10*time.Millisecond

// Start and stop cron with no entries.
func TestNoEntries(t *testing.T) {
	cron := New()
	cron.Start()

	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-stop(cron):
	}
}

// Start, stop, then add an entry. Verify entry doesn't run.
func TestStopCausesJobsToNotRun(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	cron := New()
	cron.Start()
	cron.Stop()
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })

	select {
	case <-time.After(ONE_SECOND):
		// No job ran!
	case <-wait(wg):
		t.FailNow()
	}
}

// Add a job, start cron, expect it runs.
func TestAddBeforeRunning(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	cron := New()
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
	cron.Start()
	defer cron.Stop()

	// Give cron 2 seconds to run our job (which is always activated).
	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

// Start cron, add a job, expect it runs.
func TestAddWhileRunning(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	cron := New()
	cron.Start()
	defer cron.Stop()
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })

	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

// Test timing with Entries.
func TestSnapshotEntries(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	cron := New()
	cron.AddFunc("", "@every 2s", func() { wg.Done() })
	cron.Start()
	defer cron.Stop()

	// Cron should fire in 2 seconds. After 1 second, call Entries.
	select {
	case <-time.After(ONE_SECOND):
		cron.Entries()
	}

	// Even though Entries was called, the cron should fire at the 2 second mark.
	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}

}

// Test that the entries are correctly sorted.
// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure
// that the immediate entry runs immediately.
// Also: Test that multiple jobs run in the same instant.
func TestMultipleEntries(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(2)

	cron := New()
	cron.AddFunc("", "0 0 0 1 1 ?", func() {})
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
	cron.AddFunc("", "0 0 0 31 12 ?", func() {})
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })

	cron.Start()
	defer cron.Stop()

	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

// Test running the same job twice.
func TestRunningJobTwice(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(2)

	cron := New()
	cron.AddFunc("", "0 0 0 1 1 ?", func() {})
	cron.AddFunc("", "0 0 0 31 12 ?", func() {})
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })

	cron.Start()
	defer cron.Stop()

	select {
	case <-time.After(2 * ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

func TestRunningMultipleSchedules(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(2)

	cron := New()
	cron.AddFunc("", "0 0 0 1 1 ?", func() {})
	cron.AddFunc("", "0 0 0 31 12 ?", func() {})
	cron.AddFunc("", "* * * * * ?", func() { wg.Done() })
	cron.Schedule("", "", Every(time.Minute), FuncJob(func() {}))
	cron.Schedule("", "", Every(time.Second), FuncJob(func() { wg.Done() }))
	cron.Schedule("", "", Every(time.Hour), FuncJob(func() {}))

	cron.Start()
	defer cron.Stop()

	select {
	case <-time.After(2 * ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

// Test that the cron is run in the local time zone (as opposed to UTC).
func TestLocalTimezone(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	now := time.Now().Local()
	spec := fmt.Sprintf("%d %d %d %d %d ?",
		now.Second()+1, now.Minute(), now.Hour(), now.Day(), now.Month())

	cron := New()
	cron.AddFunc("", spec, func() { wg.Done() })
	cron.Start()
	defer cron.Stop()

	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}
}

type testJob struct {
	wg   *sync.WaitGroup
	name string
}

func (t testJob) Run() {
	t.wg.Done()
}

// Simple test using Runnables.
func TestJob(t *testing.T) {
	wg := &sync.WaitGroup{}
	wg.Add(1)

	cron := New()
	cron.AddJob("", "0 0 0 30 Feb ?", testJob{wg, "job0"})
	cron.AddJob("", "0 0 0 1 1 ?", testJob{wg, "job1"})
	cron.AddJob("", "* * * * * ?", testJob{wg, "job2"})
	cron.AddJob("", "1 0 0 1 1 ?", testJob{wg, "job3"})
	cron.Schedule("", "", Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"})
	cron.Schedule("", "", Every(5*time.Minute), testJob{wg, "job5"})

	cron.Start()
	defer cron.Stop()

	select {
	case <-time.After(ONE_SECOND):
		t.FailNow()
	case <-wait(wg):
	}

	// Ensure the entries are in the right order.
	expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"}

	var actuals []string
	for _, entry := range cron.Entries() {
		actuals = append(actuals, entry.Job.(testJob).name)
	}

	for i, expected := range expecteds {
		if actuals[i] != expected {
			t.Errorf("Jobs not in the right order.  (expected) %s != %s (actual)", expecteds, actuals)
			t.FailNow()
		}
	}
}

func wait(wg *sync.WaitGroup) chan bool {
	ch := make(chan bool)
	go func() {
		wg.Wait()
		ch <- true
	}()
	return ch
}

func stop(cron *Cron) chan bool {
	ch := make(chan bool)
	go func() {
		cron.Stop()
		ch <- true
	}()
	return ch
}
<html>
<head><title>PANIC: session(release): write data/sessions/0/6/06f4790ed9609760: no space left on device</title>
<meta charset="utf-8" />
<style type="text/css">
html, body {
	font-family: "Roboto", sans-serif;
	color: #333333;
	background-color: #ea5343;
	margin: 0px;
}
h1 {
	color: #d04526;
	background-color: #ffffff;
	padding: 20px;
	border-bottom: 1px dashed #2b3848;
}
pre {
	margin: 20px;
	padding: 20px;
	border: 2px solid #2b3848;
	background-color: #ffffff;
	white-space: pre-wrap;       /* css-3 */
	white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
	white-space: -pre-wrap;      /* Opera 4-6 */
	white-space: -o-pre-wrap;    /* Opera 7 */
	word-wrap: break-word;       /* Internet Explorer 5.5+ */
}
</style>
</head><body>
<h1>PANIC</h1>
<pre style="font-weight: bold;">session(release): write data/sessions/0/6/06f4790ed9609760: no space left on device</pre>
<pre>github.com/go-macaron/session@v0.0.0-20190805070824-1a3cdc6f5659/session.go:199 (0x8b2934)
gopkg.in/macaron.v1@v1.3.9/context.go:79 (0x83d0a0)
github.com/go-macaron/inject@v0.0.0-20160627170012-d8a0b8677191/inject.go:157 (0x80ab07)
github.com/go-macaron/inject@v0.0.0-20160627170012-d8a0b8677191/inject.go:135 (0x80a8a8)
gopkg.in/macaron.v1@v1.3.9/context.go:121 (0x83d1f8)
gopkg.in/macaron.v1@v1.3.9/context.go:112 (0x84fdb5)
gopkg.in/macaron.v1@v1.3.9/recovery.go:161 (0x84fda8)
gopkg.in/macaron.v1@v1.3.9/logger.go:40 (0x840c73)
github.com/go-macaron/inject@v0.0.0-20160627170012-d8a0b8677191/inject.go:157 (0x80ab07)
github.com/go-macaron/inject@v0.0.0-20160627170012-d8a0b8677191/inject.go:135 (0x80a8a8)
gopkg.in/macaron.v1@v1.3.9/context.go:121 (0x83d1f8)
gopkg.in/macaron.v1@v1.3.9/router.go:187 (0x850fc6)
gopkg.in/macaron.v1@v1.3.9/router.go:303 (0x8493e5)
gopkg.in/macaron.v1@v1.3.9/macaron.go:220 (0x841fca)
net/http/server.go:2836 (0x7a79b2)
net/http/server.go:1924 (0x7a341b)
runtime/asm_amd64.s:1373 (0x46f9f0)
</pre>
</body>
</html>