package authn

import (
	"bytes"
	"context"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/golang-jwt/jwt"
	"github.com/molecula/featurebase/v3/logger"
	"golang.org/x/oauth2"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

func NewTestAuth(t *testing.T) *Auth {
	t.Helper()
	var (
		ClientID         = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
		ClientSecret     = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
		AuthorizeURL     = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
		TokenURL         = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token"
		GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true"
		LogoutURL        = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
		Scopes           = []string{"https://graph.microsoft.com/.default", "offline_access"}
		Key              = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
	)

	a, err := NewAuth(
		logger.NewStandardLogger(os.Stdout),
		"http://localhost:10101/",
		Scopes,
		AuthorizeURL,
		TokenURL,
		GroupEndpointURL,
		LogoutURL,
		ClientID,
		ClientSecret,
		Key,
	)
	if err != nil {
		t.Fatalf("building auth object%s", err)
	}
	return a
}
func TestAuth(t *testing.T) {
	a := NewTestAuth(t)
	t.Run("SetCookie", func(t *testing.T) {
		w := httptest.NewRecorder()
		err := a.SetCookie(w, "a cookie value", time.Now().Add(time.Hour))
		if err != nil {
			t.Fatalf("expected no errors, got: %v", err)
		}

		if w.Result().Cookies()[0].Value == "" {
			t.Errorf("expected something, got empty string")
		}

		if got, want := w.Result().Cookies()[0].Path, "/"; got != want {
			t.Fatalf("path=%s, want %s", got, want)
		}
	})
	t.Run("SetGRPCMetadata", func(t *testing.T) {
		md := metadata.MD{
			"cookie": []string{a.cookieName + "=something"},
		}
		ctx := grpc.NewContextWithServerTransportStream(
			metadata.NewIncomingContext(context.TODO(),
				md,
			),
			NewServerTransportStream(),
		)
		md, ok := metadata.FromIncomingContext(ctx)
		if !ok {
			t.Fatalf("expected ok, got: %v", ok)
		}
		err := a.SetGRPCMetadata(ctx, md, "this is a token!")
		if err != nil {
			t.Fatalf("expected no errors, got: %v", err)
		}
		md, ok = metadata.FromIncomingContext(ctx)
		if !ok {
			t.Fatalf("expected ok, got: %v", ok)
		}
		c, ok := md["cookie"]
		if !ok {
			t.Fatalf("expected ok, got: %v", ok)
		}
		var cookie string
		for _, cookie = range c {
			if strings.HasPrefix(cookie, a.cookieName) {
				break
			}
		}
		if exp, got := a.cookieName+"=this is a token!", cookie; got != exp {
			t.Fatalf("expected '%v', got '%v'", exp, got)
		}
	})
	t.Run("KeyLength", func(t *testing.T) {
		_, err := NewAuth(
			logger.NewStandardLogger(os.Stdout),
			"http://localhost:10101/",
			[]string{"https://graph.microsoft.com/.default", "offline_access"},
			"https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize",
			"https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token",
			"https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true",
			"https://login.microsoftonline.com/common/oauth2/v2.0/logout",
			"e9088663-eb08-41d7-8f65-efb5f54bbb71",
			"DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF",
			"DEADBEEFD",
		)
		if err == nil || !strings.Contains(err.Error(), "decoding secret key") {
			t.Fatalf("expected error decoding secret key got: %v", err)
		}
	})
	t.Run("GetSecretKey", func(t *testing.T) {
		want, _ := hex.DecodeString("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
		if got := a.SecretKey(); !bytes.Equal(got, want) {
			t.Fatalf("expected %v, got %v", got, want)
		}
	})
}

func TestAuthenticate(t *testing.T) {
	cases := []struct {
		name         string
		uid          string
		uname        string
		exp          int64
		refresh      bool
		errOnRefresh bool
		malformed    bool
		groups       []Group
		err          error
	}{
		{
			name:  "GoodToken",
			uid:   "42",
			uname: "A. Token",
			groups: []Group{
				{
					GroupID:   "ac97c9e2-346b-42a2-b6da-18bcb61a32fe",
					GroupName: "adminGroup",
				},
			},
		},
		{
			name:      "Malformed",
			malformed: true,
			err:       fmt.Errorf("parsing bearer token: token contains an invalid number of segments"),
		},

		{
			name:  "ExpiredTokenNoRefresh",
			uid:   "42",
			uname: "A. Token",
			groups: []Group{
				{
					GroupID:   "ac97c9e2-346b-42a2-b6da-18bcb61a32fe",
					GroupName: "adminGroup",
				},
			},
			exp: -17764800,
			err: fmt.Errorf("token is expired"),
		},
		{
			name:  "ExpiredTokenYesRefresh",
			uid:   "42",
			uname: "A. Token",
			groups: []Group{
				{
					GroupID:   "ac97c9e2-346b-42a2-b6da-18bcb61a32fe",
					GroupName: "adminGroup",
				},
			},
			refresh: true,
			exp:     -17764800,
		},
		{
			name:  "ExpiredTokenYesRefreshButError",
			uid:   "42",
			uname: "A. Token",
			groups: []Group{
				{
					GroupID:   "ac97c9e2-346b-42a2-b6da-18bcb61a32fe",
					GroupName: "adminGroup",
				},
			},
			refresh:      true,
			errOnRefresh: true,
			exp:          -17764800,
			err:          fmt.Errorf("decoding refreshed token: invalid character 'b' looking for beginning of value"),
		},
	}
	for _, test := range cases {
		t.Run(test.name, func(t *testing.T) {
			// setup the test
			a := NewTestAuth(t)
			token := ""
			var err error
			if !test.malformed {
				tkn := jwt.New(jwt.SigningMethodHS256)
				claims := tkn.Claims.(jwt.MapClaims)
				claims["oid"] = test.uid
				claims["name"] = test.uname
				if test.exp != 0 {
					claims["exp"] = strconv.Itoa(int(test.exp))
				}
				token, err = tkn.SignedString(a.SecretKey())
				if err != nil {
					t.Fatalf("unexpected error when signing token %v", err)
				}
			} else {
				token = "asdfasdfasdfasdF"
			}
			if len(test.groups) > 0 {
				a.groupsCache[token] = cachedGroups{time.Now(), test.groups}
			}
			if test.refresh {
				var srv *httptest.Server
				if !test.errOnRefresh {
					srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						tkn := jwt.New(jwt.SigningMethodHS256)
						claims := tkn.Claims.(jwt.MapClaims)
						claims["oid"] = test.uid
						claims["name"] = test.uname
						expiry := strconv.Itoa(int(time.Now().Add(2 * time.Hour).Unix()))
						claims["exp"] = expiry
						fresh, err := tkn.SignedString(a.SecretKey())
						if err != nil {
							t.Fatalf("unexpected error when signing token %v", err)
						}

						a.groupsCache[fresh] = cachedGroups{time.Now(), test.groups}
						fmt.Fprintf(w, `{"access_token": "`+fresh+`",  "refresh_token": "blah",  "token_type": "bearer",  "expires": `+expiry+` }`)
					}))
				} else {
					srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						http.Error(w, "bad", http.StatusInternalServerError)
					}))
				}
				defer srv.Close()
				a.oAuthConfig.Endpoint.TokenURL = srv.URL
				a.tokenCache[token] = cachedToken{
					time.Now(),
					&oauth2.Token{
						AccessToken:  token,
						RefreshToken: "blah",
						Expiry:       time.Unix(test.exp, 0),
					},
				}
			}

			// do the actual testing
			uinfo, err := a.Authenticate(context.TODO(), token)
			// okay this part kind of sucks bc we need to check errors and i
			// dont want to write a whole new test for things that should have
			// errors just to avoid this mess. errors.Is doesn't work either
			if (test.err == nil && err != nil) || (test.err != nil && err == nil) {
				t.Fatalf("expected %v, but got %v", test.err, err)
			} else if test.err != nil && err != nil {
				if test.err.Error() != err.Error() {
					t.Fatalf("expected %v, but got %v", test.err, err)
				} else {
					return
				}
			}

			if !reflect.DeepEqual(uinfo.Groups, test.groups) {
				t.Fatalf("expected %v, got %v", test.groups, uinfo.Groups)
			}
			if !reflect.DeepEqual(uinfo.UserID, test.uid) {
				t.Fatalf("expected %v, got %v", test.uid, uinfo.UserID)
			}
			if !reflect.DeepEqual(uinfo.UserName, test.uname) {
				t.Fatalf("expected %v, got %v", test.uname, uinfo.UserName)
			}
		})
	}
}

