package integration

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/coreos/go-oidc/oidc"
	"github.com/kylelemons/godebug/pretty"
	"google.golang.org/api/googleapi"

	"github.com/coreos/dex/admin"
	"github.com/coreos/dex/client"
	"github.com/coreos/dex/client/manager"
	"github.com/coreos/dex/db"
	"github.com/coreos/dex/schema/adminschema"
	"github.com/coreos/dex/server"
	"github.com/coreos/dex/user"
)

const (
	adminAPITestSecret = "admin_secret"
)

type adminAPITestFixtures struct {
	ur       user.UserRepo
	pwr      user.PasswordInfoRepo
	cr       client.ClientRepo
	adAPI    *admin.AdminAPI
	adSrv    *server.AdminServer
	hSrv     *httptest.Server
	hc       *http.Client
	adClient *adminschema.Service
}

func (t *adminAPITestFixtures) close() {
	t.hSrv.Close()
}

var (
	adminUsers = []user.UserWithRemoteIdentities{
		{
			User: user.User{
				ID:    "ID-1",
				Email: "Email-1@example.com",
			},
		},
		{
			User: user.User{
				ID:    "ID-2",
				Email: "Email-2@example.com",
			},
		},
		{
			User: user.User{
				ID:    "ID-3",
				Email: "Email-3@example.com",
			},
		},
	}

	adminPasswords = []user.PasswordInfo{
		{
			UserID:   "ID-1",
			Password: []byte("hi."),
		},
	}

	clients = []client.Client{
		{
			Credentials: oidc.ClientCredentials{
				ID:     "client-1",
				Secret: "Zm9vYmFy", // "foobar"
			},
			Metadata: oidc.ClientMetadata{
				RedirectURIs: []url.URL{
					url.URL{Scheme: "http", Host: "127.0.0.1:5556", Path: "/cb"},
					url.URL{Scheme: "https", Host: "example.com", Path: "/callback"},
				},
			},
			Admin: true,
		},
	}
)

type adminAPITransport struct {
	secret string
}

func (a *adminAPITransport) RoundTrip(r *http.Request) (*http.Response, error) {
	r.Header.Set("Authorization", a.secret)
	return http.DefaultTransport.RoundTrip(r)
}

func makeAdminAPITestFixtures() *adminAPITestFixtures {
	f := &adminAPITestFixtures{}

	dbMap, ur, pwr, um := makeUserObjects(adminUsers, adminPasswords)

	var cliCount int
	secGen := func() ([]byte, error) {
		id := []byte(fmt.Sprintf("client_%v", cliCount))
		cliCount++
		return id, nil
	}
	cr := db.NewClientRepo(dbMap)
	clientIDGenerator := func(hostport string) (string, error) {
		return fmt.Sprintf("client_%v", hostport), nil
	}
	for _, client := range clients {
		_, err := cr.New(nil, client)
		if err != nil {
			panic(err)
		}
	}
	cm := manager.NewClientManager(cr, db.TransactionFactory(dbMap), manager.ManagerOptions{SecretGenerator: secGen, ClientIDGenerator: clientIDGenerator})
	ccr := db.NewConnectorConfigRepo(dbMap)

	f.cr = cr
	f.ur = ur
	f.pwr = pwr
	f.adAPI = admin.NewAdminAPI(ur, pwr, cr, ccr, um, cm, "local")
	f.adSrv = server.NewAdminServer(f.adAPI, nil, adminAPITestSecret)
	f.hSrv = httptest.NewServer(f.adSrv.HTTPHandler())
	f.hc = &http.Client{
		Transport: &adminAPITransport{
			secret: adminAPITestSecret,
		},
	}
	f.adClient, _ = adminschema.NewWithBasePath(f.hc, f.hSrv.URL)

	return f
}

func TestGetAdmin(t *testing.T) {

	tests := []struct {
		id      string
		errCode int
	}{
		{
			id:      "ID-1",
			errCode: -1,
		},
		{
			id:      "ID-2",
			errCode: http.StatusNotFound,
		},
	}

	for i, tt := range tests {
		func() {
			f := makeAdminAPITestFixtures()
			defer f.close()
			admn, err := f.adClient.Admin.Get(tt.id).Do()
			if tt.errCode != -1 {
				if err == nil {
					t.Errorf("case %d: err was nil", i)
					return
				}
				gErr, ok := err.(*googleapi.Error)
				if !ok {
					t.Errorf("case %d: not a googleapi Error: %q", i, err)
					return
				}

				if gErr.Code != tt.errCode {
					t.Errorf("case %d: want=%d, got=%d", i, tt.errCode, gErr.Code)
					return
				}
			} else {
				if err != nil {
					t.Errorf("case %d: err != nil: %q", i, err)
				}
				if admn == nil {
					t.Errorf("case %d: admn was nil", i)
				}

				if admn.Id != "ID-1" {
					t.Errorf("case %d: want=%q, got=%q", i, tt.id, admn.Id)
				}
			}
		}()
	}
}

