// 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 (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/davecgh/go-spew/spew"
	"github.com/golang/protobuf/ptypes"
	google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
	"github.com/google/go-github/github"
	"golang.org/x/build/maintner/maintpb"
)

var u1 = &GitHubUser{
	Login: "gopherbot",
	ID:    100,
}
var u2 = &GitHubUser{
	Login: "kevinburke",
	ID:    101,
}

type dummyMutationLogger struct {
	Mutations []*maintpb.Mutation
}

func (d *dummyMutationLogger) Log(m *maintpb.Mutation) error {
	if d.Mutations == nil {
		d.Mutations = []*maintpb.Mutation{}
	}
	d.Mutations = append(d.Mutations, m)
	return nil
}

type mutationTest struct {
	corpus *Corpus
	want   *Corpus
}

func (mt mutationTest) test(t *testing.T, muts ...*maintpb.Mutation) {
	c := mt.corpus
	if c == nil {
		c = new(Corpus)
	}
	for _, m := range muts {
		c.processMutationLocked(m)
	}
	c.github.c = nil
	mt.want.github.c = nil
	if !reflect.DeepEqual(c.github, mt.want.github) {
		t.Errorf("corpus mismatch:\n got: %s\n\nwant: %s\n\ndiff: %v",
			spew.Sdump(c.github),
			spew.Sdump(mt.want.github),
			diffPath(reflect.ValueOf(c.github), reflect.ValueOf(mt.want.github)))
	}
}

var t1, t2 time.Time
var tp1, tp2 *google_protobuf.Timestamp

func init() {
	t1, _ = time.Parse(time.RFC3339, "2016-01-02T15:04:00Z")
	t2, _ = time.Parse(time.RFC3339, "2016-01-02T15:30:00Z")
	tp1, _ = ptypes.TimestampProto(t1)
	tp2, _ = ptypes.TimestampProto(t2)
}

func singleIssueGitHubCorpus() *Corpus {
	c := new(Corpus)
	github := &GitHub{c: c}
	c.github = github
	github.users = map[int64]*GitHubUser{
		u1.ID: u1,
	}
	github.repos = map[GitHubRepoID]*GitHubRepo{
		GitHubRepoID{"golang", "go"}: &GitHubRepo{
			github: github,
			id:     GitHubRepoID{"golang", "go"},
			issues: map[int32]*GitHubIssue{
				3: &GitHubIssue{
					Number:    3,
					User:      u1,
					Title:     "some title",
					Body:      "some body",
					Created:   t1,
					Assignees: nil,
				},
			},
		},
	}
	return c
}

func TestProcessMutation_Github_NewIssue(t *testing.T) {
	mutationTest{want: singleIssueGitHubCorpus()}.test(t, &maintpb.Mutation{
		GithubIssue: &maintpb.GithubIssueMutation{
			Owner:  "golang",
			Repo:   "go",
			Number: 3,
			User: &maintpb.GithubUser{
				Login: "gopherbot",
				Id:    100,
			},
			Title:   "some title",
			Body:    "some body",
			Created: tp1,
		},
	})
}

func TestProcessMutation_Github_RemoveBody(t *testing.T) {
	c := singleIssueGitHubCorpus()
	want := singleIssueGitHubCorpus()
	want.github.repos[GitHubRepoID{"golang", "go"}].issues[3].Body = ""
	mutationTest{corpus: c, want: want}.test(t, &maintpb.Mutation{
		GithubIssue: &maintpb.GithubIssueMutation{
			Owner:      "golang",
			Repo:       "go",
			Number:     3,
			BodyChange: &maintpb.StringChange{Val: ""},
		},
	})

	// And test that the old mutation field (Body) still works.
	want.github.repos[GitHubRepoID{"golang", "go"}].issues[3].Body = "and back"
	mutationTest{corpus: c, want: want}.test(t, &maintpb.Mutation{
		GithubIssue: &maintpb.GithubIssueMutation{
			Owner:  "golang",
			Repo:   "go",
			Number: 3,
			Body:   "and back",
		},
	})
}

func TestProcessMutation_Github(t *testing.T) {
	c := new(Corpus)
	github := &GitHub{c: c}
	c.github = github
	github.repos = map[GitHubRepoID]*GitHubRepo{
		GitHubRepoID{"golang", "go"}: &GitHubRepo{
			github: github,
			id:     GitHubRepoID{"golang", "go"},
			issues: make(map[int32]*GitHubIssue),
		},
	}
	mutationTest{want: c}.test(t, &maintpb.Mutation{
		Github: &maintpb.GithubMutation{
			Owner: "golang",
			Repo:  "go",
		},
	})
}

func TestNewMutationsFromIssue(t *testing.T) {
	gh := &github.Issue{
		Number:    github.Int(5),
		CreatedAt: &t1,
		UpdatedAt: &t2,
		Body:      github.String("body of the issue"),
		State:     github.String("closed"),
	}
	gr := &GitHubRepo{
		id: GitHubRepoID{"golang", "go"},
	}
	is := gr.newMutationFromIssue(nil, gh)
	want := &maintpb.Mutation{GithubIssue: &maintpb.GithubIssueMutation{
		Owner:       "golang",
		Repo:        "go",
		Number:      5,
		BodyChange:  &maintpb.StringChange{Val: "body of the issue"},
		Created:     tp1,
		Updated:     tp2,
		Assignees:   []*maintpb.GithubUser{},
		NoMilestone: true,
		Closed:      &maintpb.BoolChange{Val: true},
	}}
	if !reflect.DeepEqual(is, want) {
		t.Errorf("issue mismatch\n got: %v\nwant: %v\ndiff path: %v", spew.Sdump(is), spew.Sdump(want),
			diffPath(reflect.ValueOf(is), reflect.ValueOf(want)))
	}
}