func TestAuthenticate_CleanCache(t *testing.T) {
	// this deserves its own test bc it has gross setup required
	t.Run("should clean", func(t *testing.T) {
		a := NewTestAuth(t)
		now := time.Now()
		a.groupsCache["oldy"] = cachedGroups{now.Add(-24 * time.Hour), []Group{}}
		a.groupsCache["goldy"] = cachedGroups{now.Add(-4 * time.Hour), []Group{}}
		a.tokenCache["oldy"] = cachedToken{now.Add(-24 * time.Hour), &oauth2.Token{}}
		a.tokenCache["goldy"] = cachedToken{now.Add(-4 * time.Hour), &oauth2.Token{}}
		a.lastCacheClean = now.Add(-45 * time.Minute)

		_, _ = a.Authenticate(context.TODO(), "this doesn't matter")
		if a.lastCacheClean.Sub(now) <= time.Nanosecond {
			t.Fatalf("cache should have been cleaned")
		}
		if _, ok := a.groupsCache["oldy"]; ok {
			t.Errorf("oldy should have been deleted")
		}
		if _, ok := a.groupsCache["goldy"]; !ok {
			t.Errorf("goldy should not have been deleted")
		}
		if _, ok := a.tokenCache["oldy"]; ok {
			t.Errorf("oldy should have been deleted")
		}
		if _, ok := a.tokenCache["goldy"]; !ok {
			t.Errorf("goldy should not have been deleted")
		}
	})
	t.Run("shouldn't clean", func(t *testing.T) {
		a := NewTestAuth(t)
		now := time.Now()
		a.groupsCache["oldy"] = cachedGroups{now.Add(-24 * time.Hour), []Group{}}
		a.groupsCache["goldy"] = cachedGroups{now.Add(-4 * time.Hour), []Group{}}
		a.tokenCache["oldy"] = cachedToken{now.Add(-24 * time.Hour), &oauth2.Token{}}
		a.tokenCache["goldy"] = cachedToken{now.Add(-4 * time.Hour), &oauth2.Token{}}
		a.lastCacheClean = now

		_, _ = a.Authenticate(context.TODO(), "this doesn't matter")
		if a.lastCacheClean.Sub(now) >= time.Nanosecond {
			t.Fatalf("cache should not have been cleaned")
		}
		if _, ok := a.groupsCache["oldy"]; !ok {
			t.Errorf("oldy should not have been deleted")
		}
		if _, ok := a.groupsCache["goldy"]; !ok {
			t.Errorf("goldy should not have been deleted")
		}
		if _, ok := a.tokenCache["oldy"]; !ok {
			t.Errorf("oldy should not have been deleted")
		}
		if _, ok := a.tokenCache["goldy"]; !ok {
			t.Errorf("goldy should not have been deleted")
		}
	})

}

