// Copyright 2019 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 main

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/syzkaller/dashboard/dashapi"
	"github.com/google/syzkaller/pkg/email"
)

func TestEmailNotifUpstreamEmbargo(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build := testBuild(1)
	c.client2.UploadBuild(build)

	crash := testCrash(build, 1)
	c.client2.ReportCrash(crash)
	report := c.pollEmailBug()
	c.expectEQ(report.To, []string{"test@syzkaller.com"})

	// Upstreaming happens after 14 days, so no emails yet.
	c.advanceTime(13 * 24 * time.Hour)
	c.expectNoEmail()

	// Now we should get notification about upstreaming and upstream report:
	c.advanceTime(2 * 24 * time.Hour)
	notifUpstream := c.pollEmailBug()
	upstreamReport := c.pollEmailBug()
	c.expectEQ(notifUpstream.Subject, crash.Title)
	c.expectEQ(notifUpstream.Sender, report.Sender)
	c.expectEQ(notifUpstream.Body, "Sending this report to the next reporting stage.")
	c.expectEQ(upstreamReport.Subject, "[syzbot] "+crash.Title)
	c.expectNE(upstreamReport.Sender, report.Sender)
	c.expectEQ(upstreamReport.To, []string{"bugs@syzkaller.com", "default@maintainers.com"})
}

func TestEmailNotifUpstreamSkip(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build := testBuild(1)
	c.client2.UploadBuild(build)

	crash := testCrash(build, 1)
	crash.Title = "skip with repro 1"
	c.client2.ReportCrash(crash)
	report := c.pollEmailBug()
	c.expectEQ(report.To, []string{"test@syzkaller.com"})

	// No emails yet.
	c.expectNoEmail()

	// Now upload repro and it should be auto-upstreamed.
	crash.ReproOpts = []byte("repro opts")
	crash.ReproSyz = []byte("getpid()")
	c.client2.ReportCrash(crash)
	notifUpstream := c.pollEmailBug()
	upstreamReport := c.pollEmailBug()
	c.expectEQ(notifUpstream.Sender, report.Sender)
	c.expectEQ(notifUpstream.Body, "Sending this report to the next reporting stage.")
	c.expectNE(upstreamReport.Sender, report.Sender)
	c.expectEQ(upstreamReport.To, []string{"bugs@syzkaller.com", "default@maintainers.com"})
}

func TestEmailNotifBadFix(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	client := c.publicClient

	build := testBuild(1)
	client.UploadBuild(build)

	// Fake more active managers.
	for i := 1; i < 5; i++ {
		client.UploadBuild(testBuild(i + 1))
	}

	crash := testCrash(build, 1)
	client.ReportCrash(crash)
	report := c.pollEmailBug()
	c.expectEQ(report.To, []string{"test@syzkaller.com"})
	_, extBugID, err := email.RemoveAddrContext(report.Sender)
	c.expectOK(err)

	c.incomingEmail(report.Sender, "#syz fix some: commit title")
	c.expectNoEmail()

	// Notification about bad fixing commit should be send after 90 days.
	c.advanceTime(50 * 24 * time.Hour)
	c.expectNoEmail()
	c.advanceTime(35 * 24 * time.Hour)
	c.expectNoEmail()
	c.advanceTime(10 * 24 * time.Hour)
	notif := c.pollEmailBug()
	t.Logf("%s", notif.Body)

	expectReply := fmt.Sprintf(`This bug is marked as fixed by commit:
some: commit title

But I can't find it in the tested trees[1] for more than 90 days.
Is it a correct commit? Please update it by replying:

#syz fix: exact-commit-title

Until then the bug is still considered open and new crashes with
the same signature are ignored.

Kernel: access-public-email
Dashboard link: https://testapp.appspot.com/bug?extid=%s

---
[1] I expect the commit to be present in:

1. branch1 branch of
repo1

2. branch2 branch of
repo2

3. branch3 branch of
repo3

4. branch4 branch of
repo4

The full list of 5 trees can be found at
https://testapp.appspot.com/access-public-email/repos
`, extBugID)

	if diff := cmp.Diff(expectReply, notif.Body); diff != "" {
		t.Errorf("wrong notification text: %s", diff)
		fmt.Printf("received notification:\n%s\n", notif.Body)
	}
	// No notifications for another 14 days, then another one.
	c.advanceTime(13 * 24 * time.Hour)
	c.expectNoEmail()
	c.advanceTime(2 * 24 * time.Hour)
	notif = c.pollEmailBug()
	if !strings.Contains(notif.Body, "This bug is marked as fixed by commit:\nsome: commit title\n") {
		t.Fatalf("bad notification text: %q", notif.Body)
	}
}