func TestNewAssigneesHandlesNil(t *testing.T) {
	users := []*github.User{
		&github.User{Login: github.String("foo"), ID: github.Int64(3)},
	}
	got := newAssignees(nil, users)
	want := []*maintpb.GithubUser{&maintpb.GithubUser{
		Id:    3,
		Login: "foo",
	}}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("assignee mismatch\n got: %#v\nwant: %#v", got, want)
	}
}

func TestAssigneesDeleted(t *testing.T) {
	c := new(Corpus)
	assignees := []*GitHubUser{u1, u2}
	issue := &GitHubIssue{
		Number:    3,
		User:      u1,
		Body:      "some body",
		Created:   t2,
		Updated:   t2,
		Assignees: assignees,
	}
	gr := &GitHubRepo{
		id: GitHubRepoID{"golang", "go"},
		issues: map[int32]*GitHubIssue{
			3: issue,
		},
	}
	c.github = &GitHub{
		users: map[int64]*GitHubUser{
			u1.ID: u1,
		},
		repos: map[GitHubRepoID]*GitHubRepo{
			GitHubRepoID{"golang", "go"}: gr,
		},
	}

	mutation := gr.newMutationFromIssue(issue, &github.Issue{
		Number:    github.Int(3),
		Assignees: []*github.User{&github.User{ID: github.Int64(u2.ID)}},
	})
	c.addMutation(mutation)
	gi := gr.issues[3]
	if len(gi.Assignees) != 1 || gi.Assignees[0].ID != u2.ID {
		t.Errorf("expected u1 to be deleted, got %v", gi.Assignees)
	}
}

func TestSync(t *testing.T) {
	c := new(Corpus)
	assignees := []*GitHubUser{u1, u2}
	issue := &GitHubIssue{
		Number:    3,
		User:      u1,
		Body:      "some body",
		Created:   t2,
		Updated:   t2,
		Assignees: assignees,
	}
	gr := &GitHubRepo{
		id: GitHubRepoID{"golang", "go"},
		issues: map[int32]*GitHubIssue{
			3: issue,
		},
	}
	c.github = &GitHub{
		users: map[int64]*GitHubUser{
			u1.ID: u1,
		},
		repos: map[GitHubRepoID]*GitHubRepo{
			GitHubRepoID{"golang", "go"}: gr,
		},
	}

	mutation := gr.newMutationFromIssue(issue, &github.Issue{
		Number:    github.Int(3),
		Assignees: []*github.User{&github.User{ID: github.Int64(u2.ID)}},
	})
	c.addMutation(mutation)
	ctx := context.Background()
	err := c.sync(ctx, false)
	if err != nil {
		t.Fatal("error: ", err)
	}
}

func DeepDiff(got, want interface{}) error {
	return diffPath(reflect.ValueOf(got), reflect.ValueOf(want))
}

func diffPath(got, want reflect.Value) error {
	if !got.IsValid() {
		return errors.New("'got' value invalid")
	}
	if !want.IsValid() {
		return errors.New("'want' value invalid")
	}

	t := got.Type()
	if t != want.Type() {
		return fmt.Errorf("got=%s, want=%s", got.Type(), want.Type())
	}

	switch t.Kind() {
	case reflect.Ptr, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Slice:
		if got.IsNil() != want.IsNil() {
			if got.IsNil() {
				return fmt.Errorf("got = (%s)(nil), want = non-nil", t)
			}
			return fmt.Errorf("got = (%s)(non-nil), want = nil", t)
		}
	}

	switch t.Kind() {
	case reflect.Ptr:
		if got.IsNil() {
			return nil
		}
		return diffPath(got.Elem(), want.Elem())

	case reflect.Struct:
		nf := t.NumField()
		for i := 0; i < nf; i++ {
			sf := t.Field(i)
			if err := diffPath(got.Field(i), want.Field(i)); err != nil {
				inner := err.Error()
				sep := "."
				if strings.HasPrefix(inner, "field ") {
					inner = strings.TrimPrefix(inner, "field ")
				} else {
					sep = ": "
				}
				return fmt.Errorf("field %s%s%v", sf.Name, sep, inner)
			}
		}
		return nil
	case reflect.String:
		if got.String() != want.String() {
			return fmt.Errorf("got = %q; want = %q", got.String(), want.String())
		}
		return nil

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if got.Int() != want.Int() {
			return fmt.Errorf("got = %v; want = %v", got.Int(), want.Int())
		}
		return nil

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		if got.Uint() != want.Uint() {
			return fmt.Errorf("got = %v; want = %v", got.Uint(), want.Uint())
		}
		return nil

	case reflect.Bool:
		if got.Bool() != want.Bool() {
			return fmt.Errorf("got = %v; want = %v", got.Bool(), want.Bool())
		}
		return nil

	case reflect.Slice:
		gl, wl := got.Len(), want.Len()
		if gl != wl {
			return fmt.Errorf("slice len %v; want %v", gl, wl)
		}
		for i := 0; i < gl; i++ {
			if err := diffPath(got.Index(i), want.Index(i)); err != nil {
				return fmt.Errorf("index[%d] differs: %v", i, err)
			}
		}
		return nil

	default:
		return fmt.Errorf("unhandled kind %v", t.Kind())
	}
}
