package metrics

import (
	"bytes"
	"compress/gzip"
	"testing"
	"time"

	"github.com/golang/protobuf/ptypes"
	gh "github.com/google/go-github/github"
	"github.com/jinzhu/now"

	pb "code.justin.tv/dta/rockpaperscissors/proto"
)

func init() {
	now.TimeFormats = append(now.TimeFormats, time.RFC822)
}

func assertEntryBoundaryOrdering(t *testing.T, timeSeries []*pb.GetMetricResponse_TimeSeriesEntry) {
	for _, entry := range timeSeries {
		tsStart, err := ptypes.Timestamp(entry.GetTimerange().GetStart())
		if err != nil {
			t.Fatal("can't convert start timestamp to time: ", err)
		}
		tsEnd, err := ptypes.Timestamp(entry.GetTimerange().GetEnd())
		if err != nil {
			t.Fatal("can't convert end timestamp to time: ", err)
		}
		if !tsEnd.After(tsStart) {
			t.Errorf(
				"end of entry (%v) isn't after start of entry (%v)", tsEnd, tsStart)
		}
	}
}

func TestMakeDailyTimeSeries(t *testing.T) {
	start := now.MustParse("09 Apr 17 15:17 PDT")
	end := now.MustParse("10 Apr 17 15:17 PDT")

	timeSeries, err := makeDailyTimeSeries(start, end)
	if err != nil {
		t.Fatal("makeDailyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeDailyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := now.New(start).BeginningOfDay()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested days: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(start).EndOfDay()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of first day: ", tsEnd, expectedEnd)
	}

	tsStart, err = ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart = now.New(end).BeginningOfDay()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of last day: ", tsStart, expectedStart)
	}

	tsEnd, err = ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd = now.New(end).EndOfDay()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested days: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