func TestCreateAdmin(t *testing.T) {
	tests := []struct {
		admn     *adminschema.Admin
		errCode  int
		secret   string
		noSecret bool
	}{
		{
			admn: &adminschema.Admin{
				Email:    "foo@example.com",
				Password: "foopass",
			},
			errCode: -1,
		},
		{
			admn: &adminschema.Admin{
				Email:    "foo@example.com",
				Password: "foopass",
			},
			errCode: http.StatusUnauthorized,
			secret:  "bad_secret",
		},
		{
			admn: &adminschema.Admin{
				Email:    "foo@example.com",
				Password: "foopass",
			},
			errCode:  http.StatusUnauthorized,
			noSecret: true,
		},
		{
			// duplicate Email
			admn: &adminschema.Admin{
				Email:    "Email-1@example.com",
				Password: "foopass",
			},
			errCode: http.StatusConflict,
		},
		{
			// missing Email
			admn: &adminschema.Admin{
				Password: "foopass",
			},
			errCode: http.StatusBadRequest,
		},
	}
	for i, tt := range tests {
		func() {
			f := makeAdminAPITestFixtures()
			if tt.secret != "" {
				f.hc.Transport = &adminAPITransport{
					secret: tt.secret,
				}
			}
			if tt.noSecret {
				f.hc.Transport = http.DefaultTransport
			}
			defer f.close()

			admn, err := f.adClient.Admin.Create(tt.admn).Do()
			if tt.errCode != -1 {
				if err == nil {
					t.Errorf("case %d: err was nil", i)
					return
				}
				gErr, ok := err.(*googleapi.Error)
				if !ok {
					t.Errorf("case %d: not a googleapi Error: %q", i, err)
					return
				}

				if gErr.Code != tt.errCode {
					t.Errorf("case %d: want=%d, got=%d", i, tt.errCode, gErr.Code)
					return
				}
			} else {
				if err != nil {
					t.Errorf("case %d: err != nil: %q", i, err)
				}

				tt.admn.Id = admn.Id
				if diff := pretty.Compare(tt.admn, admn); diff != "" {
					t.Errorf("case %d: Compare(want, got) = %v", i, diff)
				}

				gotAdmn, err := f.adClient.Admin.Get(admn.Id).Do()
				if err != nil {
					t.Errorf("case %d: err != nil: %q", i, err)
				}
				if diff := pretty.Compare(admn, gotAdmn); diff != "" {
					t.Errorf("case %d: Compare(want, got) = %v", i, diff)
				}

				usr, err := f.ur.GetByRemoteIdentity(nil, user.RemoteIdentity{
					ConnectorID: "local",
					ID:          tt.admn.Id,
				})
				if err != nil {
					t.Errorf("case %d: err != nil: %q", i, err)
				}

				if usr.ID != tt.admn.Id {
					t.Errorf("case %d: want=%q, got=%q", i, tt.admn.Id, usr.ID)
				}

			}
		}()
	}
}

func TestConnectors(t *testing.T) {
	tests := []struct {
		req     adminschema.ConnectorsSetRequest
		want    adminschema.ConnectorsGetResponse
		wantErr bool
	}{
		{
			req: adminschema.ConnectorsSetRequest{
				Connectors: []interface{}{
					map[string]string{
						"type": "local",
						"id":   "local",
					},
				},
			},
			want: adminschema.ConnectorsGetResponse{
				Connectors: []interface{}{
					map[string]string{
						"id": "local",
					},
				},
			},
			wantErr: false,
		},
		{
			req: adminschema.ConnectorsSetRequest{
				Connectors: []interface{}{
					map[string]string{
						"type":         "github",
						"id":           "github",
						"clientID":     "foo",
						"clientSecret": "bar",
					},
					map[string]interface{}{
						"type":                 "oidc",
						"id":                   "oidc",
						"issuerURL":            "https://auth.example.com",
						"clientID":             "foo",
						"clientSecret":         "bar",
						"trustedEmailProvider": true,
					},
				},
			},
			want: adminschema.ConnectorsGetResponse{
				Connectors: []interface{}{
					map[string]string{
						"id":           "github",
						"clientID":     "foo",
						"clientSecret": "bar",
					},
					map[string]interface{}{
						"id":                   "oidc",
						"issuerURL":            "https://auth.example.com",
						"clientID":             "foo",
						"clientSecret":         "bar",
						"trustedEmailProvider": true,
					},
				},
			},
			wantErr: false,
		},
		{
			// Missing "type" argument
			req: adminschema.ConnectorsSetRequest{
				Connectors: []interface{}{
					map[string]string{
						"id": "local",
					},
				},
			},
			wantErr: true,
		},
	}

	for i, tt := range tests {
		f := makeAdminAPITestFixtures()
		if err := f.adClient.Connectors.Set(&tt.req).Do(); err != nil {
			if !tt.wantErr {
				t.Errorf("case %d: failed to set connectors: %v", i, err)
			}
			continue
		}
		if tt.wantErr {
			t.Errorf("case %d: expected error setting connectors", i)
			continue
		}

		resp, err := f.adClient.Connectors.Get().Do()
		if err != nil {
			t.Errorf("case %d: failed toget connectors: %v", i, err)
			continue
		}
		if diff := pretty.Compare(tt.want, resp); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %s", i, diff)
		}
	}
}