func TestGetGroups(t *testing.T) {
	a := NewTestAuth(t)
	a.groupsCache = map[string]cachedGroups{
		"the world is changed": {
			cacheTime: time.Now(),
			groups: []Group{
				{
					GroupID:   "i feel it in the water",
					GroupName: "i feel it in the earth",
				},
			},
		},
	}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		body, err := json.Marshal(
			Groups{
				Groups: []Group{
					{
						GroupID:   "much that once was is lost",
						GroupName: "for none now live who remember it",
					},
				},
			},
		)
		if err != nil {
			t.Fatalf("unexpected error marshalling groups response: %v", err)
		}
		fmt.Fprintf(w, "%s", body)
	}))
	a.groupEndpoint = srv.URL

	for name, test := range map[string]struct {
		token  string
		groups []Group
	}{
		"InCache": {
			token: "the world is changed",
			groups: []Group{
				{
					GroupID:   "i feel it in the water",
					GroupName: "i feel it in the earth",
				},
			},
		},
		"NotInCache": {
			token: "i smell it in the air",
			groups: []Group{
				{
					GroupID:   "much that once was is lost",
					GroupName: "for none now live who remember it",
				},
			},
		},
	} {
		t.Run(name, func(t *testing.T) {
			if got, err := a.getGroups(test.token); err != nil || !reflect.DeepEqual(got, test.groups) {
				t.Errorf("expected %v, nil, got %v, %v", test.groups, got, err)
			}
		})
	}

}

