// Copyright 2016 Google Inc. All Rights Reserved.
//
// 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 controller

import (
	"bytes"
	"errors"
	"fmt"
	"strconv"
	"testing"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"

	cd "google.golang.org/api/clouddebugger/v2"
	"google.golang.org/api/googleapi"
)

const (
	testDebuggeeID   = "d12345"
	testBreakpointID = "bp12345"
)

var (
	// The sequence of wait tokens in List requests and responses.
	expectedWaitToken = []string{"init", "token1", "token2", "token1", "token1"}
	// The set of breakpoints returned from each List call.
	expectedBreakpoints = [][]*cd.Breakpoint{
		nil,
		{
			&cd.Breakpoint{
				Id:           testBreakpointID,
				IsFinalState: false,
				Location:     &cd.SourceLocation{Line: 42, Path: "foo.go"},
			},
		},
		nil,
	}
	abortedError error = &googleapi.Error{
		Code:    409,
		Message: "Conflict",
		Body: `{
		 "error": {
		  "errors": [
		   {
		    "domain": "global",
		    "reason": "aborted",
		    "message": "Conflict"
		   }
		  ],
		  "code": 409,
		  "message": "Conflict"
		 }
		}`,
		Errors: []googleapi.ErrorItem{
			{Reason: "aborted", Message: "Conflict"},
		},
	}
	backendError error = &googleapi.Error{
		Code:    503,
		Message: "Backend Error",
		Body: `{
		 "error": {
		  "errors": [
		   {
		    "domain": "global",
		    "reason": "backendError",
		    "message": "Backend Error"
		   }
		  ],
		  "code": 503,
		  "message": "Backend Error"
		 }
		}`,
		Errors: []googleapi.ErrorItem{
			{Reason: "backendError", Message: "Backend Error"},
		},
	}
)

type mockService struct {
	t                 *testing.T
	listCallsSeen     int
	registerCallsSeen int
}

func (s *mockService) Register(ctx context.Context, req *cd.RegisterDebuggeeRequest) (*cd.RegisterDebuggeeResponse, error) {
	s.registerCallsSeen++
	if req.Debuggee == nil {
		s.t.Errorf("missing debuggee")
		return nil, nil
	}
	if req.Debuggee.AgentVersion == "" {
		s.t.Errorf("missing agent version")
	}
	if req.Debuggee.Description == "" {
		s.t.Errorf("missing debuglet description")
	}
	if req.Debuggee.Project == "" {
		s.t.Errorf("missing project id")
	}
	if req.Debuggee.Uniquifier == "" {
		s.t.Errorf("missing uniquifier")
	}
	return &cd.RegisterDebuggeeResponse{
		Debuggee: &cd.Debuggee{Id: testDebuggeeID},
	}, nil
}

func (s *mockService) Update(ctx context.Context, id, breakpointID string, req *cd.UpdateActiveBreakpointRequest) (*cd.UpdateActiveBreakpointResponse, error) {
	if id != testDebuggeeID {
		s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID)
	}
	if breakpointID != testBreakpointID {
		s.t.Errorf("got breakpoint ID %s want %s", breakpointID, testBreakpointID)
	}
	if !req.Breakpoint.IsFinalState {
		s.t.Errorf("got IsFinalState = false, want true")
	}
	return nil, nil
}

func (s *mockService) List(ctx context.Context, id, waitToken string) (*cd.ListActiveBreakpointsResponse, error) {
	if id != testDebuggeeID {
		s.t.Errorf("got debuggee ID %s want %s", id, testDebuggeeID)
	}
	if waitToken != expectedWaitToken[s.listCallsSeen] {
		s.t.Errorf("got wait token %s want %s", waitToken, expectedWaitToken[s.listCallsSeen])
	}
	s.listCallsSeen++
	if s.listCallsSeen == 4 {
		return nil, backendError
	}
	if s.listCallsSeen == 5 {
		return nil, abortedError
	}
	resp := &cd.ListActiveBreakpointsResponse{
		Breakpoints:   expectedBreakpoints[s.listCallsSeen-1],
		NextWaitToken: expectedWaitToken[s.listCallsSeen],
	}
	return resp, nil
}

