package email

import (
	"fmt"
	htmltemplate "html/template"
	"net/url"
	"testing"
	"text/template"
	"time"

	"github.com/coreos/go-oidc/jose"
	"github.com/coreos/go-oidc/key"
	"github.com/kylelemons/godebug/pretty"

	"github.com/coreos/dex/db"
	"github.com/coreos/dex/email"
	"github.com/coreos/dex/user"
)

var (
	validityWindow      = time.Hour * 1
	issuerURL           = url.URL{Host: "dex.example.com"}
	fromAddress         = "dex@example.com"
	passwordResetURL    = url.URL{Host: "dex.example.com", Path: "passwordReset"}
	verifyEmailURL      = url.URL{Host: "dex.example.com", Path: "verifyEmail"}
	acceptInvitationURL = url.URL{Host: "dex.example.com", Path: "acceptInvitation"}
	redirURL            = url.URL{Host: "client.example.com", Path: "/redirURL"}
	clientID            = "XXX"
)

type testEmailer struct {
	from, subject, text, html string
	to                        []string
	sent                      bool
}

func (t *testEmailer) SendMail(from, subject, text, html string, to ...string) error {
	t.from = from
	t.subject = subject
	t.text = text
	t.html = html
	t.to = to
	t.sent = true

	return nil
}

func makeTestFixtures() (*UserEmailer, *testEmailer, *key.PublicKey) {
	dbMap := db.NewMemDB()
	ur := func() user.UserRepo {
		repo, err := db.NewUserRepoFromUsers(dbMap, []user.UserWithRemoteIdentities{
			{
				User: user.User{
					ID:    "ID-1",
					Email: "id1@example.com",
					Admin: true,
				},
			}, {
				User: user.User{
					ID:    "ID-2",
					Email: "id2@example.com",
				},
			}, {
				User: user.User{
					ID:    "ID-3",
					Email: "id3@example.com",
				},
			},
		})
		if err != nil {
			panic("Failed to create user repo: " + err.Error())
		}
		return repo
	}()

	pwr := func() user.PasswordInfoRepo {
		repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, []user.PasswordInfo{
			{
				UserID:   "ID-1",
				Password: []byte("password-1"),
			},
			{
				UserID:   "ID-2",
				Password: []byte("password-2"),
			},
		})
		if err != nil {
			panic("Failed to create user repo: " + err.Error())
		}
		return repo
	}()

	privKey, err := key.GeneratePrivateKey()
	if err != nil {
		panic(fmt.Sprintf("Failed to generate private key, error=%v", err))
	}

	publicKey := key.NewPublicKey(privKey.JWK())
	signer := privKey.Signer()
	signerFn := func() (jose.Signer, error) {
		return signer, nil
	}

	textTemplateString := `{{define "password-reset.txt"}}{{.link}}{{end}}
{{define "verify-email.txt"}}{{.link}}{{end}}"`
	textTemplates := template.New("text")
	_, err = textTemplates.Parse(textTemplateString)
	if err != nil {
		panic(fmt.Sprintf("error parsing text templates: %v", err))
	}

	htmlTemplates := htmltemplate.New("html")

	emailer := &testEmailer{}
	tEmailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)

	userEmailer := NewUserEmailer(ur, pwr, signerFn, validityWindow, issuerURL, tEmailer, fromAddress, passwordResetURL, verifyEmailURL, acceptInvitationURL)

	return userEmailer, emailer, publicKey
}

