// Copyright 2022 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"
	"sort"
	"testing"
	"time"

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

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

	build := testBuild(1)
	build.Manager = "test_manager"
	// Embed one of the assets right away.
	build.Assets = []dashapi.NewAsset{
		{
			Type:        dashapi.KernelObject,
			DownloadURL: "http://google.com/vmlinux",
		},
	}
	c.client2.UploadBuild(build)

	// Add one more build, so that the assets of the previous one could be deprecated.
	c.advanceTime(time.Minute)
	build2 := testBuild(2)
	build2.Manager = "test_manager"
	c.client2.UploadBuild(build2)

	// "Upload" several more assets.
	c.expectOK(c.client2.AddBuildAssets(&dashapi.AddBuildAssetsReq{
		BuildID: build.ID,
		Assets: []dashapi.NewAsset{
			{
				Type:        dashapi.BootableDisk,
				DownloadURL: "http://google.com/bootable_disk",
			},
		},
	}))
	c.expectOK(c.client2.AddBuildAssets(&dashapi.AddBuildAssetsReq{
		BuildID: build.ID,
		Assets: []dashapi.NewAsset{
			{
				Type:        dashapi.HTMLCoverageReport,
				DownloadURL: "http://google.com/coverage.html",
			},
		},
	}))

	crash := testCrash(build, 1)
	crash.Maintainers = []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`, `idont@want.EMAILS`}
	c.client2.ReportCrash(crash)

	// Test that the reporting email is correct.
	msg := c.pollEmailBug()
	sender, extBugID, err := email.RemoveAddrContext(msg.Sender)
	c.expectOK(err)
	_, dbCrash, dbBuild := c.loadBug(extBugID)
	crashLogLink := externalLink(c.ctx, textCrashLog, dbCrash.Log)
	kernelConfigLink := externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig)
	c.expectEQ(sender, fromAddr(c.ctx))
	to := config.Namespaces["test2"].Reporting[0].Config.(*EmailConfig).Email
	c.expectEQ(msg.To, []string{to})
	c.expectEQ(msg.Subject, crash.Title)
	c.expectEQ(len(msg.Attachments), 0)
	c.expectEQ(msg.Body, fmt.Sprintf(`Hello,

syzbot found the following issue on:

HEAD commit:    111111111111 kernel_commit_title1
git tree:       repo1 branch1
console output: %[2]v
kernel config:  %[3]v
dashboard link: https://testapp.appspot.com/bug?extid=%[1]v
compiler:       compiler1
CC:             [bar@foo.com foo@bar.com idont@want.EMAILS]

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
disk image: http://google.com/bootable_disk
vmlinux: http://google.com/vmlinux

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+%[1]v@testapp.appspotmail.com

report1