func TestBugObsoleting(t *testing.T) {
	// To simplify test we specify all dates in days from a fixed point in time.
	const day = 24 * time.Hour
	days := func(n int) time.Time {
		t := time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC)
		return t.Add(time.Duration(n+1) * day)
	}
	tests := []struct {
		bug    *Bug
		period time.Duration
	}{
		// Final bug with just 1 crash: max final period.
		{
			bug: &Bug{
				FirstTime:  days(0),
				LastTime:   days(0),
				NumCrashes: 1,
				Reporting:  []BugReporting{{Reported: days(0)}},
			},
			period: 100 * day,
		},
		// Non-final bug with just 1 crash: max non-final period.
		{
			bug: &Bug{
				FirstTime:  days(0),
				LastTime:   days(0),
				NumCrashes: 1,
				Reporting:  []BugReporting{{Reported: days(0)}, {}},
			},
			period: 60 * day,
		},
		// Special manger: max period that that manager.
		{
			bug: &Bug{
				FirstTime:  days(0),
				LastTime:   days(0),
				NumCrashes: 1,
				HappenedOn: []string{"special-obsoleting"},
				Reporting:  []BugReporting{{Reported: days(0)}, {}},
			},
			period: 20 * day,
		},
		// Special manger and a non-special: normal rules.
		{
			bug: &Bug{
				FirstTime:  days(0),
				LastTime:   days(0),
				NumCrashes: 1,
				HappenedOn: []string{"special-obsoleting", "non-special-manager"},
				Reporting:  []BugReporting{{Reported: days(0)}},
			},
			period: 100 * day,
		},
		// Happened a lot: min period.
		{
			bug: &Bug{
				FirstTime:  days(0),
				LastTime:   days(1),
				NumCrashes: 1000,
				Reporting:  []BugReporting{{Reported: days(0)}},
			},
			period: 80 * day,
		},
	}
	for i, test := range tests {
		test.bug.Namespace = "test1"
		got := test.bug.obsoletePeriod()
		if got != test.period {
			t.Errorf("test #%v: got: %.2f, want %.2f",
				i, float64(got/time.Hour)/24, float64(test.period/time.Hour)/24)
		}
	}
}

func TestEmailNotifObsoleted(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build := testBuild(1)
	c.client2.UploadBuild(build)

	crash := testCrash(build, 1)
	crash.Maintainers = []string{"maintainer@syzkaller.com"}
	c.client2.ReportCrash(crash)
	report := c.pollEmailBug()
	// Need to upstream so that it's not auto-upstreamed before obsoleted.
	c.incomingEmail(report.Sender, "#syz upstream")
	report = c.pollEmailBug()
	// Add more people to bug CC.
	c.incomingEmail(report.Sender, "wow", EmailOptCC([]string{"somebody@else.com"}))

	// Bug is open, new crashes don't create new bug.
	c.client2.ReportCrash(crash)
	c.expectNoEmail()

	// Not yet.
	c.advanceTime(59 * 24 * time.Hour)
	c.expectNoEmail()

	// Now!
	c.advanceTime(2 * 24 * time.Hour)
	notif := c.pollEmailBug()
	if !strings.Contains(notif.Body, "Auto-closing this bug as obsolete") {
		t.Fatalf("bad notification text: %q", notif.Body)
	}
	c.expectEQ(notif.To, []string{"bugs@syzkaller.com", "default@sender.com", "somebody@else.com"})

	// New crash must create new bug.
	c.client2.ReportCrash(crash)
	report = c.pollEmailBug()
	c.expectEQ(report.Subject, "title1 (2)")
	// Now the same, but for the last reporting (must have smaller CC list).
	c.incomingEmail(report.Sender, "#syz upstream")
	report = c.pollEmailBug()
	c.incomingEmail(report.Sender, "#syz upstream")
	report = c.pollEmailBug()
	_ = report

	c.advanceTime(101 * 24 * time.Hour)
	notif = c.pollEmailBug()
	if !strings.Contains(notif.Body, "Auto-closing this bug as obsolete") {
		t.Fatalf("bad notification text: %q", notif.Body)
	}
	c.expectEQ(notif.Subject, crash.Title+" (2)")
	c.expectEQ(notif.To, []string{"bugs2@syzkaller.com"})
}

