// Copyright 2017 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package firestore

import (
	"context"
	"testing"

	tspb "github.com/golang/protobuf/ptypes/timestamp"
	pb "google.golang.org/genproto/googleapis/firestore/v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var testClient = &Client{
	projectID:  "projectID",
	databaseID: "(default)",
}

func TestClientCollectionAndDoc(t *testing.T) {
	coll1 := testClient.Collection("X")
	db := "projects/projectID/databases/(default)"
	wantc1 := &CollectionRef{
		c:          testClient,
		parentPath: db + "/documents",
		selfPath:   "X",
		Parent:     nil,
		ID:         "X",
		Path:       "projects/projectID/databases/(default)/documents/X",
		Query: Query{
			c:            testClient,
			collectionID: "X",
			path:         "projects/projectID/databases/(default)/documents/X",
			parentPath:   db + "/documents",
		},
	}
	if !testEqual(coll1, wantc1) {
		t.Fatalf("got\n%+v\nwant\n%+v", coll1, wantc1)
	}
	doc1 := testClient.Doc("X/a")
	wantd1 := &DocumentRef{
		Parent:    coll1,
		ID:        "a",
		Path:      "projects/projectID/databases/(default)/documents/X/a",
		shortPath: "X/a",
	}

	if !testEqual(doc1, wantd1) {
		t.Fatalf("got %+v, want %+v", doc1, wantd1)
	}
	coll2 := testClient.Collection("X/a/Y")
	parentPath := "projects/projectID/databases/(default)/documents/X/a"
	wantc2 := &CollectionRef{
		c:          testClient,
		parentPath: parentPath,
		selfPath:   "X/a/Y",
		Parent:     doc1,
		ID:         "Y",
		Path:       "projects/projectID/databases/(default)/documents/X/a/Y",
		Query: Query{
			c:            testClient,
			collectionID: "Y",
			parentPath:   parentPath,
			path:         "projects/projectID/databases/(default)/documents/X/a/Y",
		},
	}
	if !testEqual(coll2, wantc2) {
		t.Fatalf("\ngot  %+v\nwant %+v", coll2, wantc2)
	}
	doc2 := testClient.Doc("X/a/Y/b")
	wantd2 := &DocumentRef{
		Parent:    coll2,
		ID:        "b",
		Path:      "projects/projectID/databases/(default)/documents/X/a/Y/b",
		shortPath: "X/a/Y/b",
	}
	if !testEqual(doc2, wantd2) {
		t.Fatalf("got %+v, want %+v", doc2, wantd2)
	}
}

func TestClientCollDocErrors(t *testing.T) {
	for _, badColl := range []string{"", "/", "/a/", "/a/b", "a/b/", "a//b"} {
		coll := testClient.Collection(badColl)
		if coll != nil {
			t.Errorf("coll path %q: got %+v, want nil", badColl, coll)
		}
	}
	for _, badDoc := range []string{"", "a", "/", "/a", "a/", "a/b/c", "a//b/c"} {
		doc := testClient.Doc(badDoc)
		if doc != nil {
			t.Errorf("doc path %q: got %+v, want nil", badDoc, doc)
		}
	}
}

func TestGetAll(t *testing.T) {
	c, srv, cleanup := newMock(t)
	defer cleanup()

	const dbPath = "projects/projectID/databases/(default)"
	req := &pb.BatchGetDocumentsRequest{
		Database: dbPath,
		Documents: []string{
			dbPath + "/documents/C/a",
			dbPath + "/documents/C/b",
			dbPath + "/documents/C/c",
		},
	}
	testGetAll(t, c, srv, dbPath, func(drs []*DocumentRef) ([]*DocumentSnapshot, error) {
		return c.GetAll(context.Background(), drs)
	}, req)
}

func testGetAll(t *testing.T, c *Client, srv *mockServer, dbPath string, getAll func([]*DocumentRef) ([]*DocumentSnapshot, error), req *pb.BatchGetDocumentsRequest) {
	wantPBDocs := []*pb.Document{
		{
			Name:       dbPath + "/documents/C/a",
			CreateTime: aTimestamp,
			UpdateTime: aTimestamp,
			Fields:     map[string]*pb.Value{"f": intval(2)},
		},
		nil,
		{
			Name:       dbPath + "/documents/C/c",
			CreateTime: aTimestamp,
			UpdateTime: aTimestamp,
			Fields:     map[string]*pb.Value{"f": intval(1)},
		},
	}
	wantReadTimes := []*tspb.Timestamp{aTimestamp, aTimestamp2, aTimestamp3}
	srv.addRPC(req,
		[]interface{}{
			// deliberately put these out of order
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Found{wantPBDocs[2]},
				ReadTime: aTimestamp3,
			},
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Found{wantPBDocs[0]},
				ReadTime: aTimestamp,
			},
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Missing{dbPath + "/documents/C/b"},
				ReadTime: aTimestamp2,
			},
		},
	)
	coll := c.Collection("C")
	var docRefs []*DocumentRef
	for _, name := range []string{"a", "b", "c"} {
		docRefs = append(docRefs, coll.Doc(name))
	}
	docs, err := getAll(docRefs)
	if err != nil {
		t.Fatal(err)
	}
	if got, want := len(docs), len(wantPBDocs); got != want {
		t.Errorf("got %d docs, wanted %d", got, want)
	}
	for i, got := range docs {
		want, err := newDocumentSnapshot(docRefs[i], wantPBDocs[i], c, wantReadTimes[i])
		if err != nil {
			t.Fatal(err)
		}
		if diff := testDiff(got, want); diff != "" {
			t.Errorf("#%d: got=--, want==++\n%s", i, diff)
		}
	}
}