func TestMakeWeeklyTimeSeries(t *testing.T) {
	start := now.MustParse("03 Apr 17 15:17 PDT")
	end := now.MustParse("10 Apr 17 15:17 PDT")

	timeSeries, err := makeWeeklyTimeSeries(start, end)
	if err != nil {
		t.Fatal("makeWeeklyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeWeeklyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := now.New(start).BeginningOfWeek()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested weeks: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(end).EndOfWeek()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested weeks: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

func TestMakeMonthlyTimeSeries(t *testing.T) {
	ianaTimeZone := "America/Los_Angeles"
	loc, err := time.LoadLocation(ianaTimeZone)
	if err != nil {
		t.Fatal(err)
	}
	start := time.Unix(1490532449, 0).In(loc) // 3/26/2017, 5:47:29 AM GMT-7:00 DST
	end := time.Unix(1493160449, 0).In(loc)   // 4/25/2017, 3:47:29 PM GMT-7:00 DST

	timeSeries, err := makeMonthlyTimeSeries(start, end)
	if err != nil {
		t.Fatal("makeMonthlyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeMonthlyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := beginningOfMonth(start)
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested months: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(end).EndOfMonth()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested months: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

func TestMakeQuarterlyTimeSeries(t *testing.T) {
	start := now.MustParse("12 Dec 16 15:17 PDT")
	end := now.MustParse("12 Jan 17 15:17 PDT")

	timeSeries, err := makeQuarterlyTimeSeries(start, end)
	if err != nil {
		t.Fatal("makeQuarterlyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeQuarterlyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := now.New(start).BeginningOfQuarter()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested quarters: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(end).EndOfQuarter()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested quarters: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

func TestMakeYearlyTimeSeries(t *testing.T) {
	start := now.MustParse("12 Dec 16 15:17 PDT")
	end := now.MustParse("12 Jan 17 15:17 PDT")

	timeSeries, err := makeYearlyTimeSeries(start, end)
	if err != nil {
		t.Fatal("makeYearlyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeYearlyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := now.New(start).BeginningOfYear()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested years: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(end).EndOfYear()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested years: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

func TestMakeTimeSeries(t *testing.T) {
	ianaTimeZone := "America/Los_Angeles"
	loc, err := time.LoadLocation(ianaTimeZone)
	if err != nil {
		t.Fatal(err)
	}

	start := now.MustParse("09 Apr 17 15:17 PDT").In(loc)
	startProto, err := ptypes.TimestampProto(start)
	if err != nil {
		t.Fatal(err)
	}
	end := now.MustParse("10 Apr 17 15:17 PDT").In(loc)
	endProto, err := ptypes.TimestampProto(end)
	if err != nil {
		t.Fatal(err)
	}
	timerange := &pb.TimeRange{
		Start: startProto,
		End:   endProto,
	}

	timeSeries, err := makeTimeSeries(timerange, pb.GetMetricRequest_DAY, ianaTimeZone)
	if err != nil {
		t.Fatal("makeDailyTimeSeries returned non-nil error: ", err)
	}
	if len(timeSeries) != 2 {
		t.Error("makeDailyTimeSeries returned wrong number of entries")
	}

	tsStart, err := ptypes.Timestamp(timeSeries[0].GetTimerange().GetStart())
	if err != nil {
		t.Fatal("can't convert start timestamp to time: ", err)
	}
	expectedStart := now.New(start).BeginningOfDay()
	if !tsStart.Equal(expectedStart) {
		t.Error("start isn't beginning of requested days: ", tsStart, expectedStart)
	}

	tsEnd, err := ptypes.Timestamp(timeSeries[len(timeSeries)-1].GetTimerange().GetEnd())
	if err != nil {
		t.Fatal("can't convert end timestamp to time: ", err)
	}
	expectedEnd := now.New(end).EndOfDay()
	if !tsEnd.Equal(expectedEnd) {
		t.Error("end isn't end of requested days: ", tsEnd, expectedEnd)
	}

	assertEntryBoundaryOrdering(t, timeSeries)
}

const gitHubPingJSON = `{"zen":"Speak like a human.","hook_id":2557,"hook":{"type":"Repository","id":2557,"name":"web","active":true,"events":["push"],"config":{"content_type":"json","insecure_ssl":"0","url":"https://qkijmvrm7d.execute-api.us-west-2.amazonaws.com/development/webhook"},"updated_at":"2016-10-11T23:04:13Z","created_at":"2016-10-11T23:04:13Z","url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/hooks/2557","test_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/hooks/2557/test","ping_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/hooks/2557/pings","last_response":{"code":null,"status":"unused","message":null}},"repository":{"id":3042,"name":"rockpaperscissors","full_name":"dta/rockpaperscissors","owner":{"login":"dta","id":181,"avatar_url":"https://git-aws.internal.justin.tv/avatars/u/181?","gravatar_id":"","url":"https://git-aws.internal.justin.tv/api/v3/users/dta","html_url":"https://git-aws.internal.justin.tv/dta","followers_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/followers","following_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/following{/other_user}","gists_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/gists{/gist_id}","starred_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/starred{/owner}{/repo}","subscriptions_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/subscriptions","organizations_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/orgs","repos_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/repos","events_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/events{/privacy}","received_events_url":"https://git-aws.internal.justin.tv/api/v3/users/dta/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://git-aws.internal.justin.tv/dta/rockpaperscissors","description":"A project health metrics and dashboarding system","fork":false,"url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors","forks_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/forks","keys_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/keys{/key_id}","collaborators_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/collaborators{/collaborator}","teams_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/teams","hooks_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/hooks","issue_events_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/issues/events{/number}","events_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/events","assignees_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/assignees{/user}","branches_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/branches{/branch}","tags_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/tags","blobs_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/git/blobs{/sha}","git_tags_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/git/tags{/sha}","git_refs_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/git/refs{/sha}","trees_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/git/trees{/sha}","statuses_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/statuses/{sha}","languages_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/languages","stargazers_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/stargazers","contributors_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/contributors","subscribers_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/subscribers","subscription_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/subscription","commits_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/commits{/sha}","git_commits_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/git/commits{/sha}","comments_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/comments{/number}","issue_comment_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/issues/comments{/number}","contents_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/contents/{+path}","compare_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/compare/{base}...{head}","merges_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/merges","archive_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/{archive_format}{/ref}","downloads_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/downloads","issues_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/issues{/number}","pulls_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/pulls{/number}","milestones_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/milestones{/number}","notifications_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/notifications{?since,all,participating}","labels_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/labels{/name}","releases_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/releases{/id}","deployments_url":"https://git-aws.internal.justin.tv/api/v3/repos/dta/rockpaperscissors/deployments","created_at":"2016-08-02T22:02:22Z","updated_at":"2016-10-10T22:47:24Z","pushed_at":"2016-10-11T20:53:04Z","git_url":"git://git-aws.internal.justin.tv/dta/rockpaperscissors.git","ssh_url":"git@git-aws.internal.justin.tv:dta/rockpaperscissors.git","clone_url":"https://git-aws.internal.justin.tv/dta/rockpaperscissors.git","svn_url":"https://git-aws.internal.justin.tv/dta/rockpaperscissors","homepage":"http://link.twitch.tv/rps","size":9144,"stargazers_count":2,"watchers_count":2,"language":"Go","has_issues":false,"has_downloads":false,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":2,"default_branch":"master"},"sender":{"login":"fullht","id":489,"avatar_url":"https://git-aws.internal.justin.tv/avatars/u/489?","gravatar_id":"","url":"https://git-aws.internal.justin.tv/api/v3/users/fullht","html_url":"https://git-aws.internal.justin.tv/fullht","followers_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/followers","following_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/following{/other_user}","gists_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/gists{/gist_id}","starred_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/starred{/owner}{/repo}","subscriptions_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/subscriptions","organizations_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/orgs","repos_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/repos","events_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/events{/privacy}","received_events_url":"https://git-aws.internal.justin.tv/api/v3/users/fullht/received_events","type":"User","site_admin":true,"ldap_dn":"cn=T.R. Fullhart,ou=Users,dc=justin,dc=tv"}}`

func TestGetGitHubEvent(t *testing.T) {
	event := &pb.Event{Body: []byte(gitHubPingJSON)}
	gitHubEvent, err := getGitHubEvent(event, "ping")
	if err != nil {
		t.Error(err)
		t.FailNow()
	}
	if _, ok := gitHubEvent.(*gh.PingEvent); !ok {
		t.Error("Parsed GitHub event isn't PingEvent")
		t.FailNow()
	}

	var b bytes.Buffer
	w := gzip.NewWriter(&b)
	_, err = w.Write([]byte(gitHubPingJSON))
	if err != nil {
		t.Error(err)
		t.FailNow()
	}
	err = w.Close()
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	event = &pb.Event{Body: b.Bytes()}
	gitHubEvent, err = getGitHubEvent(event, "ping")
	if err != nil {
		t.Error(err)
		t.FailNow()
	}
	if _, ok := gitHubEvent.(*gh.PingEvent); !ok {
		t.Error("Parsed GitHub event isn't PingEvent")
		t.FailNow()
	}
}