---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the bug is already fixed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to change bug's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the bug is a duplicate of another bug, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup`,
		extBugID, crashLogLink, kernelConfigLink))
	c.checkURLContents(crashLogLink, crash.Log)
	c.checkURLContents(kernelConfigLink, build.KernelConfig)

	// We query the needed assets. We need all 3.
	needed, err := c.client2.NeededAssetsList()
	c.expectOK(err)
	sort.Strings(needed.DownloadURLs)
	allDownloadURLs := []string{
		"http://google.com/bootable_disk",
		"http://google.com/coverage.html",
		"http://google.com/vmlinux",
	}
	c.expectEQ(needed.DownloadURLs, allDownloadURLs)

	// Invalidate the bug.
	c.client.updateBug(extBugID, dashapi.BugStatusInvalid, "")
	_, err = c.GET("/cron/deprecate_assets")
	c.expectOK(err)

	// Query the needed assets once more, so far there should be no change.
	needed, err = c.client2.NeededAssetsList()
	c.expectOK(err)
	sort.Strings(needed.DownloadURLs)
	c.expectEQ(needed.DownloadURLs, allDownloadURLs)

	// Skip one month and deprecate assets.
	c.advanceTime(time.Hour * 24 * 31)
	_, err = c.GET("/cron/deprecate_assets")
	c.expectOK(err)

	// Only the html asset should have persisted.
	needed, err = c.client2.NeededAssetsList()
	c.expectOK(err)
	c.expectEQ(needed.DownloadURLs, []string{"http://google.com/coverage.html"})
}

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

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

	// Upload the second build to just make sure coverage reports are assigned per-manager.
	c.client.UploadBuild(testBuild(2))

	// We expect no coverage reports to be present.
	uiManagers, err := loadManagers(c.ctx, AccessAdmin, "test1", nil)
	c.expectOK(err)
	c.expectEQ(len(uiManagers), 2)
	c.expectEQ(uiManagers[0].CoverLink, "")
	c.expectEQ(uiManagers[1].CoverLink, "")

	// Upload an asset.
	origHTMLAsset := "http://google.com/coverage0.html"
	c.expectOK(c.client.AddBuildAssets(&dashapi.AddBuildAssetsReq{
		BuildID: build.ID,
		Assets: []dashapi.NewAsset{
			{
				Type:        dashapi.HTMLCoverageReport,
				DownloadURL: origHTMLAsset,
			},
		},
	}))
	uiManagers, err = loadManagers(c.ctx, AccessAdmin, "test1", nil)
	c.expectOK(err)
	c.expectEQ(len(uiManagers), 2)
	c.expectEQ(uiManagers[0].CoverLink, origHTMLAsset)
	c.expectEQ(uiManagers[1].CoverLink, "")

	// Upload a newer coverage.
	newHTMLAsset := "http://google.com/coverage1.html"
	c.expectOK(c.client.AddBuildAssets(&dashapi.AddBuildAssetsReq{
		BuildID: build.ID,
		Assets: []dashapi.NewAsset{
			{
				Type:        dashapi.HTMLCoverageReport,
				DownloadURL: newHTMLAsset,
			},
		},
	}))
	uiManagers, err = loadManagers(c.ctx, AccessAdmin, "test1", nil)
	c.expectOK(err)
	c.expectEQ(len(uiManagers), 2)
	c.expectEQ(uiManagers[0].CoverLink, newHTMLAsset)
	c.expectEQ(uiManagers[1].CoverLink, "")
}

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

	ensureNeeded := func(needed []string) {
		_, err := c.GET("/cron/deprecate_assets")
		c.expectOK(err)
		neededResp, err := c.client.NeededAssetsList()
		c.expectOK(err)
		sort.Strings(neededResp.DownloadURLs)
		sort.Strings(needed)
		c.expectEQ(neededResp.DownloadURLs, needed)
	}

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

	uploadReport := func(url string) {
		c.expectOK(c.client.AddBuildAssets(&dashapi.AddBuildAssetsReq{
			BuildID: build.ID,
			Assets: []dashapi.NewAsset{
				{
					Type:        dashapi.HTMLCoverageReport,
					DownloadURL: url,
				},
			},
		}))
	}

	// Week 1. Saturday Jan 1st, 2000.
	weekOneFirst := "http://google.com/coverage1_1.html"
	uploadReport(weekOneFirst)

	// Week 1. Sunday Jan 2nd, 2000.
	weekOneSecond := "http://google.com/coverage1_2.html"
	c.advanceTime(time.Hour * 24)
	uploadReport(weekOneSecond)
	ensureNeeded([]string{weekOneFirst, weekOneSecond})

	// Week 2. Tuesday Jan 4nd, 2000.
	weekTwoFirst := "http://google.com/coverage2_1.html"
	c.advanceTime(time.Hour * 24 * 2)
	uploadReport(weekTwoFirst)
	ensureNeeded([]string{weekOneFirst, weekOneSecond, weekTwoFirst})

	// Week 2. Thu Jan 6nd, 2000.
	weekTwoSecond := "http://google.com/coverage2_2.html"
	c.advanceTime(time.Hour * 24 * 2)
	uploadReport(weekTwoSecond)
	ensureNeeded([]string{weekOneFirst, weekOneSecond, weekTwoFirst, weekTwoSecond})

	// Week 3. Monday Jan 10th, 2000.
	weekThreeFirst := "http://google.com/coverage3_1.html"
	c.advanceTime(time.Hour * 24 * 4)
	uploadReport(weekThreeFirst)
	ensureNeeded([]string{weekOneFirst, weekOneSecond, weekTwoFirst, weekTwoSecond, weekThreeFirst})

	// Week 4. Monday Jan 17th, 2000.
	weekFourFirst := "http://google.com/coverage4_1.html"
	c.advanceTime(time.Hour * 24 * 7)
	uploadReport(weekFourFirst)

	t.Logf("embargo is over, time is %s", timeNow(c.ctx))
	// Note that now that the two week deletion embargo has passed, the first asset
	// begins to falls out.
	ensureNeeded([]string{weekOneSecond, weekTwoFirst, weekTwoSecond, weekThreeFirst, weekFourFirst})

	// Week 5. Monday Jan 24th, 2000.
	c.advanceTime(time.Hour * 24 * 7)
	ensureNeeded([]string{weekOneSecond, weekTwoSecond, weekThreeFirst, weekFourFirst})

	// A year later.
	c.advanceTime(time.Hour * 24 * 365)
	ensureNeeded([]string{weekOneSecond, weekTwoSecond, weekThreeFirst, weekFourFirst})
}

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

	ensureNeeded := func(needed []string) {
		_, err := c.GET("/cron/deprecate_assets")
		c.expectOK(err)
		neededResp, err := c.client.NeededAssetsList()
		c.expectOK(err)
		sort.Strings(neededResp.DownloadURLs)
		sort.Strings(needed)
		c.expectEQ(neededResp.DownloadURLs, needed)
	}

	build := testBuild(1)
	build.Manager = "manager"
	build.Assets = []dashapi.NewAsset{
		{
			Type:        dashapi.KernelObject,
			DownloadURL: "http://google.com/vmlinux",
		},
	}
	c.client.UploadBuild(build)

	// No crashes yet, but it's the latest build, so the assets must be preserved.
	ensureNeeded([]string{"http://google.com/vmlinux"})

	// Upload one more build for the same manager.
	c.advanceTime(time.Minute)
	build2 := testBuild(2)
	build2.Manager = "manager"
	build2.Assets = []dashapi.NewAsset{
		{
			Type:        dashapi.KernelObject,
			DownloadURL: "http://google.com/vmlinux2",
		},
	}
	c.client.UploadBuild(build2)

	// The assets of the previous build are reasonably new, so they must be kept.
	ensureNeeded([]string{"http://google.com/vmlinux", "http://google.com/vmlinux2"})

	// The assets of the first build must be deprecated now.
	c.advanceTime(time.Hour * 24 * 14)
	ensureNeeded([]string{"http://google.com/vmlinux2"})

	// But even if a lot of time passes, but there are no new builds, the assets must stay.
	c.advanceTime(time.Hour * 24 * 365)
	ensureNeeded([]string{"http://google.com/vmlinux2"})
}

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

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

	crash := testCrash(build, 1)
	crash.Maintainers = []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`, `idont@want.EMAILS`}
	crash.Assets = []dashapi.NewAsset{
		{
			Type:        dashapi.MountInRepro,
			DownloadURL: "http://google.com/disk_image",
		},
		{
			Type:        dashapi.MountInRepro,
			DownloadURL: "http://google.com/disk_image2",
		},
	}
	c.client2.ReportCrash(crash)

	// Test that the reported email is correct.
	msg := c.pollEmailBug()
	sender, extBugID, err := email.RemoveAddrContext(msg.Sender)
	c.expectOK(err)
	_, dbCrash, dbBuild := c.loadBug(extBugID)
	crashLogLink := externalLink(c.ctx, textCrashLog, dbCrash.Log)
	kernelConfigLink := externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig)
	c.expectEQ(sender, fromAddr(c.ctx))
	to := config.Namespaces["test2"].Reporting[0].Config.(*EmailConfig).Email
	c.expectEQ(msg.To, []string{to})
	c.expectEQ(msg.Subject, crash.Title)
	c.expectEQ(len(msg.Attachments), 0)
	c.expectEQ(msg.Body, fmt.Sprintf(`Hello,