func TestEmailNotifNotObsoleted(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build := testBuild(1)
	c.client2.UploadBuild(build)

	// Crashes with repro are not auto-obsoleted.
	crash1 := testCrash(build, 1)
	crash1.ReproSyz = []byte("repro")
	c.client2.ReportCrash(crash1)
	report1 := c.pollEmailBug()
	c.incomingEmail(report1.Sender, "#syz upstream")
	report1 = c.pollEmailBug()
	_ = report1

	// This crash will get another crash later.
	crash2 := testCrash(build, 2)
	c.client2.ReportCrash(crash2)
	report2 := c.pollEmailBug()
	c.incomingEmail(report2.Sender, "#syz upstream")
	report2 = c.pollEmailBug()
	_ = report2

	// This crash will get some activity later.
	crash3 := testCrash(build, 3)
	c.client2.ReportCrash(crash3)
	report3 := c.pollEmailBug()
	c.incomingEmail(report3.Sender, "#syz upstream")
	report3 = c.pollEmailBug()

	// This will be obsoleted (just to check that we have timings right).
	c.advanceTime(24 * time.Hour)
	crash4 := testCrash(build, 4)
	c.client2.ReportCrash(crash4)
	report4 := c.pollEmailBug()
	c.incomingEmail(report4.Sender, "#syz upstream")
	report4 = c.pollEmailBug()

	c.advanceTime(59 * 24 * time.Hour)
	c.expectNoEmail()

	c.client2.ReportCrash(crash2)
	c.incomingEmail(report3.Sender, "I am looking at it")

	c.advanceTime(5 * 24 * time.Hour)
	// Only crash 4 is obsoleted.
	notif := c.pollEmailBug()
	c.expectEQ(notif.Sender, report4.Sender)
	c.expectNoEmail()

	// Crash 3 also obsoleted after some time.
	c.advanceTime(20 * 24 * time.Hour)
	notif = c.pollEmailBug()
	c.expectEQ(notif.Sender, report3.Sender)
}

func TestEmailNotifObsoletedManager(t *testing.T) {
	// Crashes with repro are auto-obsoleted if happen on a particular manager only.
	c := NewCtx(t)
	defer c.Close()

	build := testBuild(1)
	build.Manager = noFixBisectionManager
	c.client2.UploadBuild(build)
	crash := testCrashWithRepro(build, 1)
	c.client2.ReportCrash(crash)
	report := c.pollEmailBug()
	c.incomingEmail(report.Sender, "#syz upstream")
	report = c.pollEmailBug()
	_ = report
	c.advanceTime(200 * 24 * time.Hour)
	notif := c.pollEmailBug()
	c.expectTrue(strings.Contains(notif.Body, "Auto-closing this bug as obsolete"))
}

func TestExtNotifUpstreamEmbargo(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build1 := testBuild(1)
	c.client.UploadBuild(build1)

	crash1 := testCrash(build1, 1)
	c.client.ReportCrash(crash1)
	rep := c.client.pollBug()

	// Specify fixing commit for the bug.
	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
		ID:     rep.ID,
		Status: dashapi.BugStatusOpen,
	})
	c.expectEQ(reply.OK, true)
	c.client.pollNotifs(0)
	c.advanceTime(20 * 24 * time.Hour)
	notif := c.client.pollNotifs(1)[0]
	c.expectEQ(notif.ID, rep.ID)
	c.expectEQ(notif.Type, dashapi.BugNotifUpstream)
}

func TestExtNotifUpstreamOnHold(t *testing.T) {
	c := NewCtx(t)
	defer c.Close()

	build1 := testBuild(1)
	c.client.UploadBuild(build1)

	crash1 := testCrash(build1, 1)
	c.client.ReportCrash(crash1)
	rep := c.client.pollBug()

	// Specify fixing commit for the bug.
	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
		ID:     rep.ID,
		Status: dashapi.BugStatusOpen,
		OnHold: true,
	})
	c.expectEQ(reply.OK, true)
	c.advanceTime(20 * 24 * time.Hour)
	c.client.pollNotifs(0)
}