func TestCreateClient(t *testing.T) {
	mustParseURL := func(s string) *url.URL {
		u, err := url.Parse(s)
		if err != nil {
			t.Fatalf("couldn't parse URL: %v", err)
		}
		return u
	}

	addIDAndSecret := func(cli adminschema.Client) *adminschema.Client {
		if cli.Id == "" {
			if cli.Public {
				cli.Id = "client_" + cli.ClientName
			} else {
				cli.Id = "client_auth.example.com"
			}
		}

		if cli.Secret == "" {
			cli.Secret = base64.URLEncoding.EncodeToString([]byte("client_0"))
		}
		return &cli
	}

	adminClientGood := adminschema.Client{
		RedirectURIs: []string{"https://auth.example.com/"},
	}
	clientGood := client.Client{
		Credentials: oidc.ClientCredentials{
			ID: "client_auth.example.com",
		},
		Metadata: oidc.ClientMetadata{
			RedirectURIs: []url.URL{*mustParseURL("https://auth.example.com/")},
		},
	}

	clientPublicGood := clientGood
	clientPublicGood.Public = true
	clientPublicGood.Metadata.ClientName = "PublicName"
	clientPublicGood.Metadata.RedirectURIs = []url.URL{}
	clientPublicGood.Credentials.ID = "client_PublicName"

	adminPublicClientGood := adminClientGood
	adminPublicClientGood.Public = true
	adminPublicClientGood.ClientName = "PublicName"
	adminPublicClientGood.RedirectURIs = []string{}

	adminPublicClientMissingName := adminPublicClientGood
	adminPublicClientMissingName.ClientName = ""

	adminPublicClientHasARedirect := adminPublicClientGood
	adminPublicClientHasARedirect.RedirectURIs = []string{"https://auth.example.com/"}

	adminAdminClient := adminClientGood
	adminAdminClient.IsAdmin = true
	clientGoodAdmin := clientGood
	clientGoodAdmin.Admin = true

	adminMultiRedirect := adminClientGood
	adminMultiRedirect.RedirectURIs = []string{"https://auth.example.com/", "https://auth2.example.com/"}
	clientMultiRedirect := clientGood
	clientMultiRedirect.Metadata.RedirectURIs = append(
		clientMultiRedirect.Metadata.RedirectURIs,
		*mustParseURL("https://auth2.example.com/"))

	adminClientWithPeers := adminClientGood
	adminClientWithPeers.TrustedPeers = []string{"test_client_0"}

	adminClientOwnID := adminClientGood
	adminClientOwnID.Id = "my_own_id"

	clientGoodOwnID := clientGood
	clientGoodOwnID.Credentials.ID = "my_own_id"

	adminClientOwnSecret := adminClientGood
	adminClientOwnSecret.Secret = base64.URLEncoding.EncodeToString([]byte("my_own_secret"))
	clientGoodOwnSecret := clientGood

	adminClientOwnIDAndSecret := adminClientGood
	adminClientOwnIDAndSecret.Id = "my_own_id"
	adminClientOwnIDAndSecret.Secret = base64.URLEncoding.EncodeToString([]byte("my_own_secret"))
	clientGoodOwnIDAndSecret := clientGoodOwnID

	adminClientBadSecret := adminClientGood
	adminClientBadSecret.Secret = "not_base64_encoded"

	tests := []struct {
		req              adminschema.ClientCreateRequest
		want             adminschema.ClientCreateResponse
		wantClient       client.Client
		wantError        int
		wantTrustedPeers []string
	}{
		{
			req:       adminschema.ClientCreateRequest{},
			wantError: http.StatusBadRequest,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminschema.Client{
					IsAdmin: true,
				},
			},
			wantError: http.StatusBadRequest,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminschema.Client{
					RedirectURIs: []string{"909090"},
				},
			},
			wantError: http.StatusBadRequest,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientGood,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminClientGood),
			},
			wantClient: clientGood,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminAdminClient,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminAdminClient),
			},
			wantClient: clientGoodAdmin,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminMultiRedirect,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminMultiRedirect),
			},
			wantClient: clientMultiRedirect,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientWithPeers,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminClientWithPeers),
			},
			wantClient:       clientGood,
			wantTrustedPeers: []string{"test_client_0"},
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminPublicClientGood,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminPublicClientGood),
			},
			wantClient: clientPublicGood,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminPublicClientMissingName,
			},
			wantError: http.StatusBadRequest,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminPublicClientHasARedirect,
			},
			wantError: http.StatusBadRequest,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientOwnID,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminClientOwnID),
			},
			wantClient: clientGoodOwnID,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientOwnSecret,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminClientOwnSecret),
			},
			wantClient: clientGoodOwnSecret,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientOwnIDAndSecret,
			},
			want: adminschema.ClientCreateResponse{
				Client: addIDAndSecret(adminClientOwnIDAndSecret),
			},
			wantClient: clientGoodOwnIDAndSecret,
		}, {
			req: adminschema.ClientCreateRequest{
				Client: &adminClientBadSecret,
			},
			wantError: http.StatusBadRequest,
		}, {
			// Client ID already exists
			req: adminschema.ClientCreateRequest{
				Client: &adminschema.Client{
					Id:           "client-1",
					Secret:       "Zm9vYmFy",
					RedirectURIs: []string{"https://auth.example.com/"},
				},
			},
			wantError: http.StatusConflict,
		},
	}

	for i, tt := range tests {
		f := makeAdminAPITestFixtures()
		for j, r := range []string{"https://client0.example.com",
			"https://client1.example.com"} {
			_, err := f.cr.New(nil, client.Client{
				Credentials: oidc.ClientCredentials{
					ID: fmt.Sprintf("test_client_%d", j),
				},
				Metadata: oidc.ClientMetadata{
					RedirectURIs: []url.URL{*mustParseURL(r)},
				},
			})
			if err != nil {
				t.Errorf("case %d, client %d: unexpected error creating client: %v", i, j, err)
				continue
			}
		}

		resp, err := f.adClient.Client.Create(&tt.req).Do()
		if tt.wantError != 0 {
			if err == nil {
				t.Errorf("case %d: want non-nil error.", i)
				continue
			}

			aErr, ok := err.(*googleapi.Error)
			if !ok {
				t.Errorf("case %d: could not assert as adminSchema.Error: %v", i, err)
				continue
			}
			if aErr.Code != tt.wantError {
				t.Errorf("case %d: want aErr.Code=%v, got %v: %v", i, tt.wantError, aErr.Code, aErr)
				continue
			}
			continue
		}

		if err != nil {
			t.Errorf("case %d: unexpected error creating client: %v", i, err)
			continue
		}

		if diff := pretty.Compare(tt.want, resp); diff != "" {
			t.Errorf("case %d: Compare(want, got) = %v", i, diff)
		}

		repoClient, err := f.cr.Get(nil, resp.Client.Id)
		if err != nil {
			t.Errorf("case %d: Unexpected error getting client: %v", i, err)
			continue
		}

		if diff := pretty.Compare(tt.wantClient, repoClient); diff != "" {
			t.Errorf("case %d: Compare(wantClient, repoClient) = %v", i, diff)
		}
	}
}

func TestGetState(t *testing.T) {
	tests := []struct {
		addUsers []user.User
		want     adminschema.State
	}{
		{
			addUsers: []user.User{
				user.User{
					ID:    "ID-admin",
					Email: "Admin@example.com",
					Admin: true,
				},
			},
			want: adminschema.State{
				AdminUserCreated: true,
			},
		},
		{
			want: adminschema.State{
				AdminUserCreated: false,
			},
		},
	}

	for i, tt := range tests {
		func() {
			f := makeAdminAPITestFixtures()
			defer f.close()

			for _, usr := range tt.addUsers {
				err := f.ur.Create(nil, usr)
				if err != nil {
					t.Fatalf("case %d: err != nil: %v", i, err)
				}
			}

			got, err := f.adClient.State.Get().Do()
			if err != nil {
				t.Errorf("case %d: err != nil: %q", i, err)
			}

			if diff := pretty.Compare(tt.want, got); diff != "" {
				t.Errorf("case %d: Compare(want, got) = %v", i, diff)
			}

		}()
	}

}
