// Copyright 2019 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 storage

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"cloud.google.com/go/internal/testutil"
	"google.golang.org/api/googleapi"
	"google.golang.org/api/iterator"
)

func TestHMACKeyHandle_GetParsing(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	projectID := "hmackey-project-id"
	ctx := context.Background()

	tests := []struct {
		res     string
		want    *HMACKey
		wantErr string
	}{
		{
			res: fmt.Sprintf(`
                            {
                                "kind": "storage#hmacKeyMetadata",
                                "projectId":%q,"state":"ACTIVE",
                                "timeCreated": "2019-07-06T11:21:58+00:00",
                                "updated": "2019-07-06T11:22:18+00:00"
                            }`, projectID),
			want: &HMACKey{
				State:       Active,
				ProjectID:   projectID,
				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
			},
		},
		{
			res: fmt.Sprintf(`
                            {
                                "kind": "storage#hmacKeyMetadata",
                                "projectId":%q,"state":"ACTIVE",
                                "timeCreated": "2019-07-06T11:21:58+00:00",
                                "updated": "2019-07-06T11:22:18+00:00"
                            }`, projectID),
			want: &HMACKey{
				State:       Active,
				ProjectID:   projectID,
				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
			},
		},
		{
			res: `{}`,
			// CreatedTime must be formatted in RFC 3339.
			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
		},
		{
			res: `{"timeCreated": "2019-07-foo"}`,
			// CreatedTime must be formatted in RFC 3339.
			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
		},
		{
			res: `{
                                "kind": "storage#hmacKeyMetadata",
                                "state":"INACTIVE",
                                "timeCreated": "2019-07-06T11:21:58+00:00"
                            }`,
			// UpdatedTime must be formatted in RFC 3339.
			wantErr: `UpdatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
		},
	}

	for i, tt := range tests {
		mt.addResult(&http.Response{
			ProtoMajor:    1,
			ProtoMinor:    1,
			ContentLength: int64(len(tt.res)),
			Status:        "OK",
			StatusCode:    200,
			Body:          bodyReader(tt.res),
		}, nil)
		hkh := client.HMACKeyHandle(projectID, "some-access-key-id")
		got, err := hkh.Get(ctx)
		if tt.wantErr != "" {
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
			}
			if got != nil {
				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
			}
			continue
		}

		if err != nil {
			t.Errorf("#%d: got an unexpected error: %v", i, err)
			continue
		}

		if diff := testutil.Diff(got, tt.want); diff != "" {
			t.Errorf("#%d: got - want +\n\n%s", i, diff)
		}
	}
}

func TestHMACKeyHandle_Get_NotFound(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	ctx := context.Background()

	mt.addResult(&http.Response{
		ProtoMajor: 2,
		ProtoMinor: 0,
		Status:     "OK",
		StatusCode: http.StatusNotFound,
		Body:       bodyReader("Access ID not found in project"),
	}, nil)

	hkh := client.HMACKeyHandle("project-id", "some-access-key-id")
	_, gotErr := hkh.Get(ctx)

	wantErr := &googleapi.Error{
		Body:    "Access ID not found in project",
		Code:    http.StatusNotFound,
		Message: "",
	}
	if diff := testutil.Diff(gotErr, wantErr); diff != "" {
		t.Fatalf("Error mismatch, got - want +\n%s", diff)
	}
}

func TestHMACKeyHandle_Delete(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	ctx := context.Background()

	tests := []struct {
		statusCode int
		msg        string
		wantErr    error
	}{
		{
			statusCode: http.StatusBadRequest,
			msg:        "Cannot delete keys in 'ACTIVE' state",
			wantErr: &googleapi.Error{
				Code: http.StatusBadRequest, Message: "Cannot delete keys in 'ACTIVE' state",
				Body: `{"error":{"message":"Cannot delete keys in 'ACTIVE' state"}}`,
			},
		},
		{
			statusCode: http.StatusNotFound,
			msg:        "random message",
			wantErr: &googleapi.Error{
				Code: http.StatusNotFound, Message: "random message",
				Body: `{"error":{"message":"random message"}}`,
			},
		},
		{
			statusCode: http.StatusNotFound,
			msg:        "Access ID not found in project",
			wantErr: &googleapi.Error{
				Code: http.StatusNotFound, Message: "Access ID not found in project",
				Body: `{"error":{"message":"Access ID not found in project"}}`,
			},
		},
	}

	for i, tt := range tests {
		mt.addResult(&http.Response{
			ProtoMajor: 2,
			ProtoMinor: 0,
			Status:     tt.msg,
			StatusCode: tt.statusCode,
			Body:       bodyReader(fmt.Sprintf(`{"error":{"message":%q}}`, tt.msg)),
		}, nil)

		hkh := client.HMACKeyHandle("project", "access-key-id")
		err := hkh.Delete(ctx)

		if diff := testutil.Diff(err, tt.wantErr); diff != "" {
			t.Errorf("#%d: error mismatch got - want +\n%s", i, diff)
		}
	}
}

func TestHMACKeyHandle_Create(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	projectID := "hmackey-project-id"
	serviceAccountEmail := "service-account-email-1"
	ctx := context.Background()

	tests := []struct {
		res     string
		want    *HMACKey
		wantErr string
	}{
		{
			res: `
                            {
                                "kind": "storage#hmackey",
				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
                                "metadata": {
                                    "projectId":"project-id","state":"ACTIVE",
                                    "timeCreated": "2019-07-06T11:21:58+00:00",
                                    "updated": "2019-07-06T11:22:18+00:00"
                                }
                            }`,
			want: &HMACKey{
				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
				State:       Active,
				ProjectID:   "project-id",
				UpdatedTime: time.Date(2019, 07, 06, 11, 22, 18, 0, time.UTC),
				CreatedTime: time.Date(2019, 07, 06, 11, 21, 58, 0, time.UTC),
			},
		},
		{
			res:     `{}`,
			wantErr: "Metadata cannot be nil",
		},
		{
			res: `{"metadata":{}}`,
			// CreatedTime must be non-empty and it must formatted in RFC 3339.
			wantErr: `CreatedTime: parsing time "" as "2006-01-02T15:04:05Z07:00"`,
		},
		{
			res: `{"metadata":{"timeCreated": "2019-07-foo"}}`,
			// CreatedTime must be formatted in RFC 3339.
			wantErr: `CreatedTime: parsing time "2019-07-foo" as "2006-01-02T15:04:05Z07:00"`,
		},
		{
			res: `{
                                "kind": "storage#hmackey",
				"secret":"bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
                                "metadata":{
                                    "kind": "storage#hmacKeyMetadata",
                                    "state":"ACTIVE",
                                    "timeCreated": "2019-07-06T12:11:33+00:00",
                                    "projectId": "project-id",
                                    "updated": ""
                                }
                            }`,
			// ONLY during creation is it okay for UpdatedTime to not be set.
			want: &HMACKey{
				Secret:      "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
				State:       Active,
				ProjectID:   "project-id",
				CreatedTime: time.Date(2019, 07, 06, 12, 11, 33, 0, time.UTC),
			},
		},
	}

	for i, tt := range tests {
		mt.addResult(&http.Response{
			ProtoMajor:    1,
			ProtoMinor:    1,
			ContentLength: int64(len(tt.res)),
			Status:        "OK",
			StatusCode:    200,
			Body:          bodyReader(tt.res),
		}, nil)
		got, err := client.CreateHMACKey(ctx, projectID, serviceAccountEmail)
		if tt.wantErr != "" {
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Errorf("#%d: failed to match errors:\ngot:  %q\nwant: %q", i, err, tt.wantErr)
			}
			if got != nil {
				t.Errorf("#%d: unexpectedly got a non-nil result: %#v\n", i, got)
			}
			continue
		}

		if err != nil {
			t.Errorf("#%d: got an unexpected error: %v", i, err)
			continue
		}

		if diff := testutil.Diff(got, tt.want); diff != "" {
			t.Errorf("#%d: got - want +\n\n%s", i, diff)
		}
	}

	// Lastly ensure that a blank service account will return an error.
	mt.addResult(&http.Response{
		ProtoMajor: 1,
		ProtoMinor: 1,
		Status:     "OK",
		StatusCode: 200,
		Body:       bodyReader("{}"),
	}, nil)
	hk, err := client.CreateHMACKey(ctx, projectID, "")
	if err == nil {
		t.Fatal("Unexpectedly succeeded in creating a key using a blank service account email")
	}
	if !strings.Contains(err.Error(), "non-blank service account email") {
		t.Fatalf("Expected an error about a non-blank service account email: %v", err)
	}
	if hk != nil {
		t.Fatalf("Unexpectedly got back a created HMACKey: %#v", hk)
	}
}

func TestHMACKey_UpdateState(t *testing.T) {
	// This test ensures that updating the state can only
	// happen with either of Active or Inactive.

	mt := &mockTransport{}
	client := mockClient(t, mt)
	projectID := "hmackey-project-id"
	ctx := context.Background()

	hkh := client.HMACKeyHandle(projectID, "some-access-id")

	// 1. Ensure that invalid states are NOT accepted for an Update.
	invalidStates := []HMACState{"", Deleted, "active", "inactive", "foo_bar"}
	for _, invalidState := range invalidStates {
		t.Run("invalid-"+string(invalidState), func(t *testing.T) {
			_, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
				State: invalidState,
			})
			if err == nil {
				t.Fatal("Unexpectedly succeeded")
			}
			invalidStateMsg := fmt.Sprintf(`storage: invalid state %q for update, must be either "ACTIVE" or "INACTIVE"`, invalidState)
			if err.Error() != invalidStateMsg {
				t.Fatalf("Mismatched error: got:  %q\nwant: %q", err, invalidStateMsg)
			}
		})
	}

	// 2. Ensure that valid states for Update are accepted.
	validStates := []HMACState{Active, Inactive}
	for _, validState := range validStates {
		t.Run("valid-"+string(validState), func(t *testing.T) {
			resBody := fmt.Sprintf(`{
                                    "kind": "storage#hmacKeyMetadata",
                                    "state":%q,
                                    "timeCreated": "2019-07-11T12:11:33+00:00",
                                    "projectId": "project-id",
                                    "updated": "2019-07-11T12:13:33+00:00"
                            }`, validState)
			mt.addResult(&http.Response{
				ProtoMajor:    1,
				ProtoMinor:    1,
				ContentLength: int64(len(resBody)),
				Status:        "OK",
				StatusCode:    200,
				Body:          bodyReader(resBody),
			}, nil)

			hu, err := hkh.Update(ctx, HMACKeyAttrsToUpdate{
				State: validState,
			})
			if err != nil {
				t.Fatalf("Unexpected failure: %v", err)
			}
			if hu.State != validState {
				t.Fatalf("Unexpected updated state %q, expected %q", hu.State, validState)
			}
		})
	}
}

func TestHMACKey_ListFull(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	projectID := "hmackey-project-id"
	ctx := context.Background()

	maxPages := 2
	page := 0
	mockResponse := func() {
		defer func() {
			page++
		}()

		var body string
		if page >= maxPages {
			body = `{"kind":"storage#hmacKeysMetadata","items":[]}`
		} else {
			offset := page * 2
			body = fmt.Sprintf(`
                        {
                            "kind": "storage#hmacKeysMetadata",
                            "items": [{
                                "accessId": "accessid-%d",
                                "timeCreated": "2019-08-05T12:11:10+00:00",
                                "state": "ACTIVE"
                            }, {
                                "accessId": "accessid-%d",
                                "timeCreated": "2019-08-05T13:12:11+00:00",
                                "state": "INACTIVE"
                            }],
                            "nextPageToken": "pageToken"
                        }`, offset+1, offset+2)
		}

		mt.addResult(&http.Response{
			ProtoMajor: 2,
			ProtoMinor: 0,
			Status:     "OK",
			StatusCode: 200,
			Body:       bodyReader(body),
		}, nil)
	}

	iter := client.ListHMACKeys(ctx, projectID)

	var gotKeys []*HMACKey
	for {
		mockResponse()
		key, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}
		gotKeys = append(gotKeys, key)
	}

	wantKeys := []*HMACKey{
		{
			AccessID:    "accessid-1",
			CreatedTime: time.Date(2019, time.August, 5, 12, 11, 10, 0, time.UTC),
			State:       Active,
		},
		{
			AccessID:    "accessid-2",
			CreatedTime: time.Date(2019, time.August, 5, 13, 12, 11, 0, time.UTC),
			State:       Inactive,
		},
		{
			AccessID:    "accessid-3",
			CreatedTime: time.Date(2019, time.August, 5, 12, 11, 10, 0, time.UTC),
			State:       Active,
		},
		{
			AccessID:    "accessid-4",
			CreatedTime: time.Date(2019, time.August, 5, 13, 12, 11, 0, time.UTC),
			State:       Inactive,
		},
	}

	if diff := testutil.Diff(gotKeys, wantKeys); diff != "" {
		t.Fatalf("Response mismatch: got - want +\n%s", diff)
	}
}

func TestHMACKey_List_Options(t *testing.T) {
	mt := &mockTransport{}
	client := mockClient(t, mt)
	projectID := "hmackey-project-id"

	// Our goal is just to examine the issued HTTP request's URL's
	// Path and Query to ensure that we have the appropriate paramters.
	tests := []struct {
		name      string
		opts      []HMACKeyOption
		wantQuery url.Values
	}{
		{
			name: "defaults",
			wantQuery: url.Values{
				"alt":         {"json"},
				"prettyPrint": {"false"},
			},
		},
		{
			name:      "show deleted keys",
			opts:      []HMACKeyOption{ShowDeletedHMACKeys()},
			wantQuery: url.Values{"alt": {"json"}, "prettyPrint": {"false"}, "showDeletedKeys": {"true"}},
		},
		{
			name: "for service account",
			opts: []HMACKeyOption{ForHMACKeyServiceAccountEmail("foo@example.org")},
			wantQuery: url.Values{
				"alt":                 {"json"},
				"prettyPrint":         {"false"},
				"serviceAccountEmail": {"foo@example.org"},
			},
		},
		{
			name: "for userProjectID",
			opts: []HMACKeyOption{UserProjectForHMACKeys("project-x")},
			wantQuery: url.Values{
				"alt":         {"json"},
				"prettyPrint": {"false"},
				"userProject": {"project-x"},
			},
		},
		{
			name: "all options",
			opts: []HMACKeyOption{
				ForHMACKeyServiceAccountEmail("foo@example.org"),
				UserProjectForHMACKeys("project-x"),
				ShowDeletedHMACKeys(),
			},
			wantQuery: url.Values{
				"alt":                 {"json"},
				"prettyPrint":         {"false"},
				"serviceAccountEmail": {"foo@example.org"},
				"showDeletedKeys":     {"true"},
				"userProject":         {"project-x"},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			body := `{"kind":"storage#hmacKeysMetadata","items":[]}`
			mt.addResult(&http.Response{
				ProtoMajor:    2,
				ProtoMinor:    0,
				ContentLength: int64(len(body)),
				Status:        "OK",
				StatusCode:    200,
				Body:          bodyReader(body),
			}, nil)
			ctx, cancel := context.WithCancel(context.Background())
			iter := client.ListHMACKeys(ctx, projectID, tt.opts...)
			_, _ = iter.Next()
			cancel()

			gotQuery := mt.gotReq.URL.Query()
			if diff := testutil.Diff(gotQuery, tt.wantQuery); diff != "" {
				t.Errorf("Query mismatch: got - want +\n%s", diff)
			}
		})
	}
}