syzbot found the following issue on:

HEAD commit:    111111111111 kernel_commit_title1
git tree:       repo1 branch1
console output: %[2]v
kernel config:  %[3]v
dashboard link: https://testapp.appspot.com/bug?extid=%[1]v
compiler:       compiler1
CC:             [bar@foo.com foo@bar.com idont@want.EMAILS]

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
mounted in repro #1: http://google.com/disk_image
mounted in repro #2: http://google.com/disk_image2

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+%[1]v@testapp.appspotmail.com

report1

---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the bug is already fixed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to change bug's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the bug is a duplicate of another bug, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup`,
		extBugID, crashLogLink, kernelConfigLink))
	c.checkURLContents(crashLogLink, crash.Log)
	c.checkURLContents(kernelConfigLink, build.KernelConfig)

	// We query the needed assets. We need all 2.
	needed, err := c.client2.NeededAssetsList()
	c.expectOK(err)
	sort.Strings(needed.DownloadURLs)
	allDownloadURLs := []string{
		"http://google.com/disk_image",
		"http://google.com/disk_image2",
	}
	c.expectEQ(needed.DownloadURLs, allDownloadURLs)

	// Invalidate the bug.
	c.client.updateBug(extBugID, dashapi.BugStatusInvalid, "")
	_, err = c.GET("/cron/deprecate_assets")
	c.expectOK(err)

	// Query the needed assets once more, so far there should be no change.
	needed, err = c.client2.NeededAssetsList()
	c.expectOK(err)
	sort.Strings(needed.DownloadURLs)
	c.expectEQ(needed.DownloadURLs, allDownloadURLs)

	// Skip one month and deprecate assets.
	c.advanceTime(time.Hour * 24 * 31)
	_, err = c.GET("/cron/deprecate_assets")
	c.expectOK(err)

	// Nothing should have been persisted.
	needed, err = c.client2.NeededAssetsList()
	c.expectOK(err)
	c.expectEQ(needed.DownloadURLs, []string{})
}