func TestDebugletControllerClientLibrary(t *testing.T) {
	var (
		m    *mockService
		c    *Controller
		list *cd.ListActiveBreakpointsResponse
		err  error
	)
	m = &mockService{t: t}
	newService = func(context.Context, oauth2.TokenSource) (serviceInterface, error) { return m, nil }
	opts := Options{
		ProjectNumber: "5",
		ProjectID:     "p1",
		AppModule:     "mod1",
		AppVersion:    "v1",
	}
	ctx := context.Background()
	if c, err = NewController(ctx, opts); err != nil {
		t.Fatal("Initializing Controller client:", err)
	}
	if err := validateLabels(c, opts); err != nil {
		t.Fatalf("Invalid labels:\n%v", err)
	}
	if list, err = c.List(ctx); err != nil {
		t.Fatal("List:", err)
	}
	if m.registerCallsSeen != 1 {
		t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen)
	}
	if list, err = c.List(ctx); err != nil {
		t.Fatal("List:", err)
	}
	if len(list.Breakpoints) != 1 {
		t.Fatalf("got %d breakpoints, want 1", len(list.Breakpoints))
	}
	if err = c.Update(ctx, list.Breakpoints[0].Id, &cd.Breakpoint{Id: testBreakpointID, IsFinalState: true}); err != nil {
		t.Fatal("Update:", err)
	}
	if list, err = c.List(ctx); err != nil {
		t.Fatal("List:", err)
	}
	if m.registerCallsSeen != 1 {
		t.Errorf("saw %d Register calls, want 1", m.registerCallsSeen)
	}
	// The next List call produces an error that should cause a Register call.
	if list, err = c.List(ctx); err == nil {
		t.Fatal("List should have returned an error")
	}
	if m.registerCallsSeen != 2 {
		t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen)
	}
	// The next List call produces an error that should not cause a Register call.
	if list, err = c.List(ctx); err == nil {
		t.Fatal("List should have returned an error")
	}
	if m.registerCallsSeen != 2 {
		t.Errorf("saw %d Register calls, want 2", m.registerCallsSeen)
	}
	if m.listCallsSeen != 5 {
		t.Errorf("saw %d list calls, want 5", m.listCallsSeen)
	}
}

func validateLabels(c *Controller, o Options) error {
	errMsg := new(bytes.Buffer)
	if m, ok := c.labels["module"]; ok {
		if m != o.AppModule {
			errMsg.WriteString(fmt.Sprintf("label module: want %s, got %s\n", o.AppModule, m))
		}
	} else {
		errMsg.WriteString("Missing \"module\" label\n")
	}
	if v, ok := c.labels["version"]; ok {
		if v != o.AppVersion {
			errMsg.WriteString(fmt.Sprintf("label version: want %s, got %s\n", o.AppVersion, v))
		}
	} else {
		errMsg.WriteString("Missing \"version\" label\n")
	}
	if mv, ok := c.labels["minorversion"]; ok {
		if _, err := strconv.Atoi(mv); err != nil {
			errMsg.WriteString(fmt.Sprintln("label minorversion: not a numeric string:", mv))
		}
	} else {
		errMsg.WriteString("Missing \"minorversion\" label\n")
	}
	if errMsg.Len() != 0 {
		return errors.New(errMsg.String())
	}
	return nil
}

func TestIsAbortedError(t *testing.T) {
	if !isAbortedError(abortedError) {
		t.Errorf("isAborted(%+v): got false, want true", abortedError)
	}
	if isAbortedError(backendError) {
		t.Errorf("isAborted(%+v): got true, want false", backendError)
	}
}