func TestGetAllWithEqualRefs(t *testing.T) {
	c, srv, cleanup := newMock(t)
	defer cleanup()

	const dbPath = "projects/projectID/databases/(default)"
	req := &pb.BatchGetDocumentsRequest{
		Database: dbPath,
		Documents: []string{
			dbPath + "/documents/C/a",
			dbPath + "/documents/C/a",
			dbPath + "/documents/C/c",
			dbPath + "/documents/C/a",
			dbPath + "/documents/C/b",
			dbPath + "/documents/C/c",
			dbPath + "/documents/C/b",
		},
	}
	testGetAllWithEqualRefs(t, c, srv, dbPath, func(drs []*DocumentRef) ([]*DocumentSnapshot, error) {
		return c.GetAll(context.Background(), drs)
	}, req)
}

func testGetAllWithEqualRefs(t *testing.T, c *Client, srv *mockServer, dbPath string, getAll func([]*DocumentRef) ([]*DocumentSnapshot, error), req *pb.BatchGetDocumentsRequest) {
	wantPBDocs := []*pb.Document{
		{
			Name:       dbPath + "/documents/C/a",
			CreateTime: aTimestamp,
			UpdateTime: aTimestamp,
			Fields:     map[string]*pb.Value{"f": intval(2)},
		},
		{
			Name:       dbPath + "/documents/C/c",
			CreateTime: aTimestamp,
			UpdateTime: aTimestamp,
			Fields:     map[string]*pb.Value{"f": intval(1)},
		},
		nil,
	}
	srv.addRPC(req,
		[]interface{}{
			// deliberately put these out of order
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Found{wantPBDocs[1]},
				ReadTime: aTimestamp3,
			},
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Found{wantPBDocs[0]},
				ReadTime: aTimestamp,
			},
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Missing{dbPath + "/documents/C/b"},
				ReadTime: aTimestamp2,
			},
		},
	)
	coll := c.Collection("C")
	var docRefs []*DocumentRef
	for _, name := range []string{"a", "a", "c", "a", "b", "c", "b"} {
		docRefs = append(docRefs, coll.Doc(name))
	}
	// GetAll should return the same number of document snapshots as the
	// number of document references in the input range, even when that means
	// that the same document snapshot is referenced multiple times in the
	// returned collection.
	docs, err := getAll(docRefs)
	if err != nil {
		t.Fatal(err)
	}
	wantDocsIndices := []int{0, 0, 1, 0, 2, 1, 2}
	wantReadTimes := []*tspb.Timestamp{aTimestamp, aTimestamp, aTimestamp3, aTimestamp, aTimestamp2, aTimestamp3, aTimestamp2}
	if got, want := len(docs), len(wantDocsIndices); got != want {
		t.Errorf("got %d docs, wanted %d", got, want)
	}
	for i, got := range docs {
		want, err := newDocumentSnapshot(docRefs[i], wantPBDocs[wantDocsIndices[i]], c, wantReadTimes[i])
		if err != nil {
			t.Fatal(err)
		}
		if diff := testDiff(got, want); diff != "" {
			t.Errorf("#%d: got=--, want==++\n%s", i, diff)
		}
	}
}

func TestGetAllErrors(t *testing.T) {
	ctx := context.Background()
	c, srv, cleanup := newMock(t)
	defer cleanup()

	const dbPath = "projects/projectID/databases/(default)"
	const docPath = dbPath + "/documents/C/a"
	if _, err := c.GetAll(ctx, []*DocumentRef{nil}); err != errNilDocRef {
		t.Errorf("got %v, want errNilDocRef", err)
	}

	// Internal server error.
	srv.addRPC(
		&pb.BatchGetDocumentsRequest{
			Database:  dbPath,
			Documents: []string{docPath},
		},
		[]interface{}{status.Errorf(codes.Internal, "")},
	)
	_, err := c.GetAll(ctx, []*DocumentRef{c.Doc("C/a")})
	codeEq(t, "GetAll #1", codes.Internal, err)

	// Doc appears as both found and missing (server bug).
	srv.reset()
	srv.addRPC(
		&pb.BatchGetDocumentsRequest{
			Database:  dbPath,
			Documents: []string{docPath},
		},
		[]interface{}{
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Found{&pb.Document{Name: docPath}},
				ReadTime: aTimestamp,
			},
			&pb.BatchGetDocumentsResponse{
				Result:   &pb.BatchGetDocumentsResponse_Missing{docPath},
				ReadTime: aTimestamp,
			},
		},
	)
	if _, err := c.GetAll(ctx, []*DocumentRef{c.Doc("C/a")}); err == nil {
		t.Error("got nil, want error")
	}
}