func TestSendResetPasswordEmail(t *testing.T) {
	tests := []struct {
		email      string
		hasEmailer bool

		wantUserID   string
		wantPassword string
		wantURL      bool
		wantEmail    bool
		wantErr      bool
	}{
		{
			// typical case with an emailer.
			email:      "id1@example.com",
			hasEmailer: true,

			wantURL:      false,
			wantUserID:   "ID-1",
			wantPassword: "password-1",
			wantEmail:    true,
		},
		{

			// typical case without an emailer.
			email:      "id1@example.com",
			hasEmailer: false,

			wantURL:      true,
			wantUserID:   "ID-1",
			wantPassword: "password-1",
			wantEmail:    false,
		},
		{
			// no such user.
			email:      "noone@example.com",
			hasEmailer: false,
			wantErr:    true,
		},
		{
			// user with no local password.
			email:      "id3@example.com",
			hasEmailer: false,
			wantErr:    true,
		},
	}

	for i, tt := range tests {
		ue, emailer, pubKey := makeTestFixtures()
		if !tt.hasEmailer {
			ue.SetEmailer(nil)
		}
		resetLink, err := ue.SendResetPasswordEmail(tt.email, redirURL, clientID)
		if tt.wantErr {
			if err == nil {
				t.Errorf("case %d: want non-nil err.", i)
			}
			continue
		}

		if tt.wantURL {
			if resetLink == nil {
				t.Errorf("case %d: want non-nil resetLink", i)
				continue
			}
		} else if resetLink != nil {
			t.Errorf("case %d: want resetLink==nil, got==%v", i, resetLink.String())
			continue
		}

		if tt.wantEmail {
			if !emailer.sent {
				t.Errorf("case %d: want emailer.sent", i)
				continue
			}

			// In this case the link is in the email.
			resetLink, err = url.Parse(emailer.text)
			if err != nil {
				t.Errorf("case %d: want non-nil err, got: %q", i, err)
			}
			if tt.email != emailer.to[0] {
				t.Errorf("case %d: want==%v, got==%v", i, tt.email, emailer.to[0])
			}

			if fromAddress != emailer.from {
				t.Errorf("case %d: want==%v, got==%v", i, fromAddress, emailer.from)
			}

		} else if emailer.sent {
			t.Errorf("case %d: want !emailer.sent", i)
		}

		token := resetLink.Query().Get("token")
		pr, err := user.ParseAndVerifyPasswordResetToken(token, issuerURL,
			[]key.PublicKey{*pubKey})

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

		if tt.wantUserID != pr.UserID() {
			t.Errorf("case %d: want==%v, got==%v", i, tt.wantUserID, pr.UserID())
		}
	}
}

func TestSendEmailVerificationEmail(t *testing.T) {
	tests := []struct {
		userID     string
		hasEmailer bool

		wantEmailAddress string
		wantURL          bool
		wantEmail        bool
		wantErr          bool
	}{
		{
			// typical case with an emailer.
			userID:     "ID-1",
			hasEmailer: true,

			wantURL:          false,
			wantEmailAddress: "id1@example.com",
			wantEmail:        true,
		},
		{

			// typical case without an emailer.
			userID:     "ID-1",
			hasEmailer: false,

			wantURL:          true,
			wantEmailAddress: "id1@example.com",
			wantEmail:        false,
		},
		{
			// no such user.
			userID:     "noone@example.com",
			hasEmailer: false,
			wantErr:    true,
		},
		{
			// user with no local password.
			userID:     "id3@example.com",
			hasEmailer: false,
			wantErr:    true,
		},
	}

	for i, tt := range tests {
		ue, emailer, pubKey := makeTestFixtures()
		if !tt.hasEmailer {
			ue.SetEmailer(nil)
		}
		verifyLink, err := ue.SendEmailVerification(tt.userID, clientID, redirURL)
		if tt.wantErr {
			if err == nil {
				t.Errorf("case %d: want non-nil err.", i)
			}
			continue
		}

		if tt.wantURL {
			if verifyLink == nil {
				t.Errorf("case %d: want non-nil verifyLink", i)
				continue
			}
		} else if verifyLink != nil {
			t.Errorf("case %d: want verifyLink==nil, got==%v", i, verifyLink.String())
			continue
		}

		if tt.wantEmail {
			if !emailer.sent {
				t.Errorf("case %d: want emailer.sent", i)
				continue
			}

			// In this case the link is in the email.
			verifyLink, err = url.Parse(emailer.text)
			if err != nil {
				t.Errorf("case %d: want non-nil err, got: %q", i, err)
			}
			if tt.wantEmailAddress != emailer.to[0] {
				t.Errorf("case %d: want==%v, got==%v", i, tt.wantEmailAddress, emailer.to[0])
			}

			if fromAddress != emailer.from {
				t.Errorf("case %d: want==%v, got==%v", i, fromAddress, emailer.from)
			}

		} else if emailer.sent {
			t.Errorf("case %d: want !emailer.sent", i)
		}

		token := verifyLink.Query().Get("token")
		ev, err := user.ParseAndVerifyEmailVerificationToken(token, issuerURL,
			[]key.PublicKey{*pubKey})

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

		if tt.wantEmailAddress != ev.Email() {
			t.Errorf("case %d: want==%v, got==%v", i, tt.wantEmailAddress, ev.UserID())
		}
	}
}