// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package maintner

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/golang/protobuf/ptypes"
	"github.com/golang/protobuf/ptypes/timestamp"
	"github.com/google/go-github/github"

	"golang.org/x/build/maintner/maintpb"
)

func TestParseGithubEvents(t *testing.T) {
	tests := []struct {
		name string                    // test
		j    string                    // JSON from Github API
		e    *GitHubIssueEvent         // in-memory
		p    *maintpb.GithubIssueEvent // on disk
	}{
		{
			name: "labeled",
			j: `{
    "id": 998144526,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144526",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "labeled",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:28Z",
    "label": {
      "name": "enhancement",
      "color": "84b6eb"
    }
  }
`,
			e: &GitHubIssueEvent{
				ID:      998144526,
				Type:    "labeled",
				Created: t3339("2017-03-13T22:39:28Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Label: "enhancement",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        998144526,
				EventType: "labeled",
				ActorId:   2621,
				Created:   p3339("2017-03-13T22:39:28Z"),
				Label:     &maintpb.GithubLabel{Name: "enhancement"},
			},
		},

		{
			name: "unlabeled",
			j: `{
    "id": 998144526,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144526",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "unlabeled",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:28Z",
    "label": {
      "name": "enhancement",
      "color": "84b6eb"
    }
  }
`,
			e: &GitHubIssueEvent{
				ID:      998144526,
				Type:    "unlabeled",
				Created: t3339("2017-03-13T22:39:28Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Label: "enhancement",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        998144526,
				EventType: "unlabeled",
				ActorId:   2621,
				Created:   p3339("2017-03-13T22:39:28Z"),
				Label:     &maintpb.GithubLabel{Name: "enhancement"},
			},
		},

		{
			name: "milestoned",
			j: `{
    "id": 998144529,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144529",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "milestoned",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:28Z",
    "milestone": {
      "title": "World Domination"
    }}`,
			e: &GitHubIssueEvent{
				ID:      998144529,
				Type:    "milestoned",
				Created: t3339("2017-03-13T22:39:28Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Milestone: "World Domination",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        998144529,
				EventType: "milestoned",
				ActorId:   2621,
				Created:   p3339("2017-03-13T22:39:28Z"),
				Milestone: &maintpb.GithubMilestone{Title: "World Domination"},
			},
		},

		{
			name: "demilestoned",
			j: `{
    "id": 998144529,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144529",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "demilestoned",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:28Z",
    "milestone": {
      "title": "World Domination"
    }}`,
			e: &GitHubIssueEvent{
				ID:      998144529,
				Type:    "demilestoned",
				Created: t3339("2017-03-13T22:39:28Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Milestone: "World Domination",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        998144529,
				EventType: "demilestoned",
				ActorId:   2621,
				Created:   p3339("2017-03-13T22:39:28Z"),
				Milestone: &maintpb.GithubMilestone{Title: "World Domination"},
			},
		},

		{
			name: "assigned",
			j: `{
    "id": 998144530,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144530",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "assigned",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:28Z",
    "assignee": {
      "login": "bradfitz",
      "id": 2621
    },
    "assigner": {
      "login": "bradfitz",
      "id": 2621
    }}`,
			e: &GitHubIssueEvent{
				ID:      998144530,
				Type:    "assigned",
				Created: t3339("2017-03-13T22:39:28Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Assignee: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Assigner: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
			},
			p: &maintpb.GithubIssueEvent{
				Id:         998144530,
				EventType:  "assigned",
				ActorId:    2621,
				Created:    p3339("2017-03-13T22:39:28Z"),
				AssigneeId: 2621,
				AssignerId: 2621,
			},
		},

		{
			name: "unassigned",
			j: `{
    "id": 1000077586,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1000077586",
    "actor": {
      "login": "dmitshur",
      "id": 1924134
    },
    "event": "unassigned",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-15T00:31:42Z",
    "assignee": {
      "login": "dmitshur",
      "id": 1924134
    },
    "assigner": {
      "login": "bradfitz",
      "id": 2621
    }
  }`,
			e: &GitHubIssueEvent{
				ID:      1000077586,
				Type:    "unassigned",
				Created: t3339("2017-03-15T00:31:42Z"),
				Actor: &GitHubUser{
					ID:    1924134,
					Login: "dmitshur",
				},
				Assignee: &GitHubUser{
					ID:    1924134,
					Login: "dmitshur",
				},
				Assigner: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
			},
			p: &maintpb.GithubIssueEvent{
				Id:         1000077586,
				EventType:  "unassigned",
				ActorId:    1924134,
				Created:    p3339("2017-03-15T00:31:42Z"),
				AssigneeId: 1924134,
				AssignerId: 2621,
			},
		},

		{
			name: "locked",
			j: `{
    "id": 998144646,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/998144646",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "locked",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-13T22:39:36Z"
  }`,
			e: &GitHubIssueEvent{
				ID:      998144646,
				Type:    "locked",
				Created: t3339("2017-03-13T22:39:36Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
			},
			p: &maintpb.GithubIssueEvent{
				Id:        998144646,
				EventType: "locked",
				ActorId:   2621,
				Created:   p3339("2017-03-13T22:39:36Z"),
			},
		},

		{
			name: "unlocked",
			j: `{
    "id": 1000014895,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1000014895",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "unlocked",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-14T23:26:21Z"
 }`,
			e: &GitHubIssueEvent{
				ID:      1000014895,
				Type:    "unlocked",
				Created: t3339("2017-03-14T23:26:21Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
			},
			p: &maintpb.GithubIssueEvent{
				Id:        1000014895,
				EventType: "unlocked",
				ActorId:   2621,
				Created:   p3339("2017-03-14T23:26:21Z"),
			},
		},

		{
			name: "closed",
			j: `  {
    "id": 1006040931,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1006040931",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "closed",
    "commit_id": "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
    "commit_url": "https://api.github.com/repos/bradfitz/go-issue-mirror/commits/e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
    "created_at": "2017-03-19T23:40:33Z"
  }`,
			e: &GitHubIssueEvent{
				ID:      1006040931,
				Type:    "closed",
				Created: t3339("2017-03-19T23:40:33Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				CommitID:  "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				CommitURL: "https://api.github.com/repos/bradfitz/go-issue-mirror/commits/e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        1006040931,
				EventType: "closed",
				ActorId:   2621,
				Created:   p3339("2017-03-19T23:40:33Z"),
				Commit: &maintpb.GithubCommit{
					Owner:    "bradfitz",
					Repo:     "go-issue-mirror",
					CommitId: "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				},
			},
		},

		{
			name: "reopened",
			j: `{
    "id": 1000014895,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1000014895",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "reopened",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-14T23:26:21Z"
 }`,
			e: &GitHubIssueEvent{
				ID:      1000014895,
				Type:    "reopened",
				Created: t3339("2017-03-14T23:26:21Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
			},
			p: &maintpb.GithubIssueEvent{
				Id:        1000014895,
				EventType: "reopened",
				ActorId:   2621,
				Created:   p3339("2017-03-14T23:26:21Z"),
			},
		},

		{
			name: "referenced",
			j: `{
    "id": 1006040930,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1006040930",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "referenced",
    "commit_id": "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
    "commit_url": "https://api.github.com/repos/bradfitz/go-issue-mirror/commits/e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
    "created_at": "2017-03-19T23:40:32Z"
  }`,
			e: &GitHubIssueEvent{
				ID:      1006040930,
				Type:    "referenced",
				Created: t3339("2017-03-19T23:40:32Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				CommitID:  "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				CommitURL: "https://api.github.com/repos/bradfitz/go-issue-mirror/commits/e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
			},
			p: &maintpb.GithubIssueEvent{
				Id:        1006040930,
				EventType: "referenced",
				ActorId:   2621,
				Created:   p3339("2017-03-19T23:40:32Z"),
				Commit: &maintpb.GithubCommit{
					Owner:    "bradfitz",
					Repo:     "go-issue-mirror",
					CommitId: "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				},
			},
		},

		{
			name: "renamed",
			j: `{
    "id": 1006107803,
    "url": "https://api.github.com/repos/bradfitz/go-issue-mirror/issues/events/1006107803",
    "actor": {
      "login": "bradfitz",
      "id": 2621
    },
    "event": "renamed",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2017-03-20T02:53:43Z",
    "rename": {
      "from": "test-2",
      "to": "test-2 new name"
    }
  }`,
			e: &GitHubIssueEvent{
				ID:      1006107803,
				Type:    "renamed",
				Created: t3339("2017-03-20T02:53:43Z"),
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				From: "test-2",
				To:   "test-2 new name",
			},
			p: &maintpb.GithubIssueEvent{
				Id:         1006107803,
				EventType:  "renamed",
				ActorId:    2621,
				Created:    p3339("2017-03-20T02:53:43Z"),
				RenameFrom: "test-2",
				RenameTo:   "test-2 new name",
			},
		},
	}

	var eventTypes []string

	for _, tt := range tests {
		evts, err := parseGithubEvents(strings.NewReader("[" + tt.j + "]"))
		if err != nil {
			t.Errorf("%s: parse JSON: %v", tt.name, err)
			continue
		}
		if len(evts) != 1 {
			t.Errorf("%s: parse JSON = %v entries; want 1", tt.name, len(evts))
			continue
		}
		gote := evts[0]
		if !reflect.DeepEqual(gote, tt.e) {
			t.Errorf("%s: JSON -> githubEvent differs: %v", tt.name, DeepDiff(gote, tt.e))
			continue
		}
		eventTypes = append(eventTypes, gote.Type)

		gotp := gote.Proto()
		if !reflect.DeepEqual(gotp, tt.p) {
			t.Errorf("%s: githubEvent -> proto differs: %v", tt.name, DeepDiff(gotp, tt.p))
			continue
		}

		var c Corpus
		c.initGithub()
		c.github.getOrCreateUserID(2621).Login = "bradfitz"
		c.github.getOrCreateUserID(1924134).Login = "dmitshur"
		gr := c.github.getOrCreateRepo("foowner", "bar")
		e2 := gr.newGithubEvent(gotp)

		if !reflect.DeepEqual(e2, tt.e) {
			t.Errorf("%s: proto -> githubEvent differs: %v", tt.name, DeepDiff(e2, tt.e))
			continue
		}
	}

	t.Logf("Tested event types: %q", eventTypes)
}

func TestParseMultipleGithubEvents(t *testing.T) {
	content, err := ioutil.ReadFile(filepath.Join("testdata", "TestParseMultipleGithubEvents.json"))
	if err != nil {
		t.Errorf("error while loading testdata: %s\n", err.Error())
	}
	evts, err := parseGithubEvents(strings.NewReader(string(content)))
	if err != nil {
		t.Errorf("error was not expected: %s\n", err.Error())
	}
	if len(evts) != 7 {
		t.Errorf("there should have been three events. was: %d\n", len(evts))
	}
	lastEvent := evts[len(evts)-1]
	if lastEvent.Type != "closed" {
		t.Errorf("the last event's type should have been closed. was: %s\n", lastEvent.Type)
	}
}

func TestParseMultipleGithubEventsWithForeach(t *testing.T) {
	issue := &GitHubIssue{
		PullRequest: true,
		events: map[int64]*GitHubIssueEvent{
			0: &GitHubIssueEvent{
				Type: "labelled",
			},
			1: &GitHubIssueEvent{
				Type: "milestone",
			},
			2: &GitHubIssueEvent{
				Type: "closed",
			},
		},
	}
	eventTypes := []string{"closed", "labelled", "milestone"}
	gatheredTypes := make([]string, 0)
	issue.ForeachEvent(func(e *GitHubIssueEvent) error {
		gatheredTypes = append(gatheredTypes, e.Type)
		return nil
	})
	sort.Strings(gatheredTypes)
	if !reflect.DeepEqual(eventTypes, gatheredTypes) {
		t.Fatalf("want event types: %v; got: %v\n", eventTypes, gatheredTypes)
	}
}

type ClientMock struct {
	err        error
	status     string
	statusCode int
	testdata   string
}

var timesDoWasCalled = 0

func (c *ClientMock) Do(req *http.Request) (*http.Response, error) {
	if len(c.testdata) < 1 {
		c.testdata = "TestParseMultipleGithubEvents.json"
	}
	timesDoWasCalled++
	content, _ := ioutil.ReadFile(filepath.Join("testdata", c.testdata))
	headers := make(http.Header, 0)
	t := time.Now()
	var b []byte
	headers["Date"] = []string{string(t.AppendFormat(b, "Mon Jan _2 15:04:05 2006"))}
	return &http.Response{
		Body:       ioutil.NopCloser(bytes.NewReader(content)),
		Status:     c.status,
		StatusCode: c.statusCode,
		Header:     headers,
	}, c.err
}

type MockLogger struct {
}

var eventLog = make([]string, 0)

func (m *MockLogger) Log(mut *maintpb.Mutation) error {
	for _, e := range mut.GithubIssue.Event {
		eventLog = append(eventLog, e.EventType)
	}
	return nil
}

func TestSyncEvents(t *testing.T) {
	var c Corpus
	c.initGithub()
	c.mutationLogger = &MockLogger{}
	gr := c.github.getOrCreateRepo("foowner", "bar")
	issue := &GitHubIssue{
		ID:          1001,
		PullRequest: true,
		events:      map[int64]*GitHubIssueEvent{},
	}
	gr.issues = map[int32]*GitHubIssue{
		1001: issue,
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		rw.Write([]byte("OK"))
	}))
	defer server.Close()
	p := &githubRepoPoller{
		c:             &c,
		token:         "foobar",
		gr:            gr,
		githubDirect:  github.NewClient(server.Client()),
		githubCaching: github.NewClient(server.Client()),
	}
	t.Run("successful sync", func(t2 *testing.T) {
		defer func() { eventLog = make([]string, 0) }()
		timesDoWasCalled = 0
		ctx := context.Background()
		p.client = &ClientMock{
			status:     "OK",
			statusCode: http.StatusOK,
			err:        nil,
			testdata:   "TestParseMultipleGithubEvents.json",
		}
		err := p.syncEventsOnIssue(ctx, int32(issue.ID))
		if err != nil {
			t2.Error(err)
		}
		want := []string{"labeled", "labeled", "labeled", "labeled", "labeled", "milestoned", "closed"}
		if !reflect.DeepEqual(want, eventLog) {
			t2.Errorf("want: %v; got: %v\n", want, eventLog)
		}

		wantTimesCalled := 1
		if timesDoWasCalled != wantTimesCalled {
			t.Errorf("client.Do should have been called %d times. got: %d\n", wantTimesCalled, timesDoWasCalled)
		}
	})
	t.Run("successful sync missing milestones", func(t2 *testing.T) {
		defer func() { eventLog = make([]string, 0) }()
		timesDoWasCalled = 0
		ctx := context.Background()
		p.client = &ClientMock{
			status:     "OK",
			statusCode: http.StatusOK,
			err:        nil,
			testdata:   "TestMissingMilestoneEvents.json",
		}
		err := p.syncEventsOnIssue(ctx, int32(issue.ID))
		if err != nil {
			t2.Error(err)
		}
		want := []string{"mentioned", "subscribed", "mentioned", "subscribed", "assigned", "labeled", "labeled", "milestoned", "renamed", "demilestoned", "milestoned"}
		sort.Strings(want)
		sort.Strings(eventLog)
		if !reflect.DeepEqual(want, eventLog) {
			t2.Errorf("want: %v; got: %v\n", want, eventLog)
		}

		wantTimesCalled := 1
		if timesDoWasCalled != wantTimesCalled {
			t.Errorf("client.Do should have been called %d times. got: %d\n", wantTimesCalled, timesDoWasCalled)
		}
	})
}

func TestSyncMultipleConsecutiveEvents(t *testing.T) {
	var c Corpus
	c.initGithub()
	c.mutationLogger = &MockLogger{}
	gr := c.github.getOrCreateRepo("foowner", "bar")
	issue := &GitHubIssue{
		ID:          1001,
		PullRequest: true,
		events:      map[int64]*GitHubIssueEvent{},
	}
	gr.issues = map[int32]*GitHubIssue{
		1001: issue,
	}
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		rw.Write([]byte("OK"))
	}))
	defer server.Close()
	p := &githubRepoPoller{
		c:             &c,
		token:         "foobar",
		gr:            gr,
		githubDirect:  github.NewClient(server.Client()),
		githubCaching: github.NewClient(server.Client()),
	}
	t.Run("successful sync", func(t2 *testing.T) {
		defer func() { eventLog = make([]string, 0) }()
		timesDoWasCalled = 0
		ctx := context.Background()
		for i := 1; i < 5; i++ {
			testdata := fmt.Sprintf("TestParseMultipleGithubEvents_p%d.json", i)
			p.client = &ClientMock{
				status:     "OK",
				statusCode: http.StatusOK,
				err:        nil,
				testdata:   testdata,
			}
			err := p.syncEventsOnIssue(ctx, int32(issue.ID))
			if err != nil {
				t2.Fatal(err)
			}
		}

		want := []string{"labeled", "labeled", "labeled", "labeled", "labeled", "milestoned", "closed"}
		if !reflect.DeepEqual(want, eventLog) {
			t2.Errorf("want: %v; got: %v\n", want, eventLog)
		}

		wantTimesCalled := 4
		if timesDoWasCalled != wantTimesCalled {
			t.Errorf("client.Do should have been called %d times. got: %d\n", wantTimesCalled, timesDoWasCalled)
		}
	})
}

func TestParseGitHubReviews(t *testing.T) {
	tests := []struct {
		name string                // test
		j    string                // JSON from Github API
		e    *GitHubReview         // in-memory
		p    *maintpb.GithubReview // on disk
	}{
		{
			name: "Approved",
			j: `{
				"id": 123456,
				"node_id": "548913adsafas84asdf48a",
				"user": {
					"login": "bradfitz",
					"id": 2621
				},
				"body": "I approve this commit",
				"state": "APPROVED",
				"html_url": "https://github.com/bradfitz/go-issue-mirror/pull/21",
				"pull_request_url": "https://github.com/bradfitz/go-issue-mirror/pull/21",
				"author_association": "CONTRIBUTOR",
				"_links":{
					"html":{
						"href": "https://github.com/bradfitz/go-issue-mirror/pull/21"
					},
					"pull_request":{
						"href": "https://github.com/bradfitz/go-issue-mirror/pull/21"
					}
				},
				"submitted_at": "2018-03-22T00:26:48Z",
				"commit_id" : "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126"
				}`,
			e: &GitHubReview{
				ID: 123456,
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Body:             "I approve this commit",
				State:            "APPROVED",
				CommitID:         "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				ActorAssociation: "CONTRIBUTOR",
				Created:          t3339("2018-03-22T00:26:48Z"),
			},
			p: &maintpb.GithubReview{
				Id:               123456,
				ActorId:          2621,
				Body:             "I approve this commit",
				State:            "APPROVED",
				CommitId:         "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				ActorAssociation: "CONTRIBUTOR",
				Created:          p3339("2018-03-22T00:26:48Z"),
			},
		},
		{
			name: "Extra Unknown JSON",
			j: `{
				"id": 123456,
				"node_id": "548913adsafas84asdf48a",
				"user": {
					"login": "bradfitz",
					"id": 2621
				},
				"body": "I approve this commit",
				"state": "APPROVED",
				"html_url": "https://github.com/bradfitz/go-issue-mirror/pull/21",
				"pull_request_url": "https://github.com/bradfitz/go-issue-mirror/pull/21",
				"author_association": "CONTRIBUTOR",
				"_links":{
					"html":{
						"href": "https://github.com/bradfitz/go-issue-mirror/pull/21"
					},
					"pull_request":{
						"href": "https://github.com/bradfitz/go-issue-mirror/pull/21"
					}
				},
				"submitted_at": "2018-03-22T00:26:48Z",
				"commit_id" : "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				"random_key": "some random value"
				}`,
			e: &GitHubReview{
				ID: 123456,
				Actor: &GitHubUser{
					ID:    2621,
					Login: "bradfitz",
				},
				Body:             "I approve this commit",
				State:            "APPROVED",
				CommitID:         "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				ActorAssociation: "CONTRIBUTOR",
				Created:          t3339("2018-03-22T00:26:48Z"),
				OtherJSON:        `{"random_key":"some random value"}`,
			},
			p: &maintpb.GithubReview{
				Id:               123456,
				ActorId:          2621,
				Body:             "I approve this commit",
				State:            "APPROVED",
				CommitId:         "e4d70f7e8892f024e4ed3e8b99ee6c5a9f16e126",
				ActorAssociation: "CONTRIBUTOR",
				Created:          p3339("2018-03-22T00:26:48Z"),
				OtherJson:        []byte(`{"random_key":"some random value"}`),
			},
		},
	}

	for _, tt := range tests {
		evts, err := parseGithubReviews(strings.NewReader("[" + tt.j + "]"))
		if err != nil {
			t.Errorf("%s: parse JSON: %v", tt.name, err)
			continue
		}
		if len(evts) != 1 {
			t.Errorf("%s: parse JSON = %v entries; want 1", tt.name, len(evts))
			continue
		}
		gote := evts[0]
		if !reflect.DeepEqual(gote, tt.e) {
			t.Errorf("%s: JSON -> githubReviewEvent differs: %v", tt.name, DeepDiff(gote, tt.e))
			continue
		}

		gotp := gote.Proto()
		if !reflect.DeepEqual(gotp, tt.p) {
			t.Errorf("%s: githubReviewEvent -> proto differs: %v", tt.name, DeepDiff(gotp, tt.p))
			continue
		}

		var c Corpus
		c.initGithub()
		c.github.getOrCreateUserID(2621).Login = "bradfitz"
		c.github.getOrCreateUserID(1924134).Login = "dmitshur"
		gr := c.github.getOrCreateRepo("foowner", "bar")
		e2 := gr.newGithubReview(gotp)

		if !reflect.DeepEqual(e2, tt.e) {
			t.Errorf("%s: proto -> githubReviewEvent differs: %v", tt.name, DeepDiff(e2, tt.e))
			continue
		}
	}
}

func TestForeachRepo(t *testing.T) {
	tests := []struct {
		name    string
		issue   *GitHubIssue
		want    []string
		wantErr error
	}{
		{
			name: "Skips non-PullRequests",
			issue: &GitHubIssue{
				PullRequest: false,
			},
			want:    []string{},
			wantErr: nil,
		},
		{
			name: "Processes Multiple in Order",
			issue: &GitHubIssue{
				PullRequest: true,
				reviews: map[int64]*GitHubReview{
					0: &GitHubReview{
						Body:    "Second",
						Created: t3339("2018-04-22T00:26:48Z"),
					},
					1: &GitHubReview{
						Body:    "First",
						Created: t3339("2018-03-22T00:26:48Z"),
					},
				},
			},
			want:    []string{"First", "Second"},
			wantErr: nil,
		},
		{
			name: "Will Error",
			issue: &GitHubIssue{
				PullRequest: true,
				reviews: map[int64]*GitHubReview{
					0: &GitHubReview{
						Body: "Fail",
					},
				},
			},
			want:    []string{},
			wantErr: fmt.Errorf("Planned Failure"),
		},
		{
			name: "Will Error Late",
			issue: &GitHubIssue{
				PullRequest: true,
				reviews: map[int64]*GitHubReview{
					0: &GitHubReview{
						Body:    "First Event",
						Created: t3339("2018-03-22T00:26:48Z"),
					},
					1: &GitHubReview{
						Body:    "Fail",
						Created: t3339("2018-04-22T00:26:48Z"),
					},
					2: &GitHubReview{
						Body:    "Third Event",
						Created: t3339("2018-05-22T00:26:48Z"),
					},
				},
			},
			want:    []string{"First Event"},
			wantErr: fmt.Errorf("Planned Failure"),
		}}

	for _, tt := range tests {
		got := make([]string, 0)

		err := tt.issue.ForeachReview(func(r *GitHubReview) error {
			if r.Body == "Fail" {
				return fmt.Errorf("Planned Failure")
			}
			got = append(got, r.Body)
			return nil
		})

		if !equalError(tt.wantErr, err) {
			t.Errorf("%s: ForeachReview errs differ. got: %s, want: %s", tt.name, err, tt.wantErr)
		}

		if !reflect.DeepEqual(got, tt.want) {
			t.Errorf("%s: ForeachReview calls differ. got: %s want: %s", tt.name, got, tt.want)
		}
	}

	t.Log("Tested Reviews")
}

// equalError reports whether errors a and b are considered equal.
// They're equal if both are nil, or both are not nil and a.Error() == b.Error().
func equalError(a, b error) bool {
	return a == nil && b == nil || a != nil && b != nil && a.Error() == b.Error()
}

func TestCacheableURL(t *testing.T) {
	tests := []struct {
		v    string
		want bool
	}{
		{"https://api.github.com/repos/OWNER/RePO/milestones?page=1", true},
		{"https://api.github.com/repos/OWNER/RePO/milestones?page=2", false},
		{"https://api.github.com/repos/OWNER/RePO/milestones?", false},
		{"https://api.github.com/repos/OWNER/RePO/milestones", false},

		{"https://api.github.com/repos/OWNER/RePO/labels?page=1", true},
		{"https://api.github.com/repos/OWNER/RePO/labels?page=2", false},
		{"https://api.github.com/repos/OWNER/RePO/labels?", false},
		{"https://api.github.com/repos/OWNER/RePO/labels", false},

		{"https://api.github.com/repos/OWNER/RePO/foos?page=1", false},

		{"https://api.github.com/repos/OWNER/RePO/issues?page=1", false},
		{"https://api.github.com/repos/OWNER/RePO/issues?page=1&sort=updated&direction=desc", true},
	}

	for _, tt := range tests {
		got := cacheableURL(tt.v)
		if got != tt.want {
			t.Errorf("cacheableURL(%q) = %v; want %v", tt.v, got, tt.want)
		}
	}
}

func t3339(s string) time.Time {
	t, err := time.Parse(time.RFC3339, s)
	if err != nil {
		panic(err)
	}
	return t.UTC()
}

func p3339(s string) *timestamp.Timestamp {
	tp, err := ptypes.TimestampProto(t3339(s))
	if err != nil {
		panic(err)
	}
	return tp
}

func TestParseGithubRefs(t *testing.T) {
	tests := []struct {
		gerritProj string // "go.googlesource.com/go", etc
		msg        string
		want       []string
	}{
		{"go.googlesource.com/go", "\nFixes #1234\n", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Fixes #1234\n", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Fixes #1234", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Fixes golang/go#1234", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Fixes golang/go#1234\n", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Fixes golang/go#1234.", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Mention issue #1234 a second time.\n\nFixes #1234.", []string{"golang/go#1234"}},
		{"go.googlesource.com/go", "Mention issue #1234 a second time.\n\nFixes #1234.\nUpdates #1235.", []string{"golang/go#1234", "golang/go#1235"}},
		{"go.googlesource.com/net", "Fixes golang/go#1234.", []string{"golang/go#1234"}},
		{"go.googlesource.com/net", "Fixes #1234", nil},
	}
	for _, tt := range tests {
		c := new(Corpus)
		var got []string
		for _, ref := range c.parseGithubRefs(tt.gerritProj, tt.msg) {
			got = append(got, ref.String())
		}
		if !reflect.DeepEqual(got, tt.want) {
			t.Errorf("parseGithubRefs(%q, %q) = %q; want %q", tt.gerritProj, tt.msg, got, tt.want)
		}
	}
}