func TestDecodeHex(t *testing.T) {
	t.Run("cantDecode", func(t *testing.T) {
		_, err := decodeHex("gggg")
		if err == nil {
			t.Fatalf("expected err cannot decode slice, got nil")
		}
	})
	t.Run("tooSmall", func(t *testing.T) {
		_, err := decodeHex("DEADBEEF")
		if err == nil {
			t.Fatalf("expected err wrong length, got nil")
		}
	})
	t.Run("tooBig", func(t *testing.T) {
		_, err := decodeHex("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
		if err == nil {
			t.Fatalf("expected err wrong length, got nil")
		}
	})
	t.Run("justRight", func(t *testing.T) {
		_, err := decodeHex("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
		if err != nil {
			t.Fatalf("expected nil, got %v", err)
		}
	})
}

func TestHandlers(t *testing.T) {
	a := NewTestAuth(t)
	t.Run("login", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/login", nil)
		w := httptest.NewRecorder()
		a.Login(w, req)
		resp := w.Result()
		if resp.StatusCode != http.StatusTemporaryRedirect {
			t.Fatalf("expected redirect, got %v", resp.StatusCode)
		}
		redirect := a.oAuthConfig.AuthCodeURL(a.oAuthConfig.Endpoint.AuthURL)
		if got, err := resp.Location(); err != nil || got.String() != redirect {
			t.Fatalf("expected %v, got %v", redirect, got.Path)
		}
	})
	t.Run("logout", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/logout", nil)
		w := httptest.NewRecorder()
		req.AddCookie(
			&http.Cookie{
				Name:     a.cookieName,
				Value:    "test",
				Path:     "/",
				Secure:   true,
				HttpOnly: true,
				SameSite: http.SameSiteStrictMode,
				Expires:  time.Unix(3000000, 0),
			},
		)
		a.groupsCache["test"] = cachedGroups{}
		a.tokenCache["test"] = cachedToken{time.Now(), &oauth2.Token{}}
		a.Logout(w, req)
		resp := w.Result()
		if resp.StatusCode != http.StatusTemporaryRedirect {
			t.Fatalf("expected redirect, got %v", resp.StatusCode)
		}
		redirect := fmt.Sprintf("%s?post_logout_redirect_uri=%s/", a.logoutEndpoint, a.fbURL)
		if got, err := resp.Location(); err != nil || got.String() != redirect {
			t.Fatalf("expected %v, got %v", redirect, got.Path)
		}
		for _, c := range resp.Cookies() {
			if c.Name == a.cookieName {
				if c.Value != "" {
					t.Fatalf("cookie not set to empty value!")
				}
				want := time.Unix(0, 0).Unix()
				got := c.Expires.Unix()
				if want != got {
					t.Fatalf("expected %v, got %v", want, got)
				}
				break
			}
		}
		if _, ok := a.groupsCache["test"]; ok {
			t.Fatalf("groups not deleted!")
		}
		if _, ok := a.tokenCache["test"]; ok {
			t.Fatalf("token not deleted!")
		}
	})
	t.Run("redirectGood", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/redirect", nil)
		w := httptest.NewRecorder()
		tkn := jwt.New(jwt.SigningMethodHS256)
		claims := tkn.Claims.(jwt.MapClaims)
		claims["oid"] = "user id"
		claims["name"] = "user name"
		expiresIn := 2 * time.Hour
		exp := time.Now().Add(expiresIn)
		expiry := strconv.Itoa(int(exp.Unix()))
		claims["exp"] = expiry
		fresh, err := tkn.SignedString(a.SecretKey())
		if err != nil {
			t.Fatalf("unexpected error when signing token %v", err)
		}
		freshToken := oauth2.Token{
			AccessToken:  fresh,
			RefreshToken: "blah",
			Expiry:       exp,
		}

		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			body := `{"access_token": "` + fresh + `", "refresh_token": "blah", "expires_in": "` + strconv.Itoa(int(expiresIn.Seconds())) + `"}`
			w.Header().Set("Content-Type", "application/json; charset=utf-8")
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(body))
		}))
		a.oAuthConfig.Endpoint.TokenURL = srv.URL
		a.Redirect(w, req)
		resp := w.Result()
		if resp.StatusCode != http.StatusTemporaryRedirect {
			t.Fatalf("expected redirect, got %v", resp.StatusCode)
		}
		if got, err := resp.Location(); err != nil || got.String() != "/" {
			t.Fatalf("expected %v, got %v", "/", got.Path)
		}
		cachedToken := a.tokenCache[fresh].token
		if cachedToken.AccessToken != freshToken.AccessToken {
			t.Fatalf("expected %v, got %v", freshToken.AccessToken, cachedToken.AccessToken)
		}
		if cachedToken.RefreshToken != freshToken.RefreshToken {
			t.Fatalf("expected %v, got %v", freshToken.RefreshToken, cachedToken.RefreshToken)
		}
		if cachedToken.Expiry.Sub(freshToken.Expiry) > time.Second {
			t.Fatalf("expected %v, got %v", freshToken.Expiry, cachedToken.Expiry)
		}
	})

	t.Run("redirectBad", func(t *testing.T) {
		req := httptest.NewRequest("GET", "/redirect", nil)
		w := httptest.NewRecorder()
		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			http.Error(w, "Server Error", http.StatusInternalServerError)
		}))
		a.oAuthConfig.Endpoint.TokenURL = srv.URL
		a.Redirect(w, req)
		resp := w.Result()
		if resp.StatusCode != http.StatusBadRequest {
			t.Fatalf("expected BadRequest, got %v", resp.StatusCode)
		}
	})

}

// This type is used for mocking ServerTransportStreams in tests
type ServerTransportStream struct {
	md     metadata.MD
	method string
}

func NewServerTransportStream() *ServerTransportStream {
	return &ServerTransportStream{
		md:     metadata.MD{},
		method: "test",
	}
}

func (s *ServerTransportStream) Method() string {
	return s.method
}

func (s *ServerTransportStream) SetHeader(md metadata.MD) error {
	s.md = md
	return nil
}

func (s *ServerTransportStream) SendHeader(md metadata.MD) error {
	_ = md
	return nil
}

func (s *ServerTransportStream) SetTrailer(md metadata.MD) error {
	_ = md
	return nil
}
